from typing import Callable, List from pathlib import Path import calendar from PyQt6 import QtWidgets from PyQt6.QtCore import Qt, QPoint from PyQt6.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent, QIcon from warchron.constants import ROLE_TYPE, ROLE_ID, ItemType from warchron.controller.dtos import ( ParticipantOption, TreeSelection, WarDTO, WarParticipantDTO, ObjectiveDTO, CampaignParticipantDTO, SectorDTO, ChoiceDTO, BattleDTO, ) 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_campaign: Callable[[], None] | None = None self.on_add_round: Callable[[], 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) 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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "Edit" ) delete_action = menu.addAction( QIcon(".\\src\\warchron\\view\\ui\\../resources/cross.png"), "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_campaign: self.on_add_campaign() def _on_add_round_clicked(self) -> None: if self.on_add_round: self.on_add_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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "Edit" ) delete_action = menu.addAction( QIcon(".\\src\\warchron\\view\\ui\\../resources/cross.png"), "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 tree.clear() tree.setColumnCount(1) tree.setHeaderLabels(["Wars"]) for war in wars: war_item = QTreeWidgetItem([format_war_label(war)]) 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.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.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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "Edit" ) delete_action = menu.addAction( QIcon(".\\src\\warchron\\view\\ui\\../resources/cross.png"), "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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "Edit" ) delete_action = menu.addAction( QIcon(".\\src\\warchron\\view\\ui\\../resources/cross.png"), "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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "Edit" ) delete_action = menu.addAction( QIcon(".\\src\\warchron\\view\\ui\\../resources/cross.png"), "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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "Edit" ) delete_action = menu.addAction( QIcon(".\\src\\warchron\\view\\ui\\../resources/cross.png"), "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) ) major_item = QtWidgets.QTableWidgetItem(sect.major) minor_item = QtWidgets.QTableWidgetItem(sect.minor) influence_item = QtWidgets.QTableWidgetItem(sect.influence) name_item.setData(Qt.ItemDataRole.UserRole, sect.id) table.setItem(row, 0, name_item) table.setItem(row, 1, round_item) table.setItem(row, 2, major_item) table.setItem(row, 3, minor_item) table.setItem(row, 4, influence_item) table.resizeColumnsToContents() def display_campaign_participants( self, participants: List[CampaignParticipantDTO] ) -> None: table = self.campaignParticipantsTable table.clearContents() table.setRowCount(len(participants)) for row, part in enumerate(participants): name_item = QtWidgets.QTableWidgetItem(part.player_name) lead_item = QtWidgets.QTableWidgetItem(part.leader) theme_item = QtWidgets.QTableWidgetItem(part.theme) name_item.setData(Qt.ItemDataRole.UserRole, part.id) table.setItem(row, 0, name_item) table.setItem(row, 1, lead_item) table.setItem(row, 2, theme_item) 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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "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( QIcon(".\\src\\warchron\\view\\ui\\../resources/pencil.png"), "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)) for row, battle in enumerate(sectors): sector_item = QtWidgets.QTableWidgetItem(battle.sector_name) player_1_item = QtWidgets.QTableWidgetItem(battle.player_1) player_2_item = QtWidgets.QTableWidgetItem(battle.player_2) 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.resizeColumnsToContents()