from __future__ import annotations from typing import Callable, List, Dict from pathlib import Path import calendar from PyQt6 import QtWidgets from PyQt6.QtCore import Qt, QPoint, QSize from PyQt6.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent from warchron.constants import ROLE_TYPE, ROLE_ID, ItemType, Icons, IconName from warchron.controller.dtos import ( ParticipantOption, TreeSelection, WarDTO, WarParticipantDTO, ObjectiveDTO, SectorDTO, ChoiceDTO, BattleDTO, CampaignParticipantScoreDTO, ) from warchron.view.helpers import ( format_campaign_label, format_round_label, format_war_label, ) from warchron.view.ui.ui_main_window import Ui_MainWindow class View(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, parent: QWidget | None = None) -> None: super(View, self).__init__(parent) self.setupUi(self) # type: ignore self.on_close_callback: Callable[[], bool] | None = None self.on_tree_selection_changed: ( Callable[[TreeSelection | None], None] | None ) = None self.on_major_value_changed: Callable[[int], None] | None = None self.on_minot_value_changed: Callable[[int], None] | None = None self.majorValue.setMinimum(0) self.minorValue.setMinimum(0) self.on_influence_token_changed: Callable[[int], None] | None = None self.on_add_item: Callable[[str], None] | None = None self.on_edit_item: Callable[[str, str], None] | None = None self.on_delete_item: Callable[[str, str], None] | None = None self.splitter.setSizes([200, 800]) self.show_details(None) self.playersTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.playersTable.customContextMenuRequested.connect( self._on_players_table_context_menu ) self.warsTree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.warsTree.customContextMenuRequested.connect( self._on_wars_tree_context_menu ) self.addCampaignBtn.clicked.connect(self._on_add_campaign_clicked) self.addRoundBtn.clicked.connect(self._on_add_round_clicked) # Pages self.warParticipantsTable.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu ) self.warParticipantsTable.customContextMenuRequested.connect( self._on_war_participants_table_context_menu ) self.objectivesTable.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu ) self.objectivesTable.customContextMenuRequested.connect( self._on_objectives_table_context_menu ) self.campaignParticipantsTable.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu ) self.campaignParticipantsTable.customContextMenuRequested.connect( self._on_campaign_participants_table_context_menu ) self.sectorsTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.sectorsTable.customContextMenuRequested.connect( self._on_sectors_table_context_menu ) self.choicesTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.choicesTable.customContextMenuRequested.connect( self._on_choices_table_context_menu ) self.battlesTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.battlesTable.customContextMenuRequested.connect( self._on_battles_table_context_menu ) self.majorValue.valueChanged.connect(self._on_major_changed) self.minorValue.valueChanged.connect(self._on_minor_changed) self._apply_icons() def _apply_icons(self) -> None: # Window self.setWindowIcon(Icons.get(IconName.WARCHRON)) # Menu bar self.actionNew.setIcon(Icons.get(IconName.NEW)) self.actionOpen.setIcon(Icons.get(IconName.OPEN)) self.actionSave.setIcon(Icons.get(IconName.SAVE)) self.actionSave_as.setIcon(Icons.get(IconName.SAVE_AS)) self.actionExit.setIcon(Icons.get(IconName.EXIT)) self.actionUndo.setIcon(Icons.get(IconName.UNDO)) self.actionRedo.setIcon(Icons.get(IconName.REDO)) self.actionExport.setIcon(Icons.get(IconName.EXPORT)) self.actionAbout.setIcon(Icons.get(IconName.ABOUT)) # Tabs self.tabWidget.setTabIcon(0, Icons.get(IconName.PLAYERS)) self.tabWidget.setTabIcon(1, Icons.get(IconName.WARS)) # Buttons self.addPlayerBtn.setIcon(Icons.get(IconName.ADD)) self.addWarBtn.setIcon(Icons.get(IconName.ADD)) self.addCampaignBtn.setIcon(Icons.get(IconName.ADD)) self.addRoundBtn.setIcon(Icons.get(IconName.ADD)) self.addObjectiveBtn.setIcon(Icons.get(IconName.ADD)) self.addWarParticipantBtn.setIcon(Icons.get(IconName.ADD)) self.endWarBtn.setIcon(Icons.get(IconName.END)) self.addSectorBtn.setIcon(Icons.get(IconName.ADD)) self.addCampaignParticipantBtn.setIcon(Icons.get(IconName.ADD)) self.endCampaignBtn.setIcon(Icons.get(IconName.END)) self.resolvePairingBtn.setIcon(Icons.get(IconName.PAIRING)) self.endRoundBtn.setIcon(Icons.get(IconName.END)) def _emit_selection_changed(self, current: QTreeWidgetItem | None) -> None: if not self.on_tree_selection_changed: return if not current: self.on_tree_selection_changed(None) return self.on_tree_selection_changed( TreeSelection( type=current.data(0, ROLE_TYPE), id=current.data(0, ROLE_ID), ) ) def get_current_tab(self) -> str: index = self.tabWidget.currentIndex() if index == 0: return "players" elif index == 1: return "wars" return "" # General popups def closeEvent(self, event: QCloseEvent | None = None) -> None: if event is None: return if self.on_close_callback: proceed = self.on_close_callback() if not proceed: event.ignore() return event.accept() def ask_open_file(self) -> Path | None: filename, _ = QFileDialog.getOpenFileName( self, "Open war history", "", "WarChron files (*.json)" ) return Path(filename) if filename else None def ask_save_file(self) -> Path | None: filename, _ = QFileDialog.getSaveFileName( self, "Save war history", "", "WarChron files (*.json)" ) return Path(filename) if filename else None # Players view def _on_players_table_context_menu(self, pos: QPoint) -> None: item = self.playersTable.itemAt(pos) if not item: return row = item.row() name_item = self.playersTable.item(row, 0) if not name_item: return player_id = name_item.data(Qt.ItemDataRole.UserRole) menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") delete_action = menu.addAction(Icons.get(IconName.DELETE), "Delete") viewport = self.playersTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.PLAYER, player_id) elif action == delete_action and self.on_delete_item: self.on_delete_item(ItemType.PLAYER, player_id) def display_players(self, players: List[ParticipantOption]) -> None: table = self.playersTable table.setRowCount(len(players)) for row, player in enumerate(players): play_item = QtWidgets.QTableWidgetItem(player.name) play_item.setData(Qt.ItemDataRole.UserRole, player.id) table.setItem(row, 0, play_item) table.resizeColumnsToContents() # Wars view def _on_add_campaign_clicked(self) -> None: if self.on_add_item: self.on_add_item(ItemType.CAMPAIGN) def _on_add_round_clicked(self) -> None: if self.on_add_item: self.on_add_item(ItemType.ROUND) def set_add_campaign_enabled(self, enabled: bool) -> None: self.addCampaignBtn.setEnabled(enabled) def set_add_round_enabled(self, enabled: bool) -> None: self.addRoundBtn.setEnabled(enabled) def _on_wars_tree_context_menu(self, pos: QPoint) -> None: item = self.warsTree.itemAt(pos) if not item: return item_type = item.data(0, ROLE_TYPE) item_id = item.data(0, ROLE_ID) menu = QMenu(self) edit_action = None if item_type != ItemType.ROUND: edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") delete_action = menu.addAction(Icons.get(IconName.DELETE), "Delete") viewport = self.warsTree.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action: if self.on_edit_item: self.on_edit_item(item_type, item_id) elif action == delete_action: if self.on_delete_item: self.on_delete_item(item_type, item_id) def display_wars_tree(self, wars: List[WarDTO]) -> None: tree = self.warsTree try: tree.currentItemChanged.disconnect() except TypeError: pass tree.clear() tree.setColumnCount(1) tree.setHeaderLabels(["Wars"]) hourglass = Icons.get(IconName.ONGOING) check = Icons.get(IconName.DONE) for war in wars: war_item = QTreeWidgetItem([format_war_label(war)]) war_item.setIcon(0, check if war.is_over else hourglass) war_item.setData(0, ROLE_TYPE, ItemType.WAR) war_item.setData(0, ROLE_ID, war.id) tree.addTopLevelItem(war_item) for camp in war.get_all_campaigns(): camp_item = QTreeWidgetItem([format_campaign_label(camp)]) camp_item.setIcon(0, check if camp.is_over else hourglass) camp_item.setData(0, ROLE_TYPE, ItemType.CAMPAIGN) camp_item.setData(0, ROLE_ID, camp.id) war_item.addChild(camp_item) for index, rnd in enumerate(camp.get_all_rounds(), start=1): rnd_item = QTreeWidgetItem([format_round_label(index)]) rnd_item.setIcon(0, check if rnd.is_over else hourglass) rnd_item.setData(0, ROLE_TYPE, ItemType.ROUND) rnd_item.setData(0, ROLE_ID, rnd.id) camp_item.addChild(rnd_item) tree.currentItemChanged.connect(self._emit_selection_changed) tree.expandAll() def select_tree_item(self, *, item_type: ItemType, item_id: str) -> None: def walk(item: QTreeWidgetItem) -> bool: if ( item.data(0, ROLE_TYPE) == item_type and item.data(0, ROLE_ID) == item_id ): self.warsTree.setCurrentItem(item) return True for i in range(item.childCount()): # if walk(item.child(i)): ytem = item.child(i) if ytem is not None and walk(ytem): return True return False for i in range(self.warsTree.topLevelItemCount()): # if walk(self.warsTree.topLevelItem(i)): item = self.warsTree.topLevelItem(i) if item is not None and walk(item): return def get_selected_tree_item(self) -> Dict[str, str] | None: item = self.warsTree.currentItem() if not item: return None return {"type": item.data(0, ROLE_TYPE), "id": item.data(0, ROLE_ID)} def show_details(self, item_type: str | None) -> None: if item_type == ItemType.WAR: self.selectedDetailsStack.setCurrentWidget(self.pageWar) elif item_type == ItemType.CAMPAIGN: self.selectedDetailsStack.setCurrentWidget(self.pageCampaign) elif item_type == ItemType.ROUND: self.selectedDetailsStack.setCurrentWidget(self.pageRound) else: self.selectedDetailsStack.setCurrentWidget(self.pageEmpty) # War page def _on_objectives_table_context_menu(self, pos: QPoint) -> None: item = self.objectivesTable.itemAt(pos) if not item: return row = item.row() name_item = self.objectivesTable.item(row, 0) if not name_item: return objective_id = name_item.data(Qt.ItemDataRole.UserRole) menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") delete_action = menu.addAction(Icons.get(IconName.DELETE), "Delete") viewport = self.objectivesTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.OBJECTIVE, objective_id) elif action == delete_action and self.on_delete_item: self.on_delete_item(ItemType.OBJECTIVE, objective_id) def _on_war_participants_table_context_menu(self, pos: QPoint) -> None: item = self.warParticipantsTable.itemAt(pos) if not item: return row = item.row() name_item = self.warParticipantsTable.item(row, 0) if not name_item: return participant_id = name_item.data(Qt.ItemDataRole.UserRole) menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") delete_action = menu.addAction(Icons.get(IconName.DELETE), "Delete") viewport = self.warParticipantsTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.WAR_PARTICIPANT, participant_id) elif action == delete_action and self.on_delete_item: self.on_delete_item(ItemType.WAR_PARTICIPANT, participant_id) def show_war_details(self, *, name: str, year: int) -> None: self.warName.setText(name) self.warYear.setText(str(year)) def display_war_objectives(self, objectives: List[ObjectiveDTO]) -> None: table = self.objectivesTable table.clearContents() table.setRowCount(len(objectives)) for row, obj in enumerate(objectives): name_item = QtWidgets.QTableWidgetItem(obj.name) desc_item = QtWidgets.QTableWidgetItem(obj.description) name_item.setData(Qt.ItemDataRole.UserRole, obj.id) table.setItem(row, 0, name_item) table.setItem(row, 1, desc_item) table.resizeColumnsToContents() def display_war_participants(self, participants: List[WarParticipantDTO]) -> None: table = self.warParticipantsTable table.clearContents() table.setRowCount(len(participants)) for row, part in enumerate(participants): name_item = QtWidgets.QTableWidgetItem(part.player_name) fact_item = QtWidgets.QTableWidgetItem(part.faction) name_item.setData(Qt.ItemDataRole.UserRole, part.id) table.setItem(row, 0, name_item) table.setItem(row, 1, fact_item) table.resizeColumnsToContents() def _on_major_changed(self, value: int) -> None: self.minorValue.setMaximum(value) def _on_minor_changed(self, value: int) -> None: self.majorValue.setMinimum(value) def set_war_objective_values(self, major: int, minor: int, influence: bool) -> None: self.majorValue.blockSignals(True) self.minorValue.blockSignals(True) self.influenceToken.blockSignals(True) self.majorValue.setValue(major) self.minorValue.setValue(minor) self.influenceToken.setChecked(influence) self.minorValue.setMaximum(major) self.majorValue.setMinimum(minor) self.majorValue.blockSignals(False) self.minorValue.blockSignals(False) self.influenceToken.blockSignals(False) # Campaign page def _on_sectors_table_context_menu(self, pos: QPoint) -> None: item = self.sectorsTable.itemAt(pos) if not item: return row = item.row() name_item = self.sectorsTable.item(row, 0) if not name_item: return sector_id = name_item.data(Qt.ItemDataRole.UserRole) menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") delete_action = menu.addAction(Icons.get(IconName.DELETE), "Delete") viewport = self.sectorsTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.SECTOR, sector_id) elif action == delete_action and self.on_delete_item: self.on_delete_item(ItemType.SECTOR, sector_id) def _on_campaign_participants_table_context_menu(self, pos: QPoint) -> None: item = self.campaignParticipantsTable.itemAt(pos) if not item: return row = item.row() name_item = self.campaignParticipantsTable.item(row, 0) if not name_item: return participant_id = name_item.data(Qt.ItemDataRole.UserRole) menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") delete_action = menu.addAction(Icons.get(IconName.DELETE), "Delete") viewport = self.campaignParticipantsTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.CAMPAIGN_PARTICIPANT, participant_id) elif action == delete_action and self.on_delete_item: self.on_delete_item(ItemType.CAMPAIGN_PARTICIPANT, participant_id) def show_campaign_details(self, *, name: str, month: int) -> None: self.campaignName.setText(name) self.campaignMonth.setText(calendar.month_name[month]) def display_campaign_sectors(self, sectors: List[SectorDTO]) -> None: table = self.sectorsTable table.clearContents() table.setRowCount(len(sectors)) for row, sect in enumerate(sectors): name_item = QtWidgets.QTableWidgetItem(sect.name) round_item = QtWidgets.QTableWidgetItem( format_round_label(sect.round_index) ) mission_item = QtWidgets.QTableWidgetItem(sect.mission) major_item = QtWidgets.QTableWidgetItem(sect.major) minor_item = QtWidgets.QTableWidgetItem(sect.minor) influence_item = QtWidgets.QTableWidgetItem(sect.influence) description_item = QtWidgets.QTableWidgetItem(sect.description) name_item.setData(Qt.ItemDataRole.UserRole, sect.id) table.setItem(row, 0, name_item) table.setItem(row, 1, round_item) table.setItem(row, 2, mission_item) table.setItem(row, 3, major_item) table.setItem(row, 4, minor_item) table.setItem(row, 5, influence_item) table.setItem(row, 6, description_item) table.resizeColumnsToContents() def display_campaign_participants( self, participants: List[CampaignParticipantScoreDTO], objectives: List[ObjectiveDTO], ) -> None: table = self.campaignParticipantsTable table.clearContents() base_cols = ["Player", "Leader", "Theme", "Victory"] headers = base_cols + [obj.name for obj in objectives] table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) table.setRowCount(len(participants)) table.setIconSize(QSize(32, 16)) for row, part in enumerate(participants): name_item = QtWidgets.QTableWidgetItem(part.player_name) if part.rank_icon: name_item.setIcon(part.rank_icon) lead_item = QtWidgets.QTableWidgetItem(part.leader) theme_item = QtWidgets.QTableWidgetItem(part.theme) VP_item = QtWidgets.QTableWidgetItem(str(part.victory_points)) name_item.setData(Qt.ItemDataRole.UserRole, part.campaign_participant_id) table.setItem(row, 0, name_item) table.setItem(row, 1, lead_item) table.setItem(row, 2, theme_item) table.setItem(row, 3, VP_item) col = 4 for obj in objectives: value = part.narrative_points.get(obj.id, 0) NP_item = QtWidgets.QTableWidgetItem(str(value)) table.setItem(row, col, NP_item) col += 1 table.resizeColumnsToContents() # Round page def _on_choices_table_context_menu(self, pos: QPoint) -> None: item = self.choicesTable.itemAt(pos) if not item: return row = item.row() name_item = self.choicesTable.item(row, 0) if not name_item: return choice_id = name_item.data(Qt.ItemDataRole.UserRole) if choice_id is None: return menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") viewport = self.choicesTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.CHOICE, choice_id) def _on_battles_table_context_menu(self, pos: QPoint) -> None: item = self.battlesTable.itemAt(pos) if not item: return row = item.row() name_item = self.battlesTable.item(row, 0) if not name_item: return battle_id = name_item.data(Qt.ItemDataRole.UserRole) if battle_id is None: return menu = QMenu(self) edit_action = menu.addAction(Icons.get(IconName.EDIT), "Edit") viewport = self.battlesTable.viewport() assert viewport is not None action = menu.exec(viewport.mapToGlobal(pos)) if action == edit_action and self.on_edit_item: self.on_edit_item(ItemType.BATTLE, battle_id) def show_round_details(self, *, index: int | None) -> None: self.roundNb.setText(f"Round {index}") def display_round_choices(self, participants: List[ChoiceDTO]) -> None: table = self.choicesTable table.clearContents() table.setRowCount(len(participants)) for row, choice in enumerate(participants): participant_item = QtWidgets.QTableWidgetItem(choice.participant_name) priority_item = QtWidgets.QTableWidgetItem(choice.priority_sector) secondary_item = QtWidgets.QTableWidgetItem(choice.secondary_sector) participant_item.setData(Qt.ItemDataRole.UserRole, choice.id) table.setItem(row, 0, participant_item) table.setItem(row, 1, priority_item) table.setItem(row, 2, secondary_item) table.resizeColumnsToContents() def display_round_battles(self, sectors: List[BattleDTO]) -> None: table = self.battlesTable table.clearContents() table.setRowCount(len(sectors)) table.setIconSize(QSize(32, 16)) for row, battle in enumerate(sectors): sector_item = QtWidgets.QTableWidgetItem(battle.sector_name) if battle.state_icon: sector_item.setIcon(battle.state_icon) player_1_item = QtWidgets.QTableWidgetItem(battle.player_1) if battle.player1_icon: player_1_item.setIcon(battle.player1_icon) player_2_item = QtWidgets.QTableWidgetItem(battle.player_2) if battle.player2_icon: player_2_item.setIcon(battle.player2_icon) score_item = QtWidgets.QTableWidgetItem(battle.score) vp_item = QtWidgets.QTableWidgetItem(battle.victory_condition) comment_item = QtWidgets.QTableWidgetItem(battle.comment) sector_item.setData(Qt.ItemDataRole.UserRole, battle.id) table.setItem(row, 0, sector_item) table.setItem(row, 1, player_1_item) table.setItem(row, 2, player_2_item) table.setItem(row, 3, vp_item) table.setItem(row, 4, score_item) table.setItem(row, 5, comment_item) table.resizeColumnsToContents()