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

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

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
#
@ -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"))

View file

@ -11,11 +11,11 @@
</rect>
</property>
<property name="windowTitle">
<string>Wargame campaign</string>
<string>WarChron</string>
</property>
<property name="windowIcon">
<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>
<widget class="QWidget" name="centralwidget">
<widget class="QTabWidget" name="tabWidget">
@ -187,6 +187,9 @@
<property name="text">
<string>Open</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionSave">
<property name="icon">
@ -196,6 +199,9 @@
<property name="text">
<string>Save</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionExit">
<property name="icon">
@ -205,6 +211,9 @@
<property name="text">
<string>Exit</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+Q</string>
</property>
</action>
<action name="actionUndo">
<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
#
@ -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))

View file

@ -18,7 +18,7 @@
</property>
<property name="windowIcon">
<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>
<widget class="QDialogButtonBox" name="buttonBox">
<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()