rename app ; add new,open,save actions

This commit is contained in:
Maxime Réaux 2026-01-19 11:16:23 +01:00
parent ee7a266e9d
commit 4d56a90790
37 changed files with 271 additions and 127 deletions

View file

@ -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. 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 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. 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: 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 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. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View file

@ -1,5 +1,5 @@
ress: 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 : installer :
python -m PyInstaller .\main.spec python -m PyInstaller .\main.spec
@ -14,11 +14,11 @@ ui: $(PY_FILES)
# Pattern rule: .ui -> .py using pyuic5 # Pattern rule: .ui -> .py using pyuic5
$(UI_DIR)/%.py: $(UI_DIR)/%.ui $(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 # Function to generate UI file from given name
_ui_generate: _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 # Set default UI_NAME if not provided
UI_NAME ?= ui_main_window UI_NAME ?= ui_main_window

View file

@ -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 ## Features
### Main logic ### Main logic
Manage a list of players to sign them up to be selectable for war(s) and campaign(s). 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. 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. Battle results determine campaign score which determines the war score.
Wars are independent.
### Design notes ### Design notes
Players are global identities * Players are global identities
Influence tokens are scoped to a war * Influence tokens are scoped to a war
Campaign order enables historical tie-breakers * Campaign order enables historical tie-breakers
Effects are generic → future-proof * Effects are generic → future-proof
## Installation ## Installation
@ -27,8 +28,8 @@ Effects are generic → future-proof
### Setup ### Setup
```bash ```bash
git clone <your-forge-address>/Wargame_campaign_app.git git clone <your-forge-address>/warchron_app.git
cd Wargame_campaign_app cd warchron_app
python -m venv .venv python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt pip install -r requirements.txt

View file

@ -3,16 +3,24 @@
"players": [ "players": [
{ {
"id" : "p1", "id": "e7844fbb-8366-44e4-bb43-89b9eef6ef64",
"name" :"Alice" "name": "Alice"
}, },
{ {
"id" : "p2", "id": "b7eebce7-cf04-40bc-b80c-400585adb3cd",
"name" :"Bob" "name": "Bob"
}, },
{ {
"id" : "p3", "id": "f87e6d53-30a2-4dd8-a359-d860404ef2ee",
"name" :"Charlie" "name": "Charlie"
},
{
"id": "056011da-b0c7-4dc7-8b7c-14213a8df009",
"name": "Dave"
},
{
"id": "50e83cb3-b828-4f1e-ad89-7644a84f3d8c",
"name": "Eve"
} }
], ],
@ -23,11 +31,11 @@
"year": 2025, "year": 2025,
"registered_players": { "registered_players": {
"p1": { "e7844fbb-8366-44e4-bb43-89b9eef6ef64": {
"war_points": 0, "war_points": 0,
"influence_tokens": 1 "influence_tokens": 1
}, },
"p2": { "b7eebce7-cf04-40bc-b80c-400585adb3cd": {
"war_points": 0, "war_points": 0,
"influence_tokens": 0 "influence_tokens": 0
} }
@ -40,8 +48,8 @@
"order": 1, "order": 1,
"month" : "June", "month" : "June",
"participants": { "participants": {
"p1": { "campaign_points": 0 }, "e7844fbb-8366-44e4-bb43-89b9eef6ef64": { "campaign_points": 0 },
"p2": { "campaign_points": 0 } "b7eebce7-cf04-40bc-b80c-400585adb3cd": { "campaign_points": 0 }
}, },
"rounds": [ "rounds": [
@ -50,15 +58,15 @@
"sectors": ["North", "South"], "sectors": ["North", "South"],
"choices": { "choices": {
"p1": { "primary": "North", "secondary": "South" }, "e7844fbb-8366-44e4-bb43-89b9eef6ef64": { "primary": "North", "secondary": "South" },
"p2": { "primary": "North", "secondary": "South" } "b7eebce7-cf04-40bc-b80c-400585adb3cd": { "primary": "North", "secondary": "South" }
}, },
"battles": [ "battles": [
{ {
"sector": "North", "sector": "North",
"players": ["p1", "p2"], "players": ["e7844fbb-8366-44e4-bb43-89b9eef6ef64", "b7eebce7-cf04-40bc-b80c-400585adb3cd"],
"winner": "p1", "winner": "e7844fbb-8366-44e4-bb43-89b9eef6ef64",
"effects": { "effects": {
"campaign_points": 1, "campaign_points": 1,

View file

@ -4,9 +4,9 @@ sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "src")
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from wargame_campaign.view.view import View from warchron.view.view import View
from wargame_campaign.model.model import Model from warchron.model.model import Model
from wargame_campaign.controller.controller import Controller from warchron.controller.controller import Controller
if sys.version_info < (3, 12): if sys.version_info < (3, 12):
raise RuntimeError("Python 3.12 or higher is required") raise RuntimeError("Python 3.12 or higher is required")

View file

@ -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()

View file

@ -2,20 +2,30 @@ from pathlib import Path
import json import json
import shutil import shutil
from wargame_campaign.model.player import Player from warchron.model.player import Player
class Model: class Model:
def __init__(self): def __init__(self):
self.players = {} 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): def new(self):
if not data_file_path.exists() or data_file_path.stat().st_size == 0: self.players.clear()
pass # Create empty json # 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: 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) data = json.load(f)
for player in data["players"] : for player in data["players"] :
saved_player = Player.fromDict(player["id"], player['name']) saved_player = Player.fromDict(player["id"], player['name'])
@ -26,16 +36,16 @@ class Model:
except json.JSONDecodeError: except json.JSONDecodeError:
raise RuntimeError("Data file is corrupted") raise RuntimeError("Data file is corrupted")
def save_data(self, data_file_path): def _save_data(self, path: Path):
if data_file_path.exists(): if path.exists():
shutil.copy(data_file_path, data_file_path.with_suffix(".json.bak")) shutil.copy(path, path.with_suffix(".json.bak"))
data = {} data = {}
data['version'] = "1.0" data['version'] = "1.0"
data['players'] = [] data['players'] = []
data['wars'] = [] data['wars'] = []
for player in self.players.values(): for player in self.players.values():
data['players'].append(player.toDict()) 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) json.dump(data, f, indent=2)
def add_player(self, name): def add_player(self, name):

View file

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 613 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 677 B

After

Width:  |  Height:  |  Size: 677 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 507 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 692 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 521 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 870 B

After

Width:  |  Height:  |  Size: 870 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

@ -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 # Created by: PyQt6 UI code generator 6.7.1
# #
@ -14,7 +14,7 @@ class Ui_MainWindow(object):
MainWindow.setObjectName("MainWindow") MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 600) MainWindow.resize(800, 600)
icon = QtGui.QIcon() 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) MainWindow.setWindowIcon(icon)
self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget") self.centralwidget.setObjectName("centralwidget")
@ -36,7 +36,7 @@ class Ui_MainWindow(object):
self.addPlayerBtn.setGeometry(QtCore.QRect(20, 20, 75, 23)) self.addPlayerBtn.setGeometry(QtCore.QRect(20, 20, 75, 23))
self.addPlayerBtn.setObjectName("addPlayerBtn") self.addPlayerBtn.setObjectName("addPlayerBtn")
icon1 = QtGui.QIcon() 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.tabWidget.addTab(self.playersTab, icon1, "")
self.warsTab = QtWidgets.QWidget() self.warsTab = QtWidgets.QWidget()
self.warsTab.setObjectName("warsTab") self.warsTab.setObjectName("warsTab")
@ -52,7 +52,7 @@ class Ui_MainWindow(object):
self.addCampaignBtn.setGeometry(QtCore.QRect(110, 20, 91, 23)) self.addCampaignBtn.setGeometry(QtCore.QRect(110, 20, 91, 23))
self.addCampaignBtn.setObjectName("addCampaignBtn") self.addCampaignBtn.setObjectName("addCampaignBtn")
icon2 = QtGui.QIcon() 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, "") self.tabWidget.addTab(self.warsTab, icon2, "")
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow) self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
@ -70,48 +70,48 @@ class Ui_MainWindow(object):
MainWindow.setStatusBar(self.statusbar) MainWindow.setStatusBar(self.statusbar)
self.actionNew = QtGui.QAction(parent=MainWindow) self.actionNew = QtGui.QAction(parent=MainWindow)
icon3 = QtGui.QIcon() 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.setIcon(icon3)
self.actionNew.setObjectName("actionNew") self.actionNew.setObjectName("actionNew")
self.actionOpen = QtGui.QAction(parent=MainWindow) self.actionOpen = QtGui.QAction(parent=MainWindow)
icon4 = QtGui.QIcon() 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.setIcon(icon4)
self.actionOpen.setObjectName("actionOpen") self.actionOpen.setObjectName("actionOpen")
self.actionSave = QtGui.QAction(parent=MainWindow) self.actionSave = QtGui.QAction(parent=MainWindow)
icon5 = QtGui.QIcon() 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.setIcon(icon5)
self.actionSave.setObjectName("actionSave") self.actionSave.setObjectName("actionSave")
self.actionExit = QtGui.QAction(parent=MainWindow) self.actionExit = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon() 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.setIcon(icon6)
self.actionExit.setObjectName("actionExit") self.actionExit.setObjectName("actionExit")
self.actionUndo = QtGui.QAction(parent=MainWindow) self.actionUndo = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon() 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.setIcon(icon7)
self.actionUndo.setObjectName("actionUndo") self.actionUndo.setObjectName("actionUndo")
self.actionRedo = QtGui.QAction(parent=MainWindow) self.actionRedo = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon() 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.setIcon(icon8)
self.actionRedo.setObjectName("actionRedo") self.actionRedo.setObjectName("actionRedo")
self.actionAbout = QtGui.QAction(parent=MainWindow) self.actionAbout = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon() 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.setIcon(icon9)
self.actionAbout.setObjectName("actionAbout") self.actionAbout.setObjectName("actionAbout")
self.actionExport = QtGui.QAction(parent=MainWindow) self.actionExport = QtGui.QAction(parent=MainWindow)
self.actionExport.setEnabled(False) self.actionExport.setEnabled(False)
icon10 = QtGui.QIcon() 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.setIcon(icon10)
self.actionExport.setObjectName("actionExport") self.actionExport.setObjectName("actionExport")
self.actionSave_as = QtGui.QAction(parent=MainWindow) self.actionSave_as = QtGui.QAction(parent=MainWindow)
icon11 = QtGui.QIcon() 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.setIcon(icon11)
self.actionSave_as.setObjectName("actionSave_as") self.actionSave_as.setObjectName("actionSave_as")
self.menuFile.addAction(self.actionNew) self.menuFile.addAction(self.actionNew)
@ -135,7 +135,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Wargame campaign")) MainWindow.setWindowTitle(_translate("MainWindow", "WarChron"))
item = self.playersTable.horizontalHeaderItem(0) item = self.playersTable.horizontalHeaderItem(0)
item.setText(_translate("MainWindow", "Name")) item.setText(_translate("MainWindow", "Name"))
item = self.playersTable.horizontalHeaderItem(1) item = self.playersTable.horizontalHeaderItem(1)
@ -151,8 +151,11 @@ class Ui_MainWindow(object):
self.actionNew.setText(_translate("MainWindow", "New")) self.actionNew.setText(_translate("MainWindow", "New"))
self.actionNew.setShortcut(_translate("MainWindow", "Ctrl+N")) self.actionNew.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionOpen.setText(_translate("MainWindow", "Open")) self.actionOpen.setText(_translate("MainWindow", "Open"))
self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O"))
self.actionSave.setText(_translate("MainWindow", "Save")) self.actionSave.setText(_translate("MainWindow", "Save"))
self.actionSave.setShortcut(_translate("MainWindow", "Ctrl+S"))
self.actionExit.setText(_translate("MainWindow", "Exit")) self.actionExit.setText(_translate("MainWindow", "Exit"))
self.actionExit.setShortcut(_translate("MainWindow", "Ctrl+Shift+Q"))
self.actionUndo.setText(_translate("MainWindow", "Undo")) self.actionUndo.setText(_translate("MainWindow", "Undo"))
self.actionRedo.setText(_translate("MainWindow", "Redo")) self.actionRedo.setText(_translate("MainWindow", "Redo"))
self.actionAbout.setText(_translate("MainWindow", "About")) self.actionAbout.setText(_translate("MainWindow", "About"))

View file

@ -11,11 +11,11 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Wargame campaign</string> <string>WarChron</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset>
<normaloff>../resources/wargame_campaign_logo.png</normaloff>../resources/wargame_campaign_logo.png</iconset> <normaloff>../resources/warchron_logo.png</normaloff>../resources/warchron_logo.png</iconset>
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
@ -187,6 +187,9 @@
<property name="text"> <property name="text">
<string>Open</string> <string>Open</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action> </action>
<action name="actionSave"> <action name="actionSave">
<property name="icon"> <property name="icon">
@ -196,6 +199,9 @@
<property name="text"> <property name="text">
<string>Save</string> <string>Save</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action> </action>
<action name="actionExit"> <action name="actionExit">
<property name="icon"> <property name="icon">
@ -205,6 +211,9 @@
<property name="text"> <property name="text">
<string>Exit</string> <string>Exit</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+Shift+Q</string>
</property>
</action> </action>
<action name="actionUndo"> <action name="actionUndo">
<property name="icon"> <property name="icon">

View file

@ -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 # Created by: PyQt6 UI code generator 6.7.1
# #
@ -15,7 +15,7 @@ class Ui_playerDialog(object):
playerDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) playerDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
playerDialog.resize(378, 98) playerDialog.resize(378, 98)
icon = QtGui.QIcon() 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) playerDialog.setWindowIcon(icon)
self.buttonBox = QtWidgets.QDialogButtonBox(parent=playerDialog) self.buttonBox = QtWidgets.QDialogButtonBox(parent=playerDialog)
self.buttonBox.setGeometry(QtCore.QRect(10, 60, 341, 32)) self.buttonBox.setGeometry(QtCore.QRect(10, 60, 341, 32))

View file

@ -18,7 +18,7 @@
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset>
<normaloff>../resources/wargame_campaign_logo.png</normaloff>../resources/wargame_campaign_logo.png</iconset> <normaloff>../resources/warchron_logo.png</normaloff>../resources/warchron_logo.png</iconset>
</property> </property>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry"> <property name="geometry">

57
src/warchron/view/view.py Normal file
View file

@ -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()

View file

@ -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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View file

@ -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()