diff --git a/LICENSE b/LICENSE index 5c6b8f0..9953916 100644 --- a/LICENSE +++ b/LICENSE @@ -208,7 +208,7 @@ If you develop a new program, and you want it to be of the greatest possible use To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. - Wargame_campain_app + warchron_app Copyright (C) 2025 maximator This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Wargame_campain_app Copyright (C) 2025 maximator + warchron_app Copyright (C) 2025 maximator This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/Makefile b/Makefile index d2f93c9..266e199 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ ress: - pyrcc5 .\src\wargame_campaign\view\resources\ui_ressources.qrc -o .\src\wargame_campaign\view\resources\ui_ressources_rc.py + pyrcc5 .\src\warchron\view\resources\ui_ressources.qrc -o .\src\warchron\view\resources\ui_ressources_rc.py installer : python -m PyInstaller .\main.spec @@ -14,11 +14,11 @@ ui: $(PY_FILES) # Pattern rule: .ui -> .py using pyuic5 $(UI_DIR)/%.py: $(UI_DIR)/%.ui - pyuic5 -x $< -o $@ --import-from wargame_campaign.view.resources + pyuic5 -x $< -o $@ --import-from warchron.view.resources # Function to generate UI file from given name _ui_generate: - pyuic6 -x .\src\wargame_campaign\view\ui\$(UI_NAME).ui -o .\src\wargame_campaign\view\ui\$(UI_NAME).py --import-from wargame_campaign.view.resources + pyuic6 -x .\src\warchron\view\ui\$(UI_NAME).ui -o .\src\warchron\view\ui\$(UI_NAME).py --import-from warchron.view.resources # Set default UI_NAME if not provided UI_NAME ?= ui_main_window diff --git a/README.md b/README.md index ec3c5cd..65b9e72 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,22 @@ -# Wargame_campaign_app +# WarChron -A simple local app to manage players and their scores throughout several organised games of a tabletop wargame. +A simple local app to track players' campaigns for tabletop wargames. ## Features ### Main logic -Manage a list of players to sign them up to be selectable for war(s) and campaign(s). -A year "war" contains several "campaign" events which contain several "battle" games organised in successive rounds. -Battle results determine campaign score which determines the war score. Wars are independent. +Manage a list of players to sign them up to be selectable for war(s) and campaign(s). +A "war" year contains several "campaign" events which contain several "battle" games organised in successive rounds. +Battle results determine campaign score which determines the war score. +Wars are independent. ### Design notes -Players are global identities -Influence tokens are scoped to a war -Campaign order enables historical tie-breakers -Effects are generic → future-proof +* Players are global identities +* Influence tokens are scoped to a war +* Campaign order enables historical tie-breakers +* Effects are generic → future-proof ## Installation @@ -27,8 +28,8 @@ Effects are generic → future-proof ### Setup ```bash -git clone /Wargame_campaign_app.git -cd Wargame_campaign_app +git clone /warchron_app.git +cd warchron_app python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt diff --git a/data/example.json b/data/example.json index 10ae5b8..c12a44a 100644 --- a/data/example.json +++ b/data/example.json @@ -3,16 +3,24 @@ "players": [ { - "id" : "p1", - "name" :"Alice" + "id": "e7844fbb-8366-44e4-bb43-89b9eef6ef64", + "name": "Alice" }, { - "id" : "p2", - "name" :"Bob" + "id": "b7eebce7-cf04-40bc-b80c-400585adb3cd", + "name": "Bob" }, { - "id" : "p3", - "name" :"Charlie" + "id": "f87e6d53-30a2-4dd8-a359-d860404ef2ee", + "name": "Charlie" + }, + { + "id": "056011da-b0c7-4dc7-8b7c-14213a8df009", + "name": "Dave" + }, + { + "id": "50e83cb3-b828-4f1e-ad89-7644a84f3d8c", + "name": "Eve" } ], @@ -23,11 +31,11 @@ "year": 2025, "registered_players": { - "p1": { + "e7844fbb-8366-44e4-bb43-89b9eef6ef64": { "war_points": 0, "influence_tokens": 1 }, - "p2": { + "b7eebce7-cf04-40bc-b80c-400585adb3cd": { "war_points": 0, "influence_tokens": 0 } @@ -40,8 +48,8 @@ "order": 1, "month" : "June", "participants": { - "p1": { "campaign_points": 0 }, - "p2": { "campaign_points": 0 } + "e7844fbb-8366-44e4-bb43-89b9eef6ef64": { "campaign_points": 0 }, + "b7eebce7-cf04-40bc-b80c-400585adb3cd": { "campaign_points": 0 } }, "rounds": [ @@ -50,15 +58,15 @@ "sectors": ["North", "South"], "choices": { - "p1": { "primary": "North", "secondary": "South" }, - "p2": { "primary": "North", "secondary": "South" } + "e7844fbb-8366-44e4-bb43-89b9eef6ef64": { "primary": "North", "secondary": "South" }, + "b7eebce7-cf04-40bc-b80c-400585adb3cd": { "primary": "North", "secondary": "South" } }, "battles": [ { "sector": "North", - "players": ["p1", "p2"], - "winner": "p1", + "players": ["e7844fbb-8366-44e4-bb43-89b9eef6ef64", "b7eebce7-cf04-40bc-b80c-400585adb3cd"], + "winner": "e7844fbb-8366-44e4-bb43-89b9eef6ef64", "effects": { "campaign_points": 1, diff --git a/main.py b/main.py index ced14b9..938058b 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,9 @@ sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "src") from PyQt6.QtWidgets import QApplication -from wargame_campaign.view.view import View -from wargame_campaign.model.model import Model -from wargame_campaign.controller.controller import Controller +from warchron.view.view import View +from warchron.model.model import Model +from warchron.controller.controller import Controller if sys.version_info < (3, 12): raise RuntimeError("Python 3.12 or higher is required") diff --git a/src/wargame_campaign/controller/__ini__.py b/src/warchron/controller/__ini__.py similarity index 100% rename from src/wargame_campaign/controller/__ini__.py rename to src/warchron/controller/__ini__.py diff --git a/src/warchron/controller/controller.py b/src/warchron/controller/controller.py new file mode 100644 index 0000000..734f84d --- /dev/null +++ b/src/warchron/controller/controller.py @@ -0,0 +1,119 @@ +from pathlib import Path + +from PyQt6.QtWidgets import QMessageBox, QDialog + +from warchron.view.view import PlayerDialog + +class Controller: + def __init__(self, model, view): + self.model = model + self.view = view + self.current_file: Path | None = None + self.view.on_close_callback = self.on_app_close + self.is_dirty = False + self.__connect() + self.refresh_players_view() + + 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) + + def refresh_players_view(self): + players = self.model.get_all_players() + self.view.display_players(players) + + def new(self): + if self.is_dirty: + reply = QMessageBox.question( + self.view, + "Unsaved changes", + "Discard current campaign?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return + self.model.new() + self.current_file = None + self.is_dirty = False + self.refresh_players_view() + self.update_window_title() + + def open_file(self): + if self.is_dirty: + reply = QMessageBox.question( + self.view, + "Unsaved changes", + "Discard current campaign?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return + path = self.view.ask_open_file() + if not path: + return + self.model.load(path) + self.current_file = path + self.is_dirty = False + self.refresh_players_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() + return + self.model.save(self.current_file) + self.is_dirty = False + self.update_window_title() + + def save_as(self): + path = self.view.ask_save_file() + if not path: + return + self.current_file = path + self.model.save(path) + self.is_dirty = False + self.update_window_title() + + def update_window_title(self): + base = "WarChron" + if self.current_file: + base += f" - {self.current_file.name}" + if self.is_dirty: + base = "*" + base + self.view.setWindowTitle(base) + + + def add_player(self): + dialog = PlayerDialog(self.view) + result = dialog.exec() # modal blocking dialog + if result == QDialog.DialogCode.Accepted: + name = dialog.get_player_name() + if not name: + QMessageBox.warning( + self.view, + "Invalid name", + "Player name cannot be empty." + ) + return + self.model.add_player(name) + self.is_dirty = True + self.refresh_players_view() + self.update_window_title() diff --git a/src/wargame_campaign/model/__ini__.py b/src/warchron/model/__ini__.py similarity index 100% rename from src/wargame_campaign/model/__ini__.py rename to src/warchron/model/__ini__.py diff --git a/src/wargame_campaign/model/model.py b/src/warchron/model/model.py similarity index 63% rename from src/wargame_campaign/model/model.py rename to src/warchron/model/model.py index 494f432..ff88508 100644 --- a/src/wargame_campaign/model/model.py +++ b/src/warchron/model/model.py @@ -2,20 +2,30 @@ from pathlib import Path import json import shutil -from wargame_campaign.model.player import Player +from warchron.model.player import Player class Model: def __init__(self): self.players = {} - data_file_path = Path("data/warmachron.json") - self.load_data(data_file_path) - self.save_data(data_file_path) - def load_data(self, data_file_path): - if not data_file_path.exists() or data_file_path.stat().st_size == 0: - pass # Create empty json + def new(self): + self.players.clear() + # self.wars.clear() + # self.campaigns.clear() + # self.rounds.clear() + + def load(self, path: Path): + self.players.clear() + self._load_data(path) + + def save(self, path: Path): + self._save_data(path) + + def _load_data(self, path: Path): + if not path.exists() or path.stat().st_size == 0: + return # Start empty try: - with open(data_file_path, "r", encoding="utf-8") as f: + with open(path, "r", encoding="utf-8") as f: data = json.load(f) for player in data["players"] : saved_player = Player.fromDict(player["id"], player['name']) @@ -26,16 +36,16 @@ class Model: except json.JSONDecodeError: raise RuntimeError("Data file is corrupted") - def save_data(self, data_file_path): - if data_file_path.exists(): - shutil.copy(data_file_path, data_file_path.with_suffix(".json.bak")) + def _save_data(self, path: Path): + if path.exists(): + shutil.copy(path, path.with_suffix(".json.bak")) data = {} data['version'] = "1.0" data['players'] = [] data['wars'] = [] for player in self.players.values(): data['players'].append(player.toDict()) - with open(data_file_path, "w", encoding="utf-8") as f: + with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) def add_player(self, name): diff --git a/src/wargame_campaign/model/player.py b/src/warchron/model/player.py similarity index 100% rename from src/wargame_campaign/model/player.py rename to src/warchron/model/player.py diff --git a/src/wargame_campaign/model/repository.py b/src/warchron/model/repository.py similarity index 100% rename from src/wargame_campaign/model/repository.py rename to src/warchron/model/repository.py diff --git a/src/wargame_campaign/view/__ini__.py b/src/warchron/view/__ini__.py similarity index 100% rename from src/wargame_campaign/view/__ini__.py rename to src/warchron/view/__ini__.py diff --git a/src/wargame_campaign/view/resources/arrow-curve-180-left.png b/src/warchron/view/resources/arrow-curve-180-left.png similarity index 100% rename from src/wargame_campaign/view/resources/arrow-curve-180-left.png rename to src/warchron/view/resources/arrow-curve-180-left.png diff --git a/src/wargame_campaign/view/resources/arrow-curve.png b/src/warchron/view/resources/arrow-curve.png similarity index 100% rename from src/wargame_campaign/view/resources/arrow-curve.png rename to src/warchron/view/resources/arrow-curve.png diff --git a/src/wargame_campaign/view/resources/cross.png b/src/warchron/view/resources/cross.png similarity index 100% rename from src/wargame_campaign/view/resources/cross.png rename to src/warchron/view/resources/cross.png diff --git a/src/wargame_campaign/view/resources/disk--pencil.png b/src/warchron/view/resources/disk--pencil.png similarity index 100% rename from src/wargame_campaign/view/resources/disk--pencil.png rename to src/warchron/view/resources/disk--pencil.png diff --git a/src/wargame_campaign/view/resources/disk.png b/src/warchron/view/resources/disk.png similarity index 100% rename from src/wargame_campaign/view/resources/disk.png rename to src/warchron/view/resources/disk.png diff --git a/src/wargame_campaign/view/resources/document.png b/src/warchron/view/resources/document.png similarity index 100% rename from src/wargame_campaign/view/resources/document.png rename to src/warchron/view/resources/document.png diff --git a/src/wargame_campaign/view/resources/door--arrow.png b/src/warchron/view/resources/door--arrow.png similarity index 100% rename from src/wargame_campaign/view/resources/door--arrow.png rename to src/warchron/view/resources/door--arrow.png diff --git a/src/wargame_campaign/view/resources/folder.png b/src/warchron/view/resources/folder.png similarity index 100% rename from src/wargame_campaign/view/resources/folder.png rename to src/warchron/view/resources/folder.png diff --git a/src/wargame_campaign/view/resources/notebook--arrow.png b/src/warchron/view/resources/notebook--arrow.png similarity index 100% rename from src/wargame_campaign/view/resources/notebook--arrow.png rename to src/warchron/view/resources/notebook--arrow.png diff --git a/src/wargame_campaign/view/resources/pencil.png b/src/warchron/view/resources/pencil.png similarity index 100% rename from src/wargame_campaign/view/resources/pencil.png rename to src/warchron/view/resources/pencil.png diff --git a/src/wargame_campaign/view/resources/plus.png b/src/warchron/view/resources/plus.png similarity index 100% rename from src/wargame_campaign/view/resources/plus.png rename to src/warchron/view/resources/plus.png diff --git a/src/wargame_campaign/view/resources/question.png b/src/warchron/view/resources/question.png similarity index 100% rename from src/wargame_campaign/view/resources/question.png rename to src/warchron/view/resources/question.png diff --git a/src/warchron/view/resources/swords-small.png b/src/warchron/view/resources/swords-small.png new file mode 100644 index 0000000..abd610f Binary files /dev/null and b/src/warchron/view/resources/swords-small.png differ diff --git a/src/warchron/view/resources/swords.png b/src/warchron/view/resources/swords.png new file mode 100644 index 0000000..3c09d20 Binary files /dev/null and b/src/warchron/view/resources/swords.png differ diff --git a/src/wargame_campaign/view/resources/users.png b/src/warchron/view/resources/users.png similarity index 100% rename from src/wargame_campaign/view/resources/users.png rename to src/warchron/view/resources/users.png diff --git a/src/wargame_campaign/view/resources/wargame_campaign_logo.png b/src/warchron/view/resources/warchron_logo.png similarity index 100% rename from src/wargame_campaign/view/resources/wargame_campaign_logo.png rename to src/warchron/view/resources/warchron_logo.png diff --git a/src/wargame_campaign/view/ui/ui_main_window.py b/src/warchron/view/ui/ui_main_window.py similarity index 78% rename from src/wargame_campaign/view/ui/ui_main_window.py rename to src/warchron/view/ui/ui_main_window.py index 1b2a884..e5c8a37 100644 --- a/src/wargame_campaign/view/ui/ui_main_window.py +++ b/src/warchron/view/ui/ui_main_window.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '.\src\wargame_campaign\view\ui\ui_main_window.ui' +# Form implementation generated from reading ui file '.\src\warchron\view\ui\ui_main_window.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -14,7 +14,7 @@ class Ui_MainWindow(object): MainWindow.setObjectName("MainWindow") MainWindow.resize(800, 600) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/wargame_campaign_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) MainWindow.setWindowIcon(icon) self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget.setObjectName("centralwidget") @@ -36,7 +36,7 @@ class Ui_MainWindow(object): self.addPlayerBtn.setGeometry(QtCore.QRect(20, 20, 75, 23)) self.addPlayerBtn.setObjectName("addPlayerBtn") icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/users.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon1.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/users.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.tabWidget.addTab(self.playersTab, icon1, "") self.warsTab = QtWidgets.QWidget() self.warsTab.setObjectName("warsTab") @@ -52,7 +52,7 @@ class Ui_MainWindow(object): self.addCampaignBtn.setGeometry(QtCore.QRect(110, 20, 91, 23)) self.addCampaignBtn.setObjectName("addCampaignBtn") icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/swords-small.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + 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) @@ -70,48 +70,48 @@ class Ui_MainWindow(object): MainWindow.setStatusBar(self.statusbar) self.actionNew = QtGui.QAction(parent=MainWindow) icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/document.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon3.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/document.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionNew.setIcon(icon3) self.actionNew.setObjectName("actionNew") self.actionOpen = QtGui.QAction(parent=MainWindow) icon4 = QtGui.QIcon() - icon4.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/folder.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon4.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/folder.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionOpen.setIcon(icon4) self.actionOpen.setObjectName("actionOpen") self.actionSave = QtGui.QAction(parent=MainWindow) icon5 = QtGui.QIcon() - icon5.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/disk.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon5.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/disk.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionSave.setIcon(icon5) self.actionSave.setObjectName("actionSave") self.actionExit = QtGui.QAction(parent=MainWindow) icon6 = QtGui.QIcon() - icon6.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/door--arrow.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon6.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/door--arrow.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionExit.setIcon(icon6) self.actionExit.setObjectName("actionExit") self.actionUndo = QtGui.QAction(parent=MainWindow) icon7 = QtGui.QIcon() - icon7.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/arrow-curve-180-left.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon7.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/arrow-curve-180-left.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionUndo.setIcon(icon7) self.actionUndo.setObjectName("actionUndo") self.actionRedo = QtGui.QAction(parent=MainWindow) icon8 = QtGui.QIcon() - icon8.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/arrow-curve.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon8.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/arrow-curve.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionRedo.setIcon(icon8) self.actionRedo.setObjectName("actionRedo") self.actionAbout = QtGui.QAction(parent=MainWindow) icon9 = QtGui.QIcon() - icon9.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/question.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon9.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/question.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionAbout.setIcon(icon9) self.actionAbout.setObjectName("actionAbout") self.actionExport = QtGui.QAction(parent=MainWindow) self.actionExport.setEnabled(False) icon10 = QtGui.QIcon() - icon10.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/notebook--arrow.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon10.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/notebook--arrow.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionExport.setIcon(icon10) self.actionExport.setObjectName("actionExport") self.actionSave_as = QtGui.QAction(parent=MainWindow) icon11 = QtGui.QIcon() - icon11.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/disk--pencil.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon11.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/disk--pencil.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.actionSave_as.setIcon(icon11) self.actionSave_as.setObjectName("actionSave_as") self.menuFile.addAction(self.actionNew) @@ -135,7 +135,7 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "Wargame campaign")) + MainWindow.setWindowTitle(_translate("MainWindow", "WarChron")) item = self.playersTable.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "Name")) item = self.playersTable.horizontalHeaderItem(1) @@ -151,8 +151,11 @@ class Ui_MainWindow(object): self.actionNew.setText(_translate("MainWindow", "New")) self.actionNew.setShortcut(_translate("MainWindow", "Ctrl+N")) self.actionOpen.setText(_translate("MainWindow", "Open")) + self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O")) self.actionSave.setText(_translate("MainWindow", "Save")) + self.actionSave.setShortcut(_translate("MainWindow", "Ctrl+S")) self.actionExit.setText(_translate("MainWindow", "Exit")) + self.actionExit.setShortcut(_translate("MainWindow", "Ctrl+Shift+Q")) self.actionUndo.setText(_translate("MainWindow", "Undo")) self.actionRedo.setText(_translate("MainWindow", "Redo")) self.actionAbout.setText(_translate("MainWindow", "About")) diff --git a/src/wargame_campaign/view/ui/ui_main_window.ui b/src/warchron/view/ui/ui_main_window.ui similarity index 95% rename from src/wargame_campaign/view/ui/ui_main_window.ui rename to src/warchron/view/ui/ui_main_window.ui index d68e721..b88ad16 100644 --- a/src/wargame_campaign/view/ui/ui_main_window.ui +++ b/src/warchron/view/ui/ui_main_window.ui @@ -11,11 +11,11 @@ - Wargame campaign + WarChron - ../resources/wargame_campaign_logo.png../resources/wargame_campaign_logo.png + ../resources/warchron_logo.png../resources/warchron_logo.png @@ -187,6 +187,9 @@ Open + + Ctrl+O + @@ -196,6 +199,9 @@ Save + + Ctrl+S + @@ -205,6 +211,9 @@ Exit + + Ctrl+Shift+Q + diff --git a/src/wargame_campaign/view/ui/ui_player_dialog.py b/src/warchron/view/ui/ui_player_dialog.py similarity index 88% rename from src/wargame_campaign/view/ui/ui_player_dialog.py rename to src/warchron/view/ui/ui_player_dialog.py index 5353e22..2505608 100644 --- a/src/wargame_campaign/view/ui/ui_player_dialog.py +++ b/src/warchron/view/ui/ui_player_dialog.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '.\src\wargame_campaign\view\ui\ui_player_dialog.ui' +# Form implementation generated from reading ui file '.\src\warchron\view\ui\ui_player_dialog.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -15,7 +15,7 @@ class Ui_playerDialog(object): playerDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) playerDialog.resize(378, 98) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(".\\src\\wargame_campaign\\view\\ui\\../resources/wargame_campaign_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) playerDialog.setWindowIcon(icon) self.buttonBox = QtWidgets.QDialogButtonBox(parent=playerDialog) self.buttonBox.setGeometry(QtCore.QRect(10, 60, 341, 32)) diff --git a/src/wargame_campaign/view/ui/ui_player_dialog.ui b/src/warchron/view/ui/ui_player_dialog.ui similarity index 94% rename from src/wargame_campaign/view/ui/ui_player_dialog.ui rename to src/warchron/view/ui/ui_player_dialog.ui index 029c92b..6f2301f 100644 --- a/src/wargame_campaign/view/ui/ui_player_dialog.ui +++ b/src/warchron/view/ui/ui_player_dialog.ui @@ -18,7 +18,7 @@ - ../resources/wargame_campaign_logo.png../resources/wargame_campaign_logo.png + ../resources/warchron_logo.png../resources/warchron_logo.png diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py new file mode 100644 index 0000000..7ca4f03 --- /dev/null +++ b/src/warchron/view/view.py @@ -0,0 +1,57 @@ +from pathlib import Path + +from PyQt6 import QtWidgets +from PyQt6.QtWidgets import QDialog, QFileDialog +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 + +class View(QtWidgets.QMainWindow, Ui_MainWindow): + def __init__(self, parent=None): + super(View, self).__init__(parent) + self.setupUi(self) + self.on_close_callback = 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 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 + +class PlayerDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_playerDialog() + self.ui.setupUi(self) + + def get_player_name(self) -> str: + return self.ui.playerName.text().strip() diff --git a/src/wargame_campaign/controller/controller.py b/src/wargame_campaign/controller/controller.py deleted file mode 100644 index a9869e5..0000000 --- a/src/wargame_campaign/controller/controller.py +++ /dev/null @@ -1,34 +0,0 @@ -from PyQt6.QtWidgets import QMessageBox, QDialog - -from wargame_campaign.view.view import PlayerDialog - -class Controller: - def __init__(self, model, view): - self.model = model - self.view = view - self.__connect() - self.refresh_players_view() - - def __connect(self): - self.view.addPlayerBtn.clicked.connect(self.add_player) - pass - - def refresh_players_view(self): - players = self.model.get_all_players() - self.view.display_players(players) - - def add_player(self): - dialog = PlayerDialog(self.view) - result = dialog.exec() # modal blocking dialog - if result == QDialog.DialogCode.Accepted: - name = dialog.get_player_name() - if not name: - QMessageBox.warning( - self.view, - "Invalid name", - "Player name cannot be empty." - ) - return - self.model.add_player(name) - self.refresh_players_view() - \ No newline at end of file diff --git a/src/wargame_campaign/view/resources/swords-small.png b/src/wargame_campaign/view/resources/swords-small.png deleted file mode 100644 index d76343b..0000000 Binary files a/src/wargame_campaign/view/resources/swords-small.png and /dev/null differ diff --git a/src/wargame_campaign/view/resources/swords.png b/src/wargame_campaign/view/resources/swords.png deleted file mode 100644 index b862d6e..0000000 Binary files a/src/wargame_campaign/view/resources/swords.png and /dev/null differ diff --git a/src/wargame_campaign/view/view.py b/src/wargame_campaign/view/view.py deleted file mode 100644 index 2898125..0000000 --- a/src/wargame_campaign/view/view.py +++ /dev/null @@ -1,29 +0,0 @@ -from PyQt6 import QtWidgets -from PyQt6.QtWidgets import QDialog - -from wargame_campaign.view.ui.ui_main_window import Ui_MainWindow -from wargame_campaign.view.ui.ui_player_dialog import Ui_playerDialog - -class View(QtWidgets.QMainWindow, Ui_MainWindow): - def __init__(self, parent=None): - super(View, self).__init__(parent) - self.setupUi(self) - - 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() - -class PlayerDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_playerDialog() - self.ui.setupUi(self) - - def get_player_name(self) -> str: - return self.ui.playerName.text().strip()