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.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(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 on_tree_selection_changed(self, selection): self.selected_war_id = None self.selected_campaign_id = None self.selected_round_id = None if selection: if selection["type"] == "war": self.selected_war_id = selection["id"] elif selection["type"] == "campaign": self.selected_campaign_id = selection["id"] elif selection["type"] == "round": self.selected_round_id = selection["id"] 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()