from pathlib import Path from datetime import datetime from PyQt6.QtWidgets import QMessageBox, QDialog from warchron.model.model import Model from warchron.view.view import View from warchron.constants import ItemType from warchron.view.view import PlayerDialog, WarDialog, CampaignDialog class Controller: def __init__(self, model: Model, view: View): self.model = model self.view = view self.current_file: Path | None = None self.selected_war_id = None self.selected_campaign_id = None self.selected_round_id = None self.view.on_close_callback = self.on_app_close self.is_dirty = False self.__connect() self.refresh_players_view() self.refresh_wars_view() self.update_window_title() self.update_actions_state() self.view.on_tree_selection_changed = self.on_tree_selection_changed self.view.on_add_campaign = self.add_campaign self.view.on_add_round = self.add_round def __connect(self): self.view.actionExit.triggered.connect(self.view.close) self.view.actionNew.triggered.connect(self.new) self.view.actionOpen.triggered.connect(self.open_file) self.view.actionSave.triggered.connect(self.save) self.view.actionSave_as.triggered.connect(self.save_as) self.view.addPlayerBtn.clicked.connect(self.add_player) self.view.addWarBtn.clicked.connect(self.add_war) self.view.on_edit_item = self.edit_item self.view.on_delete_item = self.delete_item def on_app_close(self) -> bool: if self.is_dirty: reply = QMessageBox.question( self.view, "Unsaved changes", "You have unsaved changes. Do you want to save before quitting?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel ) if reply == QMessageBox.StandardButton.Yes: self.save() elif reply == QMessageBox.StandardButton.Cancel: return False return True def new(self): if self.is_dirty: reply = QMessageBox.question( self.view, "Unsaved changes", "Discard current campaign?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return self.model.new() self.current_file = None self.is_dirty = False self.refresh_players_view() self.refresh_wars_view() self.update_window_title() def open_file(self): if self.is_dirty: reply = QMessageBox.question( self.view, "Unsaved changes", "Discard current campaign?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return path = self.view.ask_open_file() if not path: return self.model.load(path) self.current_file = path self.is_dirty = False self.refresh_players_view() self.refresh_wars_view() self.update_window_title() def save(self): if not self.current_file: self.save_as() return self.model.save(self.current_file) self.is_dirty = False self.update_window_title() def save_as(self): path = self.view.ask_save_file() if not path: return self.current_file = path self.model.save(path) self.is_dirty = False self.update_window_title() def update_window_title(self): base = "WarChron" if self.current_file: base += f" - {self.current_file.name}" else: base += f" - New file" if self.is_dirty: base = base + " *" self.view.setWindowTitle(base) def refresh_players_view(self): players = self.model.get_all_players() self.view.display_players(players) def refresh_wars_view(self): wars = self.model.get_all_wars() self.view.display_wars_tree(wars) def refresh_views(self): current = self.view.get_current_tab() if current == "players": self.refresh_players_view() elif current == "wars": self.refresh_wars_view() def _fill_war_details(self, war_id: str): war = self.model.get_war(war_id) self.view.warName.setText(war.name) self.view.warYear.setText(str(war.year)) def _fill_campaign_details(self, campaign_id: str): camp = self.model.get_campaign(campaign_id) self.view.show_campaign_details(name=camp.name, month=camp.month) def _fill_round_details(self, round_id: str): index = self.model.get_round_index(round_id) self.view.show_round_details(index=index) def on_tree_selection_changed(self, selection): self.selected_war_id = None self.selected_campaign_id = None self.selected_round_id = None if selection: item_type = selection["type"] item_id = selection["id"] if item_type == ItemType.WAR: self.selected_war_id = item_id self.view.show_details(ItemType.WAR) self._fill_war_details(item_id) elif item_type == ItemType.CAMPAIGN: self.selected_campaign_id = item_id self.view.show_details(ItemType.CAMPAIGN) self._fill_campaign_details(item_id) elif item_type == ItemType.ROUND: self.selected_round_id = item_id self.view.show_details(ItemType.ROUND) self._fill_round_details(item_id) else: self.view.show_details(None) self.update_actions_state() return self.update_actions_state() def update_actions_state(self): self.view.set_add_campaign_enabled(self.selected_war_id is not None) self.view.set_add_round_enabled(self.selected_campaign_id is not None) def _validate_player_inputs(self, name: str) -> bool: if not name.strip(): QMessageBox.warning( self.view, "Invalid name", "Player name cannot be empty." ) return False return True def add_player(self): dialog = PlayerDialog(self.view) result = dialog.exec() # modal blocking dialog if result == QDialog.DialogCode.Accepted: name = dialog.get_player_name() if not self._validate_player_inputs(name): return self.model.add_player(name) self.is_dirty = True self.refresh_players_view() self.update_window_title() def _validate_war_inputs(self, name: str, year: int) -> bool: if not name.strip(): QMessageBox.warning( self.view, "Invalid name", "War name cannot be empty." ) return False if not (1970 <= year <= 3000): QMessageBox.warning( self.view, "Invalid year", "Year must be between 1970 and 3000." ) return False return True def add_war(self): dialog = WarDialog(self.view, default_year=self.model.get_default_war_values()["year"]) result = dialog.exec() # modal blocking dialog if result == QDialog.DialogCode.Accepted: name = dialog.get_war_name() year = dialog.get_war_year() if not self._validate_war_inputs(name, year): return self.model.add_war(name, year) self.is_dirty = True self.refresh_wars_view() self.update_window_title() def _validate_campaign_inputs(self, name: str, month: int) -> bool: if not name.strip(): QMessageBox.warning( self.view, "Invalid name", "Campaign name cannot be empty." ) return False if not (1 <= month <= 12): QMessageBox.warning( self.view, "Invalid month", "Month must be between 1 and 12." ) return False return True def add_campaign(self): if not self.selected_war_id: return dialog = CampaignDialog(self.view, default_month=self.model.get_default_campaign_values(self.selected_war_id)["month"]) if dialog.exec() != QDialog.DialogCode.Accepted: return name = dialog.get_campaign_name() month = dialog.get_campaign_month() if not self._validate_campaign_inputs(name, month): return self.model.add_campaign(self.selected_war_id, name, month) self.is_dirty = True self.refresh_wars_view() self.update_window_title() def add_round(self): if not self.selected_campaign_id: return self.model.add_round(self.selected_campaign_id) self.is_dirty = True self.refresh_wars_view() self.update_window_title() def edit_item(self, item_type: str, item_id: str): if item_type == "player": play = self.model.get_player(item_id) dialog = PlayerDialog(self.view, default_name=play.name) if dialog.exec() == QDialog.DialogCode.Accepted: name = dialog.get_player_name() if not self._validate_player_inputs(name): return self.model.update_player(item_id, name=name) elif item_type == "war": war = self.model.get_war(item_id) dialog = WarDialog(self.view, default_name=war.name, default_year=war.year) if dialog.exec() == QDialog.DialogCode.Accepted: name = dialog.get_war_name() year = dialog.get_war_year() if not self._validate_war_inputs(name, year): return self.model.update_war(item_id, name=name, year=year) elif item_type == "campaign": camp = self.model.get_campaign(item_id) dialog = CampaignDialog(self.view, default_name=camp.name, default_month=camp.month) if dialog.exec() == QDialog.DialogCode.Accepted: name = dialog.get_campaign_name() month = dialog.get_campaign_month() if not self._validate_campaign_inputs(name, month): return self.model.update_campaign(item_id, name=name, month=month) self.is_dirty = True self.refresh_views() def delete_item(self, item_type: str, item_id: str): reply = QMessageBox.question( self.view, "Confirm deletion", "Are you sure you want to delete this item?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return if item_type == "player": self.model.remove_player(item_id) elif item_type == "war": self.model.remove_war(item_id) elif item_type == "campaign": self.model.remove_campaign(item_id) elif item_type == "round": self.model.remove_round(item_id) self.is_dirty = True self.refresh_views()