diff --git a/game/observer.py b/game/observer.py new file mode 100644 index 00000000..d749c335 --- /dev/null +++ b/game/observer.py @@ -0,0 +1,21 @@ +class Event(object): + pass + + +class Observable(object): + def __init__(self) -> None: + self.callbacks = [] + + def subscribe(self, callback) -> None: + self.callbacks.append(callback) + + def unsubscribe(self, callback) -> None: + self.callbacks.remove(callback) + + def fire(self, **attrs) -> None: + e = Event() + e.source = self + for k, v in attrs.items(): + setattr(e, k, v) + for fn in self.callbacks: + fn(e) diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index 1a7df093..6a46a3a4 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -9,19 +9,39 @@ from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ObjectiveDistanceCache from ..theater import ControlPoint, MissionTarget +from ..observer import Observable + if TYPE_CHECKING: from ..ato.flighttype import FlightType from .squadron import Squadron -class AirWing: +class AirWing(Observable): def __init__(self, player: bool) -> None: + super().__init__() self.player = player self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) def add_squadron(self, squadron: Squadron) -> None: + new_aircraft_type = squadron.aircraft not in self.squadrons + self.squadrons[squadron.aircraft].append(squadron) + if new_aircraft_type: + self.fire(type="add_aircraft_type", obj=squadron.aircraft) + self.fire(type="add_squadron", obj=squadron) + + def remove_squadron(self, toRemove: Squadron) -> None: + if toRemove.aircraft in self.squadrons: + self.squadrons[toRemove.aircraft].remove(toRemove) + self.fire(type="remove_squadron", obj=toRemove) + if len(self.squadrons[toRemove.aircraft]) == 0: + self.remove_aircraft_type(toRemove.aircraft) + + def remove_aircraft_type(self, toRemove: AircraftType) -> None: + self.squadrons.pop(toRemove) + self.fire(type="remove_aircraft_type", obj=toRemove) + def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]: return self.squadrons[aircraft] diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index a372830c..cc9afeb9 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -24,15 +24,20 @@ from PySide2.QtWidgets import ( QVBoxLayout, QWidget, QCheckBox, + QPushButton, + QGridLayout, ) from game import Game from game.ato.flighttype import FlightType +from game.coalition import Coalition from game.dcs.aircrafttype import AircraftType from game.squadrons import AirWing, Pilot, Squadron from game.theater import ConflictTheater, ControlPoint from qt_ui.uiconstants import AIRCRAFT_ICONS +from qt_ui.windows.SquadronConfigPopup import SquadronConfigPopup + class AllowedMissionTypeControls(QVBoxLayout): def __init__(self, squadron: Squadron) -> None: @@ -67,6 +72,7 @@ class AllowedMissionTypeControls(QVBoxLayout): self.allowed_mission_types.add(task) else: self.allowed_mission_types.remove(task) + self.squadron.set_allowed_mission_types(self.allowed_mission_types) class SquadronBaseSelector(QComboBox): @@ -95,10 +101,13 @@ class SquadronBaseSelector(QComboBox): class SquadronConfigurationBox(QGroupBox): - def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None: + def __init__( + self, squadron: Squadron, theater: ConflictTheater, air_wing: AirWing + ) -> None: super().__init__() - self.setCheckable(True) + self.setCheckable(False) self.squadron = squadron + self.air_wing = air_wing self.reset_title() columns = QHBoxLayout() @@ -141,6 +150,14 @@ class SquadronConfigurationBox(QGroupBox): self.player_list.setAcceptRichText(False) self.player_list.setEnabled(squadron.player) left_column.addWidget(self.player_list) + self.player_list.textChanged.connect(self.on_pilots_changed) + + delete_button = QPushButton("Remove") + delete_button.setMaximumWidth(80) + delete_button.clicked.connect( + lambda state: self.air_wing.remove_squadron(self.squadron) + ) + left_column.addWidget(delete_button) left_column.addStretch() @@ -163,42 +180,56 @@ class SquadronConfigurationBox(QGroupBox): def reset_title(self) -> None: self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}") - def apply(self) -> Squadron: + def on_pilots_changed(self) -> None: player_names = self.player_list.toPlainText().splitlines() # Prepend player pilots so they get set active first. self.squadron.pilot_pool = [ Pilot(n, player=True) for n in player_names ] + self.squadron.pilot_pool - self.squadron.set_allowed_mission_types( - self.allowed_missions.allowed_mission_types - ) - return self.squadron class SquadronConfigurationLayout(QVBoxLayout): - def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None: + def __init__( + self, squadrons: list[Squadron], theater: ConflictTheater, air_wing: AirWing + ) -> None: super().__init__() - self.squadron_configs = [] + self.theater = theater + self.air_wing = air_wing + self.squadron_configs: dict[Squadron, SquadronConfigurationBox] = {} for squadron in squadrons: - squadron_config = SquadronConfigurationBox(squadron, theater) - self.squadron_configs.append(squadron_config) + squadron_config = SquadronConfigurationBox( + squadron, self.theater, self.air_wing + ) + self.squadron_configs[squadron] = squadron_config self.addWidget(squadron_config) - def apply(self) -> list[Squadron]: - keep_squadrons = [] - for squadron_config in self.squadron_configs: - if squadron_config.isChecked(): - keep_squadrons.append(squadron_config.apply()) - return keep_squadrons + def addSquadron(self, squadron: Squadron) -> None: + if squadron not in self.squadron_configs: + squadron_config = SquadronConfigurationBox( + squadron, self.theater, self.air_wing + ) + self.squadron_configs[squadron] = squadron_config + self.addWidget(squadron_config) + self.update() + + def removeSquadron(self, squadron: Squadron) -> None: + if squadron in self.squadron_configs: + self.removeWidget(self.squadron_configs[squadron]) + self.squadron_configs.pop(squadron) + self.update() class AircraftSquadronsPage(QWidget): - def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None: + def __init__( + self, squadrons: list[Squadron], theater: ConflictTheater, air_wing: AirWing + ) -> None: super().__init__() layout = QVBoxLayout() self.setLayout(layout) - self.squadrons_config = SquadronConfigurationLayout(squadrons, theater) + self.squadrons_config = SquadronConfigurationLayout( + squadrons, theater, air_wing + ) scrolling_widget = QWidget() scrolling_widget.setLayout(self.squadrons_config) @@ -211,23 +242,50 @@ class AircraftSquadronsPage(QWidget): layout.addWidget(scrolling_area) - def apply(self) -> list[Squadron]: - return self.squadrons_config.apply() + def addSquadron(self, squadron: Squadron) -> None: + self.squadrons_config.addSquadron(squadron) + + def removeSquadron(self, squadron: Squadron) -> None: + self.squadrons_config.removeSquadron(squadron) class AircraftSquadronsPanel(QStackedLayout): def __init__(self, air_wing: AirWing, theater: ConflictTheater) -> None: super().__init__() self.air_wing = air_wing + self.theater = theater + self.air_wing.subscribe(self.handleChanges) + self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} for aircraft, squadrons in self.air_wing.squadrons.items(): - page = AircraftSquadronsPage(squadrons, theater) + page = AircraftSquadronsPage(squadrons, self.theater, self.air_wing) self.addWidget(page) self.squadrons_pages[aircraft] = page - def apply(self) -> None: - for aircraft, page in self.squadrons_pages.items(): - self.air_wing.squadrons[aircraft] = page.apply() + def __del__(self) -> None: + self.air_wing.unsubscribe(self.handleChanges) + + def handleChanges(self, event) -> None: + if event.type == "add_aircraft_type": + aircraft_type = event.obj + if aircraft_type not in self.squadrons_pages: + page = AircraftSquadronsPage( + self.air_wing.squadrons[aircraft_type], self.theater, self.air_wing + ) + self.addWidget(page) + self.squadrons_pages[aircraft_type] = page + elif event.type == "remove_aircraft_type": + aircraft_type = event.obj + if aircraft_type in self.squadrons_pages: + self.removeWidget(self.squadrons_pages[aircraft_type]) + self.squadrons_pages.pop(aircraft_type) + elif event.type == "add_squadron": + squadron = event.obj + self.squadrons_pages[squadron.aircraft].addSquadron(squadron) + elif event.type == "remove_squadron": + squadron = event.obj + self.squadrons_pages[squadron.aircraft].removeSquadron(squadron) + self.update() class AircraftTypeList(QListView): @@ -238,21 +296,47 @@ class AircraftTypeList(QListView): self.setIconSize(QSize(91, 24)) self.setMinimumWidth(300) - model = QStandardItemModel(self) - self.setModel(model) + self.air_wing = air_wing - self.selectionModel().setCurrentIndex( - model.index(0, 0), QItemSelectionModel.Select - ) - self.selectionModel().selectionChanged.connect(self.on_selection_changed) - for aircraft in air_wing.squadrons: + self.item_model = QStandardItemModel(self) + self.setModel(self.item_model) + + for aircraft in self.air_wing.squadrons: aircraft_item = QStandardItem(aircraft.name) icon = self.icon_for(aircraft) if icon is not None: aircraft_item.setIcon(icon) aircraft_item.setEditable(False) aircraft_item.setSelectable(True) - model.appendRow(aircraft_item) + self.item_model.appendRow(aircraft_item) + + self.selectionModel().setCurrentIndex( + self.item_model.index(0, 0), QItemSelectionModel.Select + ) + self.selectionModel().selectionChanged.connect(self.on_selection_changed) + + self.air_wing.subscribe(self.handleChanges) + + def __del__(self) -> None: + self.air_wing.unsubscribe(self.handleChanges) + + def handleChanges(self, event) -> None: + if event.type == "remove_aircraft_type": + aircraft_type = event.obj + items = self.item_model.findItems(aircraft_type.name) + if len(items) == 1: + for item in items: + self.item_model.takeRow(item.row()) + elif event.type == "add_aircraft_type": + aircraft_type = event.obj + aircraft_item = QStandardItem(aircraft_type.name) + icon = self.icon_for(aircraft_type) + if icon is not None: + aircraft_item.setIcon(icon) + aircraft_item.setEditable(False) + aircraft_item.setSelectable(True) + self.item_model.appendRow(aircraft_item) + self.update() def on_selection_changed( self, selected: QItemSelection, _deselected: QItemSelection @@ -264,6 +348,18 @@ class AircraftTypeList(QListView): return self.page_index_changed.emit(indexes[0].row()) + def deleteSelectedType(self) -> None: + if self.selectionModel().currentIndex().isValid(): + aircraftName = str(self.selectionModel().currentIndex().data()) + to_remove = None + for type in self.air_wing.squadrons: + if str(type) == aircraftName: + to_remove = type + if to_remove != None: + self.air_wing.remove_aircraft_type(to_remove) + else: + raise RuntimeError("No aircraft was selected for removal") + @staticmethod def icon_for(aircraft: AircraftType) -> Optional[QIcon]: name = aircraft.dcs_id @@ -273,24 +369,37 @@ class AircraftTypeList(QListView): class AirWingConfigurationTab(QWidget): - def __init__(self, air_wing: AirWing, theater: ConflictTheater) -> None: + def __init__( + self, coalition: Coalition, theater: ConflictTheater, game: Game + ) -> None: super().__init__() + self.game = game + self.theater = theater + self.coalition = coalition + self.air_wing = coalition.air_wing - layout = QHBoxLayout() + layout = QGridLayout() self.setLayout(layout) - type_list = AircraftTypeList(air_wing) - type_list.page_index_changed.connect(self.on_aircraft_changed) - layout.addWidget(type_list) + self.type_list = AircraftTypeList(self.air_wing) - self.squadrons_panel = AircraftSquadronsPanel(air_wing, theater) - layout.addLayout(self.squadrons_panel) + layout.addWidget(self.type_list, 1, 1, 1, 2) - def apply(self) -> None: - self.squadrons_panel.apply() + add_button = QPushButton("Add Aircraft/Squadron") + add_button.clicked.connect(lambda state: self.addAircraftType()) + layout.addWidget(add_button, 2, 1, 1, 1) - def on_aircraft_changed(self, index: QModelIndex) -> None: - self.squadrons_panel.setCurrentIndex(index) + remove_button = QPushButton("Remove Aircraft") + remove_button.clicked.connect(lambda state: self.type_list.deleteSelectedType()) + layout.addWidget(remove_button, 2, 2, 1, 1) + + self.squadrons_panel = AircraftSquadronsPanel(self.air_wing, self.theater) + layout.addLayout(self.squadrons_panel, 1, 3, 2, 1) + + self.type_list.page_index_changed.connect(self.squadrons_panel.setCurrentIndex) + + def addAircraftType(self) -> None: + SquadronConfigPopup(self.coalition, self.theater, self.game).exec_() class AirWingConfigurationDialog(QDialog): @@ -328,12 +437,10 @@ class AirWingConfigurationDialog(QDialog): self.tabs = [] for coalition in game.coalitions: - coalition_tab = AirWingConfigurationTab(coalition.air_wing, game.theater) + coalition_tab = AirWingConfigurationTab(coalition, game.theater, game) name = "Blue" if coalition.player else "Red" tab_widget.addTab(coalition_tab, name) self.tabs.append(coalition_tab) def reject(self) -> None: - for tab in self.tabs: - tab.apply() super().reject() diff --git a/qt_ui/windows/SquadronConfigPopup.py b/qt_ui/windows/SquadronConfigPopup.py new file mode 100644 index 00000000..f90c75d0 --- /dev/null +++ b/qt_ui/windows/SquadronConfigPopup.py @@ -0,0 +1,145 @@ +from typing import Optional, Callable, Iterable + +from PySide2.QtWidgets import ( + QDialog, + QPushButton, + QVBoxLayout, + QLabel, + QLineEdit, + QTextEdit, + QCheckBox, + QHBoxLayout, + QComboBox, +) + +from game.dcs.aircrafttype import AircraftType +from game.game import Game +from game.squadrons import Squadron +from game.theater import ConflictTheater, ControlPoint +from game.coalition import Coalition +from game.factions.faction import Faction +from game.campaignloader.squadrondefgenerator import SquadronDefGenerator + +from gen.flights.flight import FlightType + + +class AircraftTypeSelector(QComboBox): + def __init__(self, faction: Faction) -> None: + super().__init__() + self.types = faction.aircrafts + self.setSizeAdjustPolicy(self.AdjustToContents) + + self.addItem("Select aircraft type...", None) + self.setCurrentText("Select aircraft type...") + self.types.sort(key=str) + + for type in self.types: + self.addItem(type.name, type) + + +class SquadronConfigPopup(QDialog): + def __init__( + self, coalition: Coalition, theater: ConflictTheater, game: Game + ) -> None: + super().__init__() + self.game = game + self.coalition = coalition + self.theater = theater + self.squadron = None + + # self.setMinimumSize(500, 800) + self.setWindowTitle(f"Add new Squadron") + + self.column = QVBoxLayout() + self.setLayout(self.column) + + self.aircraft_type_selector = AircraftTypeSelector(coalition.faction) + self.column.addWidget(self.aircraft_type_selector) + + self.column.addWidget(QLabel("Name:")) + self.name_edit = QLineEdit("---") + self.name_edit.setEnabled(False) + self.name_edit.textChanged.connect(self.on_name_changed) + self.column.addWidget(self.name_edit) + + self.column.addWidget(QLabel("Nickname:")) + self.nickname_edit = QLineEdit("---") + self.nickname_edit.setEnabled(False) + self.nickname_edit.textChanged.connect(self.on_nickname_changed) + self.column.addWidget(self.nickname_edit) + + self.column.addStretch() + self.aircraft_type_selector.currentIndexChanged.connect( + self.on_aircraft_selection + ) + + self.button_layout = QHBoxLayout() + self.column.addLayout(self.button_layout) + + self.accept_button = QPushButton("Accept") + self.accept_button.clicked.connect(lambda state: self.accept()) + self.accept_button.setEnabled(False) + self.button_layout.addWidget(self.accept_button) + + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(lambda state: self.cancel()) + self.button_layout.addWidget(self.cancel_button) + + def create_Squadron( + self, aircraft_type: AircraftType, base: ControlPoint + ) -> Squadron: + squadron_def = SquadronDefGenerator(self.coalition).generate_for_aircraft( + aircraft_type + ) + squadron = Squadron( + squadron_def.name, + squadron_def.nickname, + squadron_def.country, + squadron_def.role, + squadron_def.aircraft, + squadron_def.livery, + squadron_def.mission_types, + squadron_def.operating_bases, + squadron_def.pilot_pool, + self.coalition, + self.game.settings, + base, + ) + return squadron + + def on_aircraft_selection(self, index: int) -> None: + aircraft_type_name = self.aircraft_type_selector.currentText() + aircraft_type = None + for aircraft in self.coalition.faction.aircrafts: + if str(aircraft) == aircraft_type_name: + aircraft_type = aircraft + + if aircraft != None: + self.squadron = self.create_Squadron( + aircraft_type, + next(self.theater.control_points_for(self.coalition.player)), + ) + + self.name_edit.setText(self.squadron.name) + self.name_edit.setEnabled(True) + + self.nickname_edit.setText(self.squadron.nickname) + self.nickname_edit.setEnabled(True) + + self.accept_button.setStyleSheet("background-color: green") + self.accept_button.setEnabled(True) + + self.update() + + def on_name_changed(self, text: str) -> None: + self.squadron.name = text + + def on_nickname_changed(self, text: str) -> None: + self.squadron.nickname = text + + def accept(self) -> None: + self.coalition.air_wing.add_squadron(self.squadron) + return super().accept() + + def cancel(self) -> None: + return super().reject()