diff --git a/src/warchron/controller/controller.py b/src/warchron/controller/controller.py index e7ba6a4..15a9461 100644 --- a/src/warchron/controller/controller.py +++ b/src/warchron/controller/controller.py @@ -1,32 +1,53 @@ 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 +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.addPlayerBtn.clicked.connect(self.add_player) 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.addPlayerBtn.clicked.connect(self.add_player) + self.view.addWarBtn.clicked.connect(self.add_war) - def refresh_players_view(self): - players = self.model.get_all_players() - self.view.display_players(players) - + 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 + def new(self): if self.is_dirty: reply = QMessageBox.question( @@ -41,6 +62,7 @@ class Controller: self.current_file = None self.is_dirty = False self.refresh_players_view() + self.refresh_wars_view() self.update_window_title() def open_file(self): @@ -60,22 +82,9 @@ class Controller: self.current_file = path self.is_dirty = False self.refresh_players_view() + self.refresh_wars_view() self.update_window_title() - - 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 - + def save(self): if not self.current_file: self.save_as() @@ -103,6 +112,30 @@ class Controller: base = base + " *" self.view.setWindowTitle(base) + def refresh_players_view(self): + 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) @@ -120,3 +153,42 @@ class Controller: self.is_dirty = True self.refresh_players_view() self.update_window_title() + + def add_war(self): + dialog = WarDialog(self.view) + result = dialog.exec() # modal blocking dialog + if result == QDialog.DialogCode.Accepted: + name = dialog.get_war_name() + if not name: + QMessageBox.warning( + self.view, + "Invalid name", + "War name cannot be empty." + ) + return + self.model.add_war(name) + 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/ui/ui_war_dialog.py b/src/warchron/view/ui/ui_war_dialog.py new file mode 100644 index 0000000..46404e7 --- /dev/null +++ b/src/warchron/view/ui/ui_war_dialog.py @@ -0,0 +1,50 @@ +# Form implementation generated from reading ui file '.\src\warchron\view\ui\ui_war_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_warDialog(object): + def setupUi(self, warDialog): + warDialog.setObjectName("warDialog") + warDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) + warDialog.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) + warDialog.setWindowIcon(icon) + self.buttonBox = QtWidgets.QDialogButtonBox(parent=warDialog) + 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=warDialog) + self.label.setGeometry(QtCore.QRect(10, 20, 47, 14)) + self.label.setObjectName("label") + self.warName = QtWidgets.QLineEdit(parent=warDialog) + self.warName.setGeometry(QtCore.QRect(60, 20, 113, 20)) + self.warName.setObjectName("warName") + + self.retranslateUi(warDialog) + self.buttonBox.accepted.connect(warDialog.accept) # type: ignore + self.buttonBox.rejected.connect(warDialog.reject) # type: ignore + QtCore.QMetaObject.connectSlotsByName(warDialog) + + def retranslateUi(self, warDialog): + _translate = QtCore.QCoreApplication.translate + warDialog.setWindowTitle(_translate("warDialog", "War")) + self.label.setText(_translate("warDialog", "Name:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + warDialog = QtWidgets.QDialog() + ui = Ui_warDialog() + ui.setupUi(warDialog) + warDialog.show() + sys.exit(app.exec()) diff --git a/src/warchron/view/ui/ui_war_dialog.ui b/src/warchron/view/ui/ui_war_dialog.ui new file mode 100644 index 0000000..89ae183 --- /dev/null +++ b/src/warchron/view/ui/ui_war_dialog.ui @@ -0,0 +1,98 @@ + + + warDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 378 + 98 + + + + War + + + + ../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() + warDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + warDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 7ca4f03..d44615f 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -1,25 +1,49 @@ from pathlib import Path +import calendar from PyQt6 import QtWidgets -from PyQt6.QtWidgets import QDialog, QFileDialog +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 display_players(self, players: list): - table = self.playersTable - table.setRowCount(len(players)) - for row, player in enumerate(players): - table.setItem(row, 0, QtWidgets.QTableWidgetItem(player.name)) - table.setItem(row, 1, QtWidgets.QTableWidgetItem(player.id)) - table.resizeColumnsToContents() + 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: @@ -47,6 +71,51 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): ) return Path(filename) if filename else None + def display_players(self, players: list): + table = self.playersTable + table.setRowCount(len(players)) + for row, player in enumerate(players): + table.setItem(row, 0, QtWidgets.QTableWidgetItem(player.name)) + table.setItem(row, 1, QtWidgets.QTableWidgetItem(player.id)) + table.resizeColumnsToContents() + + def display_wars(self, wars: list): + tree = self.warsTree + tree.clear() + tree.setColumnCount(1) + tree.setHeaderLabels(["Wars"]) + for war in wars: + war_item = QTreeWidgetItem([f"{war.name} ({war.year})"]) + 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) @@ -55,3 +124,21 @@ class PlayerDialog(QDialog): def get_player_name(self) -> str: return self.ui.playerName.text().strip() + +class WarDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_warDialog() + self.ui.setupUi(self) + + 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()