diff --git a/src/warchron/controller/controller.py b/src/warchron/controller/controller.py index a247488..15a9461 100644 --- a/src/warchron/controller/controller.py +++ b/src/warchron/controller/controller.py @@ -1,20 +1,29 @@ from pathlib import Path 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 +from warchron.view.view import PlayerDialog, WarDialog, CampaignDialog class Controller: - def __init__(self, model, view): + 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) @@ -107,6 +116,27 @@ class Controller: 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 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 add_player(self): dialog = PlayerDialog(self.view) result = dialog.exec() # modal blocking dialog @@ -123,11 +153,7 @@ class Controller: self.is_dirty = True self.refresh_players_view() self.update_window_title() - - def refresh_wars_view(self): - wars = self.model.get_all_wars() - self.view.display_wars(wars) - + def add_war(self): dialog = WarDialog(self.view) result = dialog.exec() # modal blocking dialog @@ -144,3 +170,25 @@ class Controller: self.is_dirty = True self.refresh_wars_view() self.update_window_title() + + def add_campaign(self): + if not self.selected_war_id: + return + dialog = CampaignDialog(self.view) + if dialog.exec() != QDialog.DialogCode.Accepted: + return + name = dialog.get_campaign_name() + if not name: + return + self.model.add_campaign(self.selected_war_id, name) + 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() diff --git a/src/warchron/model/campaign.py b/src/warchron/model/campaign.py index ffe070b..6a7eb99 100644 --- a/src/warchron/model/campaign.py +++ b/src/warchron/model/campaign.py @@ -9,7 +9,7 @@ class Campaign: self.name = name self.month = datetime.now().month self.entrants = {} - self.rounds = {} + self.rounds = [] self.is_over = False def set_id(self, new_id): @@ -42,4 +42,20 @@ class Campaign: ## entrants placeholder ## rounds placeholder tmp.set_state(is_over) - return tmp \ No newline at end of file + return tmp + + def add_round(self, number: int) -> Round: + round = Round() + self.rounds.append(round) + return round + + def get_round(self, round_id) -> Round: + return self.rounds[round_id] + + def get_all_rounds(self) -> list[Round]: + return list(self.rounds) + + def add_round(self) -> Round: + round = Round() + self.rounds.append(round) + return round \ No newline at end of file diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py index 5e97b44..0cc4bdf 100644 --- a/src/warchron/model/model.py +++ b/src/warchron/model/model.py @@ -4,6 +4,8 @@ import shutil from warchron.model.player import Player from warchron.model.war import War +from warchron.model.campaign import Campaign +from warchron.model.round import Round class Model: def __init__(self): @@ -69,13 +71,36 @@ class Model: def get_all_players(self) -> list[Player]: return list(self.players.values()) - def add_war(self, name): + def add_war(self, name) -> War: war = War(name) self.wars[war.id] = war return war - def get_war(self, id): + def get_war(self, id) -> War: return self.wars[id] def get_all_wars(self) -> list[War]: return list(self.wars.values()) + + def add_campaign(self, war_id: str, name: str) -> Campaign: + war = self.get_war(war_id) + return war.add_campaign(name) + + def get_campaign(self, campaign_id) -> Campaign: + for war in self.wars.values(): + for campaign in war.campaigns: + if campaign.id == campaign_id: + return campaign + raise KeyError("Campaign not found") + + def add_round(self, campaign_id: str) -> Round: + campaign = self.get_campaign(campaign_id) + return campaign.add_round() + + def get_round(self, round_id: str) -> Round: + for war in self.wars.values(): + for campaign in war.campaigns: + for rnd in campaign.rounds: + if rnd.id == round_id: + return rnd + raise KeyError("Round not found") \ No newline at end of file diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index faceb0d..04cfb81 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -1,20 +1,21 @@ +from uuid import uuid4 + class Round: - def __init__(self, number): - self.number = number + def __init__(self): + self.id = str(uuid4()) self.sectors = {} self.choices = {} self.battles = {} self.is_over = False - def set_number(self, new_number): - self.number = new_number + def set_id(self, new_id): + self.id = new_id def set_state(self, new_state): self.is_over = new_state def toDict(self): return { - "number" : self.number, "sectors" : self.sectors, "choices" : self.choices, "battles" : self.battles, @@ -22,8 +23,9 @@ class Round: } @staticmethod - def fromDict(id, number, sectors, choices, battles, is_over): - tmp = Round(number=number) + def fromDict(id, sectors, choices, battles, is_over): + tmp = Round() + tmp.set_id(id) ## sectors placeholder ## choices placeholder ## battles placeholder diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py index 308cec2..564aeb3 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -9,10 +9,9 @@ class War: self.name = name self.year = datetime.now().year self.entrants = {} - self.campaigns = {} + self.campaigns = [] self.is_over = False - def set_id(self, new_id): self.id = new_id @@ -43,4 +42,20 @@ class War: ## entrants placeholder ## campaigns placeholder tmp.set_state(is_over) - return tmp \ No newline at end of file + return tmp + + def add_campaign(self, name) -> Campaign: + campaign = Campaign(name) + self.campaigns.append(campaign) + return campaign + + def get_campaign(self, campaign_id) -> Campaign: + return self.campaigns[campaign_id] + + def get_all_campaigns(self) -> list[Campaign]: + return list(self.campaigns) + + def add_campaign(self, name: str) -> Campaign: + campaign = Campaign(name) + self.campaigns.append(campaign) + return campaign \ No newline at end of file diff --git a/src/warchron/view/ui/ui_campaign_dialog.py b/src/warchron/view/ui/ui_campaign_dialog.py new file mode 100644 index 0000000..2482d5b --- /dev/null +++ b/src/warchron/view/ui/ui_campaign_dialog.py @@ -0,0 +1,50 @@ +# Form implementation generated from reading ui file '.\src\warchron\view\ui\ui_campaign_dialog.ui' +# +# Created by: PyQt6 UI code generator 6.7.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_campaignDialog(object): + def setupUi(self, campaignDialog): + campaignDialog.setObjectName("campaignDialog") + campaignDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) + campaignDialog.resize(378, 98) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + campaignDialog.setWindowIcon(icon) + self.buttonBox = QtWidgets.QDialogButtonBox(parent=campaignDialog) + self.buttonBox.setGeometry(QtCore.QRect(10, 60, 341, 32)) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok) + self.buttonBox.setObjectName("buttonBox") + self.label = QtWidgets.QLabel(parent=campaignDialog) + self.label.setGeometry(QtCore.QRect(10, 20, 47, 14)) + self.label.setObjectName("label") + self.campaignName = QtWidgets.QLineEdit(parent=campaignDialog) + self.campaignName.setGeometry(QtCore.QRect(60, 20, 113, 20)) + self.campaignName.setObjectName("campaignName") + + self.retranslateUi(campaignDialog) + self.buttonBox.accepted.connect(campaignDialog.accept) # type: ignore + self.buttonBox.rejected.connect(campaignDialog.reject) # type: ignore + QtCore.QMetaObject.connectSlotsByName(campaignDialog) + + def retranslateUi(self, campaignDialog): + _translate = QtCore.QCoreApplication.translate + campaignDialog.setWindowTitle(_translate("campaignDialog", "Campaign")) + self.label.setText(_translate("campaignDialog", "Name:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + campaignDialog = QtWidgets.QDialog() + ui = Ui_campaignDialog() + ui.setupUi(campaignDialog) + campaignDialog.show() + sys.exit(app.exec()) diff --git a/src/warchron/view/ui/ui_campaign_dialog.ui b/src/warchron/view/ui/ui_campaign_dialog.ui new file mode 100644 index 0000000..c5ef227 --- /dev/null +++ b/src/warchron/view/ui/ui_campaign_dialog.ui @@ -0,0 +1,98 @@ + + + campaignDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 378 + 98 + + + + Campaign + + + + ../resources/warchron_logo.png../resources/warchron_logo.png + + + + + 10 + 60 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 10 + 20 + 47 + 14 + + + + Name: + + + + + + 60 + 20 + 113 + 20 + + + + + + + + buttonBox + accepted() + campaignDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + campaignDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/warchron/view/ui/ui_main_window.py b/src/warchron/view/ui/ui_main_window.py index e5c8a37..a6c422d 100644 --- a/src/warchron/view/ui/ui_main_window.py +++ b/src/warchron/view/ui/ui_main_window.py @@ -51,12 +51,16 @@ class Ui_MainWindow(object): self.addCampaignBtn.setEnabled(False) self.addCampaignBtn.setGeometry(QtCore.QRect(110, 20, 91, 23)) self.addCampaignBtn.setObjectName("addCampaignBtn") + self.addRoundBtn = QtWidgets.QPushButton(parent=self.warsTab) + self.addRoundBtn.setEnabled(False) + self.addRoundBtn.setGeometry(QtCore.QRect(220, 20, 91, 23)) + self.addRoundBtn.setObjectName("addRoundBtn") icon2 = QtGui.QIcon() icon2.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/swords-small.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.tabWidget.addTab(self.warsTab, icon2, "") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(parent=MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21)) self.menubar.setObjectName("menubar") self.menuFile = QtWidgets.QMenu(parent=self.menubar) self.menuFile.setObjectName("menuFile") @@ -144,6 +148,7 @@ class Ui_MainWindow(object): self.tabWidget.setTabText(self.tabWidget.indexOf(self.playersTab), _translate("MainWindow", "Players")) self.addWarBtn.setText(_translate("MainWindow", "Add war")) self.addCampaignBtn.setText(_translate("MainWindow", "Add Campaign")) + self.addRoundBtn.setText(_translate("MainWindow", "Add Round")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.warsTab), _translate("MainWindow", "Wars")) self.menuFile.setTitle(_translate("MainWindow", "File")) self.menuEdit.setTitle(_translate("MainWindow", "Edit")) diff --git a/src/warchron/view/ui/ui_main_window.ui b/src/warchron/view/ui/ui_main_window.ui index b88ad16..c20c876 100644 --- a/src/warchron/view/ui/ui_main_window.ui +++ b/src/warchron/view/ui/ui_main_window.ui @@ -124,6 +124,22 @@ Add Campaign + + + false + + + + 220 + 20 + 91 + 23 + + + + Add Round + + @@ -133,7 +149,7 @@ 0 0 800 - 22 + 21 diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index c9cc662..d44615f 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -1,18 +1,49 @@ from pathlib import Path +import calendar from PyQt6 import QtWidgets +from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QDialog, QFileDialog, QTreeWidgetItem from PyQt6.QtGui import QCloseEvent 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 + +ROLE_TYPE = Qt.ItemDataRole.UserRole +ROLE_ID = Qt.ItemDataRole.UserRole + 1 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.warsTree.currentItemChanged.connect(self._emit_selection_changed) + 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 _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 closeEvent(self, event: QCloseEvent): if self.on_close_callback: @@ -55,10 +86,36 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): tree.setHeaderLabels(["Wars"]) for war in wars: war_item = QTreeWidgetItem([f"{war.name} ({war.year})"]) - war_item.setData(0, 1, war.id) # role=1 to store id + war_item.setData(0, ROLE_TYPE, "war") + war_item.setData(0, ROLE_ID, war.id) tree.addTopLevelItem(war_item) + for camp in war.get_all_campaigns(): + camp_item = QTreeWidgetItem([f"{camp.name} ({calendar.month_name[camp.month]})"]) + camp_item.setData(0, ROLE_TYPE, "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([f"Round {index}"]) + rnd_item.setData(0, ROLE_TYPE, "round") + rnd_item.setData(0, ROLE_ID, rnd.id) + camp_item.addChild(rnd_item) tree.expandAll() + 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 set_add_campaign_enabled(self, enabled: bool): + self.addCampaignBtn.setEnabled(enabled) + + def set_add_round_enabled(self, enabled: bool): + self.addRoundBtn.setEnabled(enabled) + class PlayerDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) @@ -76,3 +133,12 @@ class WarDialog(QDialog): def get_war_name(self) -> str: return self.ui.warName.text().strip() + +class CampaignDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_campaignDialog() + self.ui.setupUi(self) + + def get_campaign_name(self) -> str: + return self.ui.campaignName.text().strip()