from pathlib import Path import calendar from PyQt6 import QtWidgets from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QDialog, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent from warchron.constants import ROLE_TYPE, ROLE_ID, ItemType from warchron.view.ui.ui_main_window import Ui_MainWindow from warchron.view.ui.ui_player_dialog import Ui_playerDialog from warchron.view.ui.ui_war_dialog import Ui_warDialog from warchron.view.ui.ui_campaign_dialog import Ui_campaignDialog from warchron.view.ui.ui_objective_dialog import Ui_objectiveDialog from warchron.view.ui.ui_war_participant_dialog import Ui_warParticipantDialog from warchron.view.ui.ui_campaign_participant_dialog import Ui_campaignParticipantDialog from warchron.view.ui.ui_sector_dialog import Ui_sectorDialog from warchron.controller.dtos import ParticipantOption # utils... def select_if_exists(combo, value): if value is None: return idx = combo.findData(value) if idx != -1: combo.setCurrentIndex(idx) def format_war_label(war) -> str: return f"{war.name} ({war.year})" def format_campaign_label(camp) -> str: return f"{camp.name} ({calendar.month_name[camp.month]})" def format_round_label(round, index: int) -> str: if index is None: return "" return f"Round {index}" class View(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super(View, self).__init__(parent) self.setupUi(self) self.on_close_callback = None self.on_selection_changed = None self.on_add_campaign = None self.on_add_round = None self.on_edit_item = None self.on_delete_item = None self.show_details(None) self.playersTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.playersTable.customContextMenuRequested.connect(self._on_players_table_context_menu) 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.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) def _emit_selection_changed(self, current, previous): if not self.on_tree_selection_changed: return if not current: self.on_tree_selection_changed(None) return self.on_tree_selection_changed({ "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): 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): 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("Edit") delete_action = menu.addAction("Delete") action = menu.exec(self.playersTable.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): 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): if self.on_add_campaign: self.on_add_campaign() def _on_add_round_clicked(self): if self.on_add_round: self.on_add_round() def set_add_campaign_enabled(self, enabled: bool): self.addCampaignBtn.setEnabled(enabled) def set_add_round_enabled(self, enabled: bool): self.addRoundBtn.setEnabled(enabled) def _on_wars_tree_context_menu(self, pos): 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("Edit") delete_action = menu.addAction("Delete") action = menu.exec(self.warsTree.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): 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(rnd, 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): def walk(item: QTreeWidgetItem): 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)): return True return False for i in range(self.warsTree.topLevelItemCount()): if walk(self.warsTree.topLevelItem(i)): return def get_selected_tree_item(self): 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): 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): 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("Edit") delete_action = menu.addAction("Delete") action = menu.exec(self.objectivesTable.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): 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("Edit") delete_action = menu.addAction("Delete") action = menu.exec(self.warParticipantsTable.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): self.warName.setText(name) self.warYear.setText(str(year)) def display_war_objectives(self, objectives: list): 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[tuple[str, str, str]]): table = self.warParticipantsTable table.clearContents() table.setRowCount(len(participants)) for row, (name, faction, pid) in enumerate(participants): name_item = QtWidgets.QTableWidgetItem(name) fact_item = QtWidgets.QTableWidgetItem(faction) name_item.setData(Qt.ItemDataRole.UserRole, pid) table.setItem(row, 0, name_item) table.setItem(row, 1, fact_item) table.resizeColumnsToContents() # Campaign page def _on_sectors_table_context_menu(self, pos): 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("Edit") delete_action = menu.addAction("Delete") action = menu.exec(self.sectorsTable.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): 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("Edit") delete_action = menu.addAction("Delete") action = menu.exec(self.campaignParticipantsTable.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): self.campaignName.setText(name) self.campaignMonth.setText(calendar.month_name[month]) def display_campaign_sectors(self, sectors: list[tuple[str, str, str, str, str, str]]): table = self.sectorsTable table.clearContents() table.setRowCount(len(sectors)) for row, (name, round_index, major, minor, influence, pid) in enumerate(sectors): name_item = QtWidgets.QTableWidgetItem(name) round_item = QtWidgets.QTableWidgetItem(format_round_label(None, round_index)) major_item = QtWidgets.QTableWidgetItem(major) minor_item = QtWidgets.QTableWidgetItem(minor) influence_item = QtWidgets.QTableWidgetItem(influence) name_item.setData(Qt.ItemDataRole.UserRole, pid) 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[tuple[str, str, str, str]]): table = self.campaignParticipantsTable table.clearContents() table.setRowCount(len(participants)) for row, (name, leader, theme, pid) in enumerate(participants): name_item = QtWidgets.QTableWidgetItem(name) lead_item = QtWidgets.QTableWidgetItem(leader) theme_item = QtWidgets.QTableWidgetItem(theme) name_item.setData(Qt.ItemDataRole.UserRole, pid) table.setItem(row, 0, name_item) table.setItem(row, 1, lead_item) table.setItem(row, 2, theme_item) table.resizeColumnsToContents() # Round page def show_round_details(self, *, index: int): self.roundNb.setText(f"Round {index}") class PlayerDialog(QDialog): def __init__(self, parent=None, *, default_name: str = ""): super().__init__(parent) self.ui = Ui_playerDialog() self.ui.setupUi(self) self.ui.playerName.setText(default_name) def get_player_name(self) -> str: return self.ui.playerName.text().strip() class WarDialog(QDialog): def __init__(self, parent=None, default_name: str = "", default_year: int | None = None): super().__init__(parent) self.ui = Ui_warDialog() self.ui.setupUi(self) self.ui.warName.setText(default_name) if default_year is not None: self.ui.warYear.setValue(default_year) def get_war_name(self) -> str: return self.ui.warName.text().strip() def get_war_year(self) -> int: return int(self.ui.warYear.value()) class CampaignDialog(QDialog): def __init__(self, parent=None, default_name: str = "", default_month: int | None = None): super().__init__(parent) self.ui = Ui_campaignDialog() self.ui.setupUi(self) self.ui.campaignName.setText(default_name) if default_month is not None: self.ui.campaignMonth.setValue(default_month) def get_campaign_name(self) -> str: return self.ui.campaignName.text().strip() def get_campaign_month(self) -> int: return int(self.ui.campaignMonth.value()) class ObjectiveDialog(QDialog): def __init__(self, parent=None, *, default_name="", default_description=""): super().__init__(parent) self.ui = Ui_objectiveDialog() self.ui.setupUi(self) self.ui.objectiveName.setText(default_name) self.ui.objectiveDescription.setPlainText(default_description) def get_objective_name(self) -> str: return self.ui.objectiveName.text().strip() def get_objective_description(self) -> str: return self.ui.objectiveDescription.toPlainText().strip() class WarParticipantDialog(QDialog): def __init__(self, parent=None, *, players: list, default_player_id=None, default_faction="", editable_player=True): super().__init__(parent) self.ui = Ui_warParticipantDialog() self.ui.setupUi(self) for player in players: self.ui.playerComboBox.addItem(player.name, player.id) select_if_exists(self.ui.playerComboBox, default_player_id) self.ui.playerComboBox.setEnabled(editable_player) self.ui.faction.setText(default_faction) def get_player_id(self) -> str: return self.ui.playerComboBox.currentData() def get_participant_faction(self) -> str: return self.ui.faction.text().strip() class CampaignParticipantDialog(QDialog): def __init__(self, parent=None, *, participants: list[ParticipantOption], default_participant_id=None, default_leader="", default_theme="", editable_player=True): super().__init__(parent) self.ui = Ui_campaignParticipantDialog() self.ui.setupUi(self) for part in participants: self.ui.playerComboBox.addItem(part.name, part.id) select_if_exists(self.ui.playerComboBox, default_participant_id) self.ui.playerComboBox.setEnabled(editable_player) self.ui.leader.setText(default_leader) self.ui.theme.setText(default_theme) def get_player_id(self) -> str: return self.ui.playerComboBox.currentData() def get_participant_leader(self) -> str: return self.ui.leader.text().strip() def get_participant_theme(self) -> str: return self.ui.theme.text().strip() class SectorDialog(QDialog): def __init__(self, parent=None, *, default_name="", rounds: list, default_round_id=None, objectives: list, default_major_id=None, default_minor_id=None, default_influence_id=None): super().__init__(parent) self.ui = Ui_sectorDialog() self.ui.setupUi(self) self.ui.majorComboBox.addItem("(none)", None) self.ui.minorComboBox.addItem("(none)", None) self.ui.influenceComboBox.addItem("(none)", None) self.ui.sectorName.setText(default_name) for index, rnd in enumerate(rounds, start=1): self.ui.roundComboBox.addItem(format_round_label(rnd, index), rnd.id) select_if_exists(self.ui.roundComboBox, default_round_id) for obj in objectives: self.ui.majorComboBox.addItem(obj.name, obj.id) self.ui.minorComboBox.addItem(obj.name, obj.id) self.ui.influenceComboBox.addItem(obj.name, obj.id) select_if_exists(self.ui.majorComboBox, default_major_id) select_if_exists(self.ui.minorComboBox, default_minor_id) select_if_exists(self.ui.influenceComboBox, default_influence_id) def get_sector_name(self) -> str: return self.ui.sectorName.text().strip() def get_round_id(self) -> str: return self.ui.roundComboBox.currentData() def get_major_id(self) -> str: return self.ui.majorComboBox.currentData() def get_minor_id(self) -> str: return self.ui.minorComboBox.currentData() def get_influence_id(self) -> str: return self.ui.influenceComboBox.currentData()