from pathlib import Path from PyQt6.QtWidgets import QMessageBox from warchron.model.model import Model from warchron.model.exception import DomainError, RequiresConfirmation from warchron.view.view import View from warchron.constants import ItemType, RefreshScope from warchron.controller.navigation_controller import NavigationController from warchron.controller.player_controller import PlayerController from warchron.controller.war_controller import WarController from warchron.controller.campaign_controller import CampaignController from warchron.controller.round_controller import RoundController class AppController: def __init__(self, model: Model, view: View, version: str) -> None: self.model: Model = model self.view: View = view self.app_version = version self.navigation = NavigationController(self) self.players = PlayerController(self) self.wars = WarController(self) self.campaigns = CampaignController(self) self.rounds = RoundController(self) self.current_file: Path | None = None self.view.on_close_callback = self.on_app_close self.is_dirty: bool = False self.__connect() self.navigation.refresh_players_view() self.navigation.refresh_wars_view() self.update_window_title() self.view.on_tree_selection_changed = self.navigation.on_tree_selection_changed self.view.on_add_item = self.add_item def __connect(self) -> None: 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.actionAbout.triggered.connect(self.show_about) self.view.addPlayerBtn.clicked.connect(lambda: self.add_item(ItemType.PLAYER)) self.view.addWarBtn.clicked.connect(lambda: self.add_item(ItemType.WAR)) self.view.majorValue.valueChanged.connect(self.wars.set_major_value) self.view.minorValue.valueChanged.connect(self.wars.set_minor_value) self.view.influenceToken.toggled.connect(self.wars.set_influence_token) self.view.addObjectiveBtn.clicked.connect( lambda: self.add_item(ItemType.OBJECTIVE) ) self.view.addWarParticipantBtn.clicked.connect( lambda: self.add_item(ItemType.WAR_PARTICIPANT) ) self.view.endWarBtn.clicked.connect(self.wars.close_war) self.view.addSectorBtn.clicked.connect(lambda: self.add_item(ItemType.SECTOR)) self.view.addCampaignParticipantBtn.clicked.connect( lambda: self.add_item(ItemType.CAMPAIGN_PARTICIPANT) ) self.view.endCampaignBtn.clicked.connect(self.campaigns.close_campaign) self.view.endRoundBtn.clicked.connect(self.rounds.close_round) self.view.resolvePairingBtn.clicked.connect(self.rounds.resolve_pairing) self.view.on_add_item = self.add_item 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 # Menu bar methods def new(self) -> None: 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.navigation.refresh_players_view() self.navigation.refresh_wars_view() self.update_window_title() def open_file(self) -> None: 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 try: self.model.load(path) except RuntimeError as e: QMessageBox.warning( self.view, "Add forbidden", str(e), ) return self.current_file = path self.is_dirty = False self.navigation.refresh_players_view() self.navigation.refresh_wars_view() self.update_window_title() def save(self) -> None: 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) -> None: 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 show_about(self) -> None: QMessageBox.about( self.view, "About WarChron", f"""

WarChron

Version: {self.app_version}

Campaign & War management tool

© 2026 Your Name

Licensed under GNU GPL v3


Icons from Fugue Icons 3.5.6
© Yusuke Kamiyamane
Licensed under Creative Commons Attribution 3.0

""", ) # Display methods def update_window_title(self) -> None: base = "WarChron" if self.current_file: base += f" - {self.current_file.name}" else: base += " - New file" if self.is_dirty: base = base + " *" self.view.setWindowTitle(base) # Command methods def add_item(self, item_type: str) -> None: try: if item_type == ItemType.PLAYER: play = self.players.create_player() if not play: return self.navigation.refresh(RefreshScope.PLAYERS_LIST) elif item_type == ItemType.WAR: war = self.wars.create_war() if not war: return self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id ) elif item_type == ItemType.CAMPAIGN: camp = self.campaigns.create_campaign() if not camp: return self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id ) elif item_type == ItemType.OBJECTIVE: obj = self.wars.create_objective() if not obj: return self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.WAR_PARTICIPANT: war_part = self.wars.create_war_participant() if not war_part: return self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.SECTOR: sect = self.campaigns.create_sector() if not sect: return self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.CAMPAIGN_PARTICIPANT: camp_part = self.campaigns.create_campaign_participant() if not camp_part: return self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.ROUND: rnd = self.rounds.create_round() if not rnd: return self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id ) except DomainError as e: QMessageBox.warning( self.view, "Add forbidden", str(e), ) return except RequiresConfirmation as e: reply = QMessageBox.question( self.view, "Confirm update", str(e), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: e.action() else: return self.is_dirty = True self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def edit_item(self, item_type: str, item_id: str) -> None: try: if item_type == ItemType.PLAYER: self.players.edit_player(item_id) self.navigation.refresh(RefreshScope.PLAYERS_LIST) elif item_type == ItemType.WAR: self.wars.edit_war(item_id) self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=item_id ) elif item_type == ItemType.CAMPAIGN: self.campaigns.edit_campaign(item_id) self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=item_id ) elif item_type == ItemType.OBJECTIVE: self.wars.edit_objective(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.WAR_PARTICIPANT: self.wars.edit_war_participant(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.SECTOR: self.campaigns.edit_sector(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.CAMPAIGN_PARTICIPANT: self.campaigns.edit_campaign_participant(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.CHOICE: self.rounds.edit_round_choice(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.BATTLE: self.rounds.edit_round_battle(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) except DomainError as e: QMessageBox.warning( self.view, "Update forbidden", str(e), ) return except RequiresConfirmation as e: reply = QMessageBox.question( self.view, "Confirm update", str(e), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: try: e.action() except DomainError as inner: QMessageBox.warning( self.view, "Update forbidden", str(inner), ) return else: return self.is_dirty = True self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def delete_item(self, item_type: str, item_id: str) -> None: 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 try: if item_type == ItemType.PLAYER: self.model.remove_player(item_id) self.navigation.refresh(RefreshScope.PLAYERS_LIST) elif item_type == ItemType.WAR: self.model.remove_war(item_id) self.navigation.refresh(RefreshScope.WARS_TREE) elif item_type == ItemType.CAMPAIGN: war = self.model.get_war_by_campaign(item_id) war_id = war.id self.model.remove_campaign(item_id) self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id ) elif item_type == ItemType.OBJECTIVE: self.model.remove_objective(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.WAR_PARTICIPANT: self.model.remove_war_participant(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.SECTOR: self.model.remove_sector(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.CAMPAIGN_PARTICIPANT: self.model.remove_campaign_participant(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) elif item_type == ItemType.ROUND: camp = self.model.get_campaign_by_round(item_id) camp_id = camp.id self.model.remove_round(item_id) self.navigation.refresh_and_select( RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id ) except DomainError as e: QMessageBox.warning( self.view, "Deletion forbidden", str(e), ) return except RequiresConfirmation as e: reply = QMessageBox.question( self.view, "Confirm deletion", str(e), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: e.action() else: return self.is_dirty = True self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)