warchron_app/src/warchron/controller/controller.py
2026-01-28 16:25:40 +01:00

429 lines
No EOL
17 KiB
Python

from pathlib import Path
from datetime import datetime
from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.model.model import Model
from warchron.view.view import View
from warchron.constants import ItemType, RefreshScope
from warchron.view.view import PlayerDialog, WarDialog, CampaignDialog, ObjectiveDialog, ParticipantDialog
class Controller:
def __init__(self, model: Model, view: View):
self.model = model
self.view = view
self.current_file: Path | None = None
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
self.view.on_close_callback = self.on_app_close
self.is_dirty = False
self.__connect()
self.refresh_players_view()
self.refresh_wars_view()
self.update_window_title()
self.update_actions_state()
self.view.on_tree_selection_changed = self.on_tree_selection_changed
self.view.on_add_campaign = self.add_campaign
self.view.on_add_round = self.add_round
def __connect(self):
self.view.actionExit.triggered.connect(self.view.close)
self.view.actionNew.triggered.connect(self.new)
self.view.actionOpen.triggered.connect(self.open_file)
self.view.actionSave.triggered.connect(self.save)
self.view.actionSave_as.triggered.connect(self.save_as)
self.view.addPlayerBtn.clicked.connect(self.add_player)
self.view.addWarBtn.clicked.connect(self.add_war)
self.view.addObjectiveBtn.clicked.connect(self.add_objective)
self.view.addWarParticipantBtn.clicked.connect(self.add_war_participant)
self.view.on_edit_item = self.edit_item
self.view.on_delete_item = self.delete_item
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
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.refresh_wars_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.refresh_wars_view()
self.update_window_title()
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
def update_window_title(self):
base = "WarChron"
if self.current_file:
base += f" - {self.current_file.name}"
else:
base += f" - New file"
if self.is_dirty:
base = base + " *"
self.view.setWindowTitle(base)
def refresh_players_view(self):
players = self.model.get_all_players()
self.view.display_players(players)
def refresh_wars_view(self):
wars = self.model.get_all_wars()
self.view.display_wars_tree(wars)
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)
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)
def on_tree_selection_changed(self, selection):
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
if selection:
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
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
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
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 self._validate_player_inputs(name):
return
self.model.add_player(name)
self.is_dirty = True
self.refresh(RefreshScope.PLAYERS_LIST)
# War methods
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
def add_war(self):
dialog = WarDialog(self.view, default_year=self.model.get_default_war_values()["year"])
result = dialog.exec() # modal blocking dialog
if result == QDialog.DialogCode.Accepted:
name = dialog.get_war_name()
year = dialog.get_war_year()
if not self._validate_war_inputs(name, year):
return
war = self.model.add_war(name, year)
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
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
def add_campaign(self):
if not self.selected_war_id:
return
dialog = CampaignDialog(self.view, default_month=self.model.get_default_campaign_values(self.selected_war_id)["month"])
if dialog.exec() != QDialog.DialogCode.Accepted:
return
name = dialog.get_campaign_name()
month = dialog.get_campaign_month()
if not self._validate_campaign_inputs(name, month):
return
camp = self.model.add_campaign(self.selected_war_id, name, month)
self.is_dirty = True
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id)
# Round methods
def add_round(self):
if not self.selected_campaign_id:
return
rnd = self.model.add_round(self.selected_campaign_id)
self.is_dirty = True
self.refresh_and_select(RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id)