warchron_app/src/warchron/controller/controller.py

429 lines
17 KiB
Python
Raw Normal View History

2026-01-19 11:16:23 +01:00
from pathlib import Path
2026-01-22 23:42:47 +01:00
from datetime import datetime
2026-01-19 11:16:23 +01:00
from PyQt6.QtWidgets import QMessageBox, QDialog
2026-01-21 07:43:04 +01:00
from warchron.model.model import Model
from warchron.view.view import View
2026-01-19 11:16:23 +01:00
from warchron.constants import ItemType, RefreshScope
from warchron.view.view import PlayerDialog, WarDialog, CampaignDialog, ObjectiveDialog, ParticipantDialog
2026-01-19 11:16:23 +01:00
class Controller:
2026-01-21 07:43:04 +01:00
def __init__(self, model: Model, view: View):
2026-01-19 11:16:23 +01:00
self.model = model
self.view = view
self.current_file: Path | None = None
2026-01-21 07:43:04 +01:00
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
2026-01-19 11:16:23 +01:00
self.view.on_close_callback = self.on_app_close
self.is_dirty = False
self.__connect()
self.refresh_players_view()
2026-01-20 08:46:58 +01:00
self.refresh_wars_view()
2026-01-19 11:29:41 +01:00
self.update_window_title()
2026-01-21 07:43:04 +01:00
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
2026-01-19 11:16:23 +01:00
def __connect(self):
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)
2026-01-20 08:46:58 +01:00
self.view.addPlayerBtn.clicked.connect(self.add_player)
self.view.addWarBtn.clicked.connect(self.add_war)
self.view.addObjectiveBtn.clicked.connect(self.add_objective)
self.view.addWarParticipantBtn.clicked.connect(self.add_war_participant)
2026-01-22 23:42:47 +01:00
self.view.on_edit_item = self.edit_item
self.view.on_delete_item = self.delete_item
2026-01-19 11:16:23 +01:00
2026-01-20 08:46:58 +01:00
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
# Menu bar methods
2026-01-20 08:46:58 +01:00
2026-01-19 11:16:23 +01:00
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()
2026-01-20 08:46:58 +01:00
self.refresh_wars_view()
2026-01-19 11:16:23 +01:00
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()
2026-01-20 08:46:58 +01:00
self.refresh_wars_view()
2026-01-19 11:16:23 +01:00
self.update_window_title()
2026-01-20 08:46:58 +01:00
2026-01-19 11:16:23 +01:00
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()
# Display methods
2026-01-19 11:16:23 +01:00
def update_window_title(self):
base = "WarChron"
if self.current_file:
base += f" - {self.current_file.name}"
2026-01-19 11:29:41 +01:00
else:
base += f" - New file"
2026-01-19 11:16:23 +01:00
if self.is_dirty:
2026-01-19 11:29:41 +01:00
base = base + " *"
2026-01-19 11:16:23 +01:00
self.view.setWindowTitle(base)
2026-01-20 08:46:58 +01:00
def refresh_players_view(self):
players = self.model.get_all_players()
self.view.display_players(players)
2026-01-19 11:16:23 +01:00
2026-01-21 07:43:04 +01:00
def refresh_wars_view(self):
wars = self.model.get_all_wars()
2026-01-27 11:49:37 +01:00
self.view.display_wars_tree(wars)
2026-01-21 07:43:04 +01:00
2026-01-27 11:49:37 +01:00
def _fill_war_details(self, war_id: str):
war = self.model.get_war(war_id)
self.view.show_war_details(name=war.name, year=war.year)
objectives = war.get_all_objectives()
self.view.display_war_objectives(objectives)
participants = war.get_all_war_participants()
participants_for_display = [
(self.model.get_player_name(p.id), p.faction, p.id)
for p in participants
]
self.view.display_war_participants(participants_for_display)
2026-01-27 11:49:37 +01:00
def _fill_campaign_details(self, campaign_id: str):
camp = self.model.get_campaign(campaign_id)
self.view.show_campaign_details(name=camp.name, month=camp.month)
def _fill_round_details(self, round_id: str):
index = self.model.get_round_index(round_id)
self.view.show_round_details(index=index)
2026-01-21 07:43:04 +01:00
def on_tree_selection_changed(self, selection):
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
if selection:
2026-01-27 11:49:37 +01:00
item_type = selection["type"]
item_id = selection["id"]
if item_type == ItemType.WAR:
self.selected_war_id = item_id
self.view.show_details(ItemType.WAR)
self._fill_war_details(item_id)
elif item_type == ItemType.CAMPAIGN:
self.selected_campaign_id = item_id
self.view.show_details(ItemType.CAMPAIGN)
self._fill_campaign_details(item_id)
elif item_type == ItemType.ROUND:
self.selected_round_id = item_id
self.view.show_details(ItemType.ROUND)
self._fill_round_details(item_id)
else:
self.view.show_details(None)
self.update_actions_state()
return
2026-01-21 07:43:04 +01:00
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 refresh(self, scope: RefreshScope):
match scope:
case RefreshScope.PLAYERS_LIST:
self.refresh_players_view()
case RefreshScope.WARS_TREE:
self.refresh_wars_view()
case RefreshScope.WAR_DETAILS:
if self.selected_war_id:
self.view.show_details(ItemType.WAR)
self._fill_war_details(self.selected_war_id)
case RefreshScope.CAMPAIGN_DETAILS:
if self.selected_campaign_id:
self.view.show_details(ItemType.CAMPAIGN)
self._fill_campaign_details(self.selected_campaign_id)
case RefreshScope.ROUND_DETAILS:
if self.selected_round_id:
self.view.show_details(ItemType.ROUND)
self._fill_round_details(self.selected_round_id)
self.update_window_title()
# Common command methods
def refresh_and_select(self, scope: RefreshScope, *, item_type: ItemType, item_id: str):
self.refresh(scope)
self.view.select_tree_item(item_type=item_type, item_id=item_id)
def edit_item(self, item_type: str, item_id: str):
if item_type == ItemType.PLAYER:
play = self.model.get_player(item_id)
dialog = PlayerDialog(self.view, default_name=play.name)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_player_name()
if not self._validate_player_inputs(name):
return
self.model.update_player(item_id, name=name)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
war = self.model.get_war(item_id)
dialog = WarDialog(self.view, default_name=war.name, default_year=war.year)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_war_name()
year = dialog.get_war_year()
if not self._validate_war_inputs(name, year):
return
self.model.update_war(item_id, name=name, year=year)
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id)
elif item_type == ItemType.CAMPAIGN:
camp = self.model.get_campaign(item_id)
dialog = CampaignDialog(self.view, default_name=camp.name, default_month=camp.month)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_campaign_name()
month = dialog.get_campaign_month()
if not self._validate_campaign_inputs(name, month):
return
self.model.update_campaign(item_id, name=name, month=month)
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id)
elif item_type == ItemType.OBJECTIVE:
obj = self.model.get_objective(item_id)
dialog = ObjectiveDialog(self.view, default_name=obj.name, default_description=obj.description)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_objective_name()
description = dialog.get_objective_description()
if not self._validate_objective_inputs(name, description):
return
self.model.update_objective(item_id, name=name, description=description)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
part = self.model.get_war_participant(item_id)
player = self.model.get_player(part.id)
dialog = ParticipantDialog(self.view, players=[player], default_player_id=part.id, default_faction=part.faction)
if dialog.exec() == QDialog.DialogCode.Accepted:
id = dialog.get_player_id()
faction = dialog.get_participant_faction()
self.model.update_war_participant(item_id, faction=faction)
self.refresh(RefreshScope.WAR_DETAILS)
self.is_dirty = True
def delete_item(self, item_type: str, item_id: str):
reply = QMessageBox.question(
self.view,
"Confirm deletion",
"Are you sure you want to delete this item?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
return
if item_type == ItemType.PLAYER:
self.model.remove_player(item_id)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
self.model.remove_war(item_id)
self.refresh(RefreshScope.WARS_TREE)
elif item_type == ItemType.CAMPAIGN:
war = self.model.get_war_by_campaign(item_id)
war_id = war.id
self.model.remove_campaign(item_id)
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id)
elif item_type == ItemType.OBJECTIVE:
self.model.remove_objective(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
self.model.remove_war_participant(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.ROUND:
camp = self.model.get_campaign_by_round(item_id)
camp_id = camp.id
self.model.remove_round(item_id)
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id)
self.is_dirty = True
# Player methods
2026-01-22 23:42:47 +01:00
def _validate_player_inputs(self, name: str) -> bool:
if not name.strip():
QMessageBox.warning(
self.view,
"Invalid name",
"Player name cannot be empty."
)
return False
return True
2026-01-19 11:16:23 +01:00
def add_player(self):
dialog = PlayerDialog(self.view)
result = dialog.exec() # modal blocking dialog
if result == QDialog.DialogCode.Accepted:
name = dialog.get_player_name()
2026-01-22 23:42:47 +01:00
if not self._validate_player_inputs(name):
2026-01-19 11:16:23 +01:00
return
self.model.add_player(name)
self.is_dirty = True
self.refresh(RefreshScope.PLAYERS_LIST)
# War methods
2026-01-22 23:42:47 +01:00
def _validate_war_inputs(self, name: str, year: int) -> bool:
if not name.strip():
QMessageBox.warning(
self.view,
"Invalid name",
"War name cannot be empty."
)
return False
if not (1970 <= year <= 3000):
QMessageBox.warning(
self.view,
"Invalid year",
"Year must be between 1970 and 3000."
)
return False
return True
2026-01-20 08:46:58 +01:00
def add_war(self):
2026-01-22 23:42:47 +01:00
dialog = WarDialog(self.view, default_year=self.model.get_default_war_values()["year"])
2026-01-20 08:46:58 +01:00
result = dialog.exec() # modal blocking dialog
if result == QDialog.DialogCode.Accepted:
name = dialog.get_war_name()
2026-01-22 23:42:47 +01:00
year = dialog.get_war_year()
if not self._validate_war_inputs(name, year):
2026-01-20 08:46:58 +01:00
return
war = self.model.add_war(name, year)
2026-01-20 08:46:58 +01:00
self.is_dirty = True
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id)
# Objective methods
def _validate_objective_inputs(self, name: str, description: int) -> bool:
if not name.strip():
QMessageBox.warning(
self.view,
"Invalid name",
"Campaign name cannot be empty."
)
return False
return True
def add_objective(self):
if not self.selected_war_id:
return
dialog = ObjectiveDialog(self.view)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
name = dialog.get_objective_name()
description = dialog.get_objective_description()
if not name:
return
self.model.add_objective(self.selected_war_id, name, description)
self.is_dirty = True
self.refresh(RefreshScope.WAR_DETAILS)
# War participant methods
def add_war_participant(self):
if not self.selected_war_id:
return
players = self.model.get_available_players(self.selected_war_id)
dialog = ParticipantDialog(self.view, players=players)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
player_id = dialog.get_player_id()
faction = dialog.get_participant_faction()
if not player_id:
return
self.model.add_war_participant(self.selected_war_id, player_id, faction)
self.is_dirty = True
self.refresh(RefreshScope.WAR_DETAILS)
# Campaign methods
2026-01-21 07:43:04 +01:00
2026-01-22 23:42:47 +01:00
def _validate_campaign_inputs(self, name: str, month: int) -> bool:
if not name.strip():
QMessageBox.warning(
self.view,
"Invalid name",
"Campaign name cannot be empty."
)
return False
if not (1 <= month <= 12):
QMessageBox.warning(
self.view,
"Invalid month",
"Month must be between 1 and 12."
)
return False
return True
2026-01-21 07:43:04 +01:00
def add_campaign(self):
if not self.selected_war_id:
return
2026-01-22 23:42:47 +01:00
dialog = CampaignDialog(self.view, default_month=self.model.get_default_campaign_values(self.selected_war_id)["month"])
2026-01-21 07:43:04 +01:00
if dialog.exec() != QDialog.DialogCode.Accepted:
return
name = dialog.get_campaign_name()
2026-01-22 23:42:47 +01:00
month = dialog.get_campaign_month()
if not self._validate_campaign_inputs(name, month):
2026-01-21 07:43:04 +01:00
return
camp = self.model.add_campaign(self.selected_war_id, name, month)
2026-01-21 07:43:04 +01:00
self.is_dirty = True
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id)
# Round methods
2026-01-21 07:43:04 +01:00
def add_round(self):
if not self.selected_campaign_id:
return
rnd = self.model.add_round(self.selected_campaign_id)
2026-01-22 23:42:47 +01:00
self.is_dirty = True
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id)