diff --git a/game/campaignloader/defaultsquadronassigner.py b/game/campaignloader/defaultsquadronassigner.py index 5adbe387..54cd66e5 100644 --- a/game/campaignloader/defaultsquadronassigner.py +++ b/game/campaignloader/defaultsquadronassigner.py @@ -5,10 +5,8 @@ from typing import Optional, TYPE_CHECKING from game.squadrons import Squadron from game.squadrons.squadrondef import SquadronDef -from game.squadrons.squadrondefloader import SquadronDefLoader from ..ato.flighttype import FlightType from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig -from .squadrondefgenerator import SquadronDefGenerator from ..dcs.aircrafttype import AircraftType from ..theater import ControlPoint @@ -25,14 +23,6 @@ class DefaultSquadronAssigner: self.game = game self.coalition = coalition self.air_wing = coalition.air_wing - self.squadron_defs = SquadronDefLoader(game, coalition).load() - self.squadron_def_generator = SquadronDefGenerator(self.coalition) - - def claim_squadron_def(self, squadron: SquadronDef) -> None: - try: - self.squadron_defs[squadron.aircraft].remove(squadron) - except ValueError: - pass def assign(self) -> None: for control_point in self.game.theater.control_points_for( @@ -47,7 +37,6 @@ class DefaultSquadronAssigner: ) continue - self.claim_squadron_def(squadron_def) squadron = Squadron.create_from( squadron_def, control_point, self.coalition, self.game ) @@ -74,7 +63,7 @@ class DefaultSquadronAssigner: # If we can't find any squadron matching the requirement, we should # create one. - return self.squadron_def_generator.generate_for_task( + return self.air_wing.squadron_def_generator.generate_for_task( config.primary, control_point ) @@ -105,7 +94,7 @@ class DefaultSquadronAssigner: # No premade squadron available for this aircraft that meets the requirements, # so generate one if possible. - return self.squadron_def_generator.generate_for_aircraft(aircraft) + return self.air_wing.squadron_def_generator.generate_for_aircraft(aircraft) @staticmethod def squadron_compatible_with( @@ -121,18 +110,24 @@ class DefaultSquadronAssigner: def find_squadron_for_airframe( self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint ) -> Optional[SquadronDef]: - for squadron in self.squadron_defs[aircraft]: - if self.squadron_compatible_with(squadron, task, control_point): + for squadron in self.air_wing.squadron_defs[aircraft]: + if not squadron.claimed and self.squadron_compatible_with( + squadron, task, control_point + ): return squadron return None def find_squadron_by_name( self, name: str, task: FlightType, control_point: ControlPoint ) -> Optional[SquadronDef]: - for squadrons in self.squadron_defs.values(): + for squadrons in self.air_wing.squadron_defs.values(): for squadron in squadrons: - if squadron.name == name and self.squadron_compatible_with( - squadron, task, control_point, ignore_base_preference=True + if ( + not squadron.claimed + and squadron.name == name + and self.squadron_compatible_with( + squadron, task, control_point, ignore_base_preference=True + ) ): return squadron return None @@ -140,8 +135,10 @@ class DefaultSquadronAssigner: def find_squadron_for_task( self, task: FlightType, control_point: ControlPoint ) -> Optional[SquadronDef]: - for squadrons in self.squadron_defs.values(): + for squadrons in self.air_wing.squadron_defs.values(): for squadron in squadrons: - if self.squadron_compatible_with(squadron, task, control_point): + if not squadron.claimed and self.squadron_compatible_with( + squadron, task, control_point + ): return squadron return None diff --git a/game/campaignloader/squadrondefgenerator.py b/game/campaignloader/squadrondefgenerator.py index 63a9c269..82462910 100644 --- a/game/campaignloader/squadrondefgenerator.py +++ b/game/campaignloader/squadrondefgenerator.py @@ -12,12 +12,12 @@ from gen.flights.ai_flight_planner_db import aircraft_for_task, tasks_for_aircra from game.ato.flighttype import FlightType if TYPE_CHECKING: - from game.coalition import Coalition + from game.factions.faction import Faction class SquadronDefGenerator: - def __init__(self, coalition: Coalition) -> None: - self.coalition = coalition + def __init__(self, faction: Faction) -> None: + self.faction = faction self.count = itertools.count(1) self.used_nicknames: set[str] = set() @@ -26,7 +26,7 @@ class SquadronDefGenerator: ) -> Optional[SquadronDef]: aircraft_choice: Optional[AircraftType] = None for aircraft in aircraft_for_task(task): - if aircraft not in self.coalition.faction.aircrafts: + if aircraft not in self.faction.aircrafts: continue if not control_point.can_operate(aircraft): continue @@ -44,7 +44,7 @@ class SquadronDefGenerator: return SquadronDef( name=f"Squadron {next(self.count):03}", nickname=self.random_nickname(), - country=self.coalition.country_name, + country=self.faction.country, role="Flying Squadron", aircraft=aircraft, livery=None, diff --git a/game/coalition.py b/game/coalition.py index 128ac873..d7ee4d81 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -40,7 +40,7 @@ class Coalition: self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet() self.bullseye = Bullseye(Point(0, 0)) self.faker = Faker(self.faction.locales) - self.air_wing = AirWing(player) + self.air_wing = AirWing(player, game, self.faction) self.transfers = PendingTransfers(game, player) # Late initialized because the two coalitions in the game are mutually diff --git a/game/observer.py b/game/observer.py deleted file mode 100644 index d749c335..00000000 --- a/game/observer.py +++ /dev/null @@ -1,21 +0,0 @@ -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 6a46a3a4..109d1fe1 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -7,41 +7,34 @@ from typing import Sequence, Iterator, TYPE_CHECKING, Optional from game.dcs.aircrafttype import AircraftType from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ObjectiveDistanceCache +from .squadrondef import SquadronDef +from .squadrondefloader import SquadronDefLoader +from ..campaignloader.squadrondefgenerator import SquadronDefGenerator +from ..factions.faction import Faction from ..theater import ControlPoint, MissionTarget -from ..observer import Observable - if TYPE_CHECKING: + from game.game import Game from ..ato.flighttype import FlightType from .squadron import Squadron -class AirWing(Observable): - def __init__(self, player: bool) -> None: - super().__init__() +class AirWing: + def __init__(self, player: bool, game: Game, faction: Faction) -> None: self.player = player self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) + self.squadron_defs = SquadronDefLoader(game, faction).load() + self.squadron_def_generator = SquadronDefGenerator(faction) + + def unclaim_squadron_def(self, squadron: Squadron) -> None: + if squadron.aircraft in self.squadron_defs: + for squadron_def in self.squadron_defs[squadron.aircraft]: + if squadron_def.claimed and squadron_def.name == squadron.name: + squadron_def.claimed = False 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/game/squadrons/squadron.py b/game/squadrons/squadron.py index 9edcdb3e..f2078bd9 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -418,6 +418,7 @@ class Squadron: coalition: Coalition, game: Game, ) -> Squadron: + squadron_def.claimed = True return Squadron( squadron_def.name, squadron_def.nickname, diff --git a/game/squadrons/squadrondef.py b/game/squadrons/squadrondef.py index 3551521f..6ed28815 100644 --- a/game/squadrons/squadrondef.py +++ b/game/squadrons/squadrondef.py @@ -28,6 +28,7 @@ class SquadronDef: mission_types: tuple[FlightType, ...] operating_bases: OperatingBases pilot_pool: list[Pilot] + claimed: bool = False auto_assignable_mission_types: set[FlightType] = field( init=False, hash=False, compare=False diff --git a/game/squadrons/squadrondefloader.py b/game/squadrons/squadrondefloader.py index 5af93576..27f906d1 100644 --- a/game/squadrons/squadrondefloader.py +++ b/game/squadrons/squadrondefloader.py @@ -10,13 +10,13 @@ from .squadrondef import SquadronDef if TYPE_CHECKING: from game import Game - from game.coalition import Coalition + from ..factions.faction import Faction class SquadronDefLoader: - def __init__(self, game: Game, coalition: Coalition) -> None: + def __init__(self, game: Game, faction: Faction) -> None: self.game = game - self.coalition = coalition + self.faction = faction @staticmethod def squadron_directories() -> Iterator[Path]: @@ -27,8 +27,8 @@ class SquadronDefLoader: def load(self) -> dict[AircraftType, list[SquadronDef]]: squadrons: dict[AircraftType, list[SquadronDef]] = defaultdict(list) - country = self.coalition.country_name - faction = self.coalition.faction + country = self.faction.country + faction = self.faction any_country = country.startswith("Combined Joint Task Forces ") for directory in self.squadron_directories(): for path, squadron_def in self.load_squadrons_from(directory): diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index ceab4893..491053bf 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -111,6 +111,9 @@ def load_icons(): "./resources/ui/misc/" + get_theme_icons() + "/pluginsoptions.png" ) ICONS["Notes"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/notes.png") + ICONS["Reload"] = QPixmap( + "./resources/ui/misc/" + get_theme_icons() + "/reload.png" + ) ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png") ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png") diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index cc9afeb9..90e26413 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -26,6 +26,7 @@ from PySide2.QtWidgets import ( QCheckBox, QPushButton, QGridLayout, + QToolButton, ) from game import Game @@ -33,10 +34,9 @@ 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.squadrons.squadrondef import SquadronDef from game.theater import ConflictTheater, ControlPoint -from qt_ui.uiconstants import AIRCRAFT_ICONS - -from qt_ui.windows.SquadronConfigPopup import SquadronConfigPopup +from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS class AllowedMissionTypeControls(QVBoxLayout): @@ -72,7 +72,6 @@ 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): @@ -85,29 +84,39 @@ class SquadronBaseSelector(QComboBox): def __init__( self, bases: Iterable[ControlPoint], - squadron: Squadron, + selected_base: Optional[ControlPoint], + aircraft_type: Optional[AircraftType], ) -> None: super().__init__() - self.bases = list(bases) - self.squadron = squadron self.setSizeAdjustPolicy(self.AdjustToContents) + self.bases = list(bases) + self.set_aircraft_type(aircraft_type) - for base in self.bases: - if not base.can_operate(self.squadron.aircraft): - continue - self.addItem(base.name, base) - self.model().sort(0) - self.setCurrentText(self.squadron.location.name) + if selected_base: + self.setCurrentText(selected_base.name) + # TODO can we get a prefered base if none is selected? + + def set_aircraft_type(self, aircraft_type: Optional[AircraftType]): + self.clear() + if aircraft_type: + for base in self.bases: + if not base.can_operate(aircraft_type): + continue + self.addItem(base.name, base) + self.model().sort(0) + self.setEnabled(True) + else: + self.addItem("Select aircraft type first", None) + self.setEnabled(False) + self.update() class SquadronConfigurationBox(QGroupBox): - def __init__( - self, squadron: Squadron, theater: ConflictTheater, air_wing: AirWing - ) -> None: + remove_squadron_signal = Signal(Squadron) + + def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None: super().__init__() - self.setCheckable(False) self.squadron = squadron - self.air_wing = air_wing self.reset_title() columns = QHBoxLayout() @@ -121,14 +130,24 @@ class SquadronConfigurationBox(QGroupBox): self.name_edit.textChanged.connect(self.on_name_changed) left_column.addWidget(self.name_edit) - left_column.addWidget(QLabel("Nickname:")) + nickname_edit_layout = QGridLayout() + left_column.addLayout(nickname_edit_layout) + + nickname_edit_layout.addWidget(QLabel("Nickname:"), 0, 0, 1, 2) self.nickname_edit = QLineEdit(squadron.nickname) self.nickname_edit.textChanged.connect(self.on_nickname_changed) - left_column.addWidget(self.nickname_edit) + nickname_edit_layout.addWidget(self.nickname_edit, 1, 0, Qt.AlignTop) + reroll_nickname_button = QToolButton() + reroll_nickname_button.setIcon(QIcon(ICONS["Reload"])) + reroll_nickname_button.setToolTip("Re-roll nickname") + reroll_nickname_button.clicked.connect(self.reroll_nickname) + nickname_edit_layout.addWidget(reroll_nickname_button, 1, 1, Qt.AlignTop) left_column.addWidget(QLabel("Base:")) self.base_selector = SquadronBaseSelector( - theater.control_points_for(squadron.player), squadron + theater.control_points_for(squadron.player), + squadron.location, + squadron.aircraft, ) self.base_selector.currentIndexChanged.connect(self.on_base_changed) left_column.addWidget(self.base_selector) @@ -150,20 +169,18 @@ 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) - ) + delete_button = QPushButton("Remove Squadron") + delete_button.setMaximumWidth(140) + delete_button.clicked.connect(self.remove_from_squadron_config) left_column.addWidget(delete_button) - left_column.addStretch() self.allowed_missions = AllowedMissionTypeControls(squadron) columns.addLayout(self.allowed_missions) + def remove_from_squadron_config(self) -> None: + self.remove_squadron_signal.emit(self.squadron) + def on_name_changed(self, text: str) -> None: self.squadron.name = text self.reset_title() @@ -180,56 +197,66 @@ class SquadronConfigurationBox(QGroupBox): def reset_title(self) -> None: self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}") - def on_pilots_changed(self) -> None: + def reroll_nickname(self) -> None: + self.nickname_edit.setText( + self.squadron.coalition.air_wing.squadron_def_generator.random_nickname() + ) + + def apply(self) -> Squadron: 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, air_wing: AirWing - ) -> None: + config_changed = Signal(AircraftType) + + def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> 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, self.theater, self.air_wing - ) - self.squadron_configs[squadron] = squadron_config - self.addWidget(squadron_config) + self.add_squadron(squadron) - 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 apply(self) -> list[Squadron]: + keep_squadrons = [] + for squadron_config in self.squadron_configs: + keep_squadrons.append(squadron_config.apply()) + return keep_squadrons - 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() + def remove_squadron(self, squadron: Squadron) -> None: + for squadron_config in self.squadron_configs: + if squadron_config.squadron == squadron: + squadron_config.deleteLater() + self.squadron_configs.remove(squadron_config) + squadron.coalition.air_wing.unclaim_squadron_def(squadron) + self.update() + self.config_changed.emit(squadron.aircraft) + return + + def add_squadron(self, squadron: Squadron) -> None: + squadron_config = SquadronConfigurationBox(squadron, self.theater) + squadron_config.remove_squadron_signal.connect(self.remove_squadron) + self.squadron_configs.append(squadron_config) + self.addWidget(squadron_config) class AircraftSquadronsPage(QWidget): - def __init__( - self, squadrons: list[Squadron], theater: ConflictTheater, air_wing: AirWing - ) -> None: + remove_squadron_page = Signal(AircraftType) + + def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None: super().__init__() layout = QVBoxLayout() self.setLayout(layout) - self.squadrons_config = SquadronConfigurationLayout( - squadrons, theater, air_wing - ) + self.squadrons_config = SquadronConfigurationLayout(squadrons, theater) + self.squadrons_config.config_changed.connect(self.on_squadron_config_changed) scrolling_widget = QWidget() scrolling_widget.setLayout(self.squadrons_config) @@ -242,51 +269,59 @@ class AircraftSquadronsPage(QWidget): layout.addWidget(scrolling_area) - def addSquadron(self, squadron: Squadron) -> None: - self.squadrons_config.addSquadron(squadron) + def on_squadron_config_changed(self, aircraft_type: AircraftType): + if len(self.squadrons_config.squadron_configs) == 0: + self.remove_squadron_page.emit(aircraft_type) - def removeSquadron(self, squadron: Squadron) -> None: - self.squadrons_config.removeSquadron(squadron) + def add_squadron_to_page(self, squadron: Squadron): + self.squadrons_config.add_squadron(squadron) + + def apply(self) -> list[Squadron]: + return self.squadrons_config.apply() class AircraftSquadronsPanel(QStackedLayout): + page_removed = Signal(AircraftType) + 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, self.theater, self.air_wing) - self.addWidget(page) - self.squadrons_pages[aircraft] = page + self.new_page_for_type(aircraft, squadrons) - 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) + def remove_page_for_type(self, aircraft_type: AircraftType): + page = self.squadrons_pages[aircraft_type] + self.removeWidget(page) + page.deleteLater() + self.squadrons_pages.pop(aircraft_type) + self.page_removed.emit(aircraft_type) self.update() + def new_page_for_type( + self, aircraft_type: AircraftType, squadrons: list[Squadron] + ) -> None: + page = AircraftSquadronsPage(squadrons, self.theater) + page.remove_squadron_page.connect(self.remove_page_for_type) + self.addWidget(page) + self.squadrons_pages[aircraft_type] = page + + def add_squadron_to_panel(self, squadron: Squadron): + # Find existing page or add new one + if squadron.aircraft in self.squadrons_pages: + page = self.squadrons_pages[squadron.aircraft] + page.add_squadron_to_page(squadron) + else: + self.new_page_for_type(squadron.aircraft, [squadron]) + + self.update() + + def apply(self) -> None: + self.air_wing.squadrons = {} + for aircraft, page in self.squadrons_pages.items(): + self.air_wing.squadrons[aircraft] = page.apply() + class AircraftTypeList(QListView): page_index_changed = Signal(int) @@ -296,47 +331,29 @@ class AircraftTypeList(QListView): self.setIconSize(QSize(91, 24)) self.setMinimumWidth(300) - self.air_wing = air_wing - 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) - 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) + for aircraft in air_wing.squadrons: + self.add_aircraft_type(aircraft) - self.air_wing.subscribe(self.handleChanges) + def remove_aircraft_type(self, aircraft: AircraftType): + for item in self.item_model.findItems(aircraft.name): + self.item_model.removeRow(item.row()) + self.page_index_changed.emit(self.selectionModel().currentIndex().row()) - 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 add_aircraft_type(self, aircraft: AircraftType): + 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) + self.item_model.appendRow(aircraft_item) def on_selection_changed( self, selected: QItemSelection, _deselected: QItemSelection @@ -348,18 +365,6 @@ 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 @@ -369,37 +374,69 @@ class AircraftTypeList(QListView): class AirWingConfigurationTab(QWidget): - def __init__( - self, coalition: Coalition, theater: ConflictTheater, game: Game - ) -> None: + def __init__(self, coalition: Coalition, game: Game) -> None: super().__init__() - self.game = game - self.theater = theater - self.coalition = coalition - self.air_wing = coalition.air_wing layout = QGridLayout() self.setLayout(layout) + self.game = game + self.coalition = coalition - self.type_list = AircraftTypeList(self.air_wing) + self.type_list = AircraftTypeList(coalition.air_wing) layout.addWidget(self.type_list, 1, 1, 1, 2) - add_button = QPushButton("Add Aircraft/Squadron") - add_button.clicked.connect(lambda state: self.addAircraftType()) + add_button = QPushButton("Add Squadron") + add_button.clicked.connect(lambda state: self.add_squadron()) layout.addWidget(add_button, 2, 1, 1, 1) - 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) + self.squadrons_panel = AircraftSquadronsPanel(coalition.air_wing, game.theater) + self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type) 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_() + def add_squadron(self) -> None: + selected_aircraft = None + if self.type_list.selectionModel().currentIndex().row() >= 0: + selected_aircraft = self.type_list.item_model.item( + self.type_list.selectionModel().currentIndex().row() + ).text() + + popup = SquadronConfigPopup( + selected_aircraft, + self.coalition.faction.aircrafts, + list(self.game.theater.control_points_for(self.coalition.player)), + self.coalition.air_wing.squadron_defs, + ) + if popup.exec_() != QDialog.Accepted: + return + + selected_type = popup.aircraft_type_selector.currentData() + selected_base = popup.squadron_base_selector.currentData() + selected_def = popup.squadron_def_selector.currentData() + + # Let user choose the preset or generate one + squadron_def = ( + selected_def + or self.coalition.air_wing.squadron_def_generator.generate_for_aircraft( + selected_type + ) + ) + + squadron = Squadron.create_from( + squadron_def, selected_base, self.coalition, self.game + ) + + # Add Squadron + if not self.type_list.item_model.findItems(selected_type.name): + self.type_list.add_aircraft_type(selected_type) + # TODO Select the newly added type + self.squadrons_panel.add_squadron_to_panel(squadron) + self.update() + + def apply(self) -> None: + self.squadrons_panel.apply() class AirWingConfigurationDialog(QDialog): @@ -414,22 +451,13 @@ class AirWingConfigurationDialog(QDialog): layout = QVBoxLayout() self.setLayout(layout) - doc_url = ( - "https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots" - ) doc_label = QLabel( "Use this opportunity to customize the squadrons available to your " "coalition. This is your only opportunity to make changes." "

" - "To accept your changes and continue, close this window.
" - "
" - "To remove a squadron from the game, uncheck the box in the title. New " - "squadrons cannot be added via the UI at this time. To add a custom " - "squadron,
" - f'see the wiki.' + "To accept your changes and continue, close this window." ) - doc_label.setOpenExternalLinks(True) layout.addWidget(doc_label) tab_widget = QTabWidget() @@ -437,10 +465,113 @@ class AirWingConfigurationDialog(QDialog): self.tabs = [] for coalition in game.coalitions: - coalition_tab = AirWingConfigurationTab(coalition, game.theater, game) + coalition_tab = AirWingConfigurationTab(coalition, 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() + + +class SquadronAircraftTypeSelector(QComboBox): + def __init__( + self, types: list[AircraftType], selected_aircraft: Optional[str] + ) -> None: + super().__init__() + self.setSizeAdjustPolicy(self.AdjustToContents) + + for type in sorted(types, key=lambda type: type.name): + self.addItem(type.name, type) + + if selected_aircraft: + self.setCurrentText(selected_aircraft) + + +class SquadronDefSelector(QComboBox): + def __init__( + self, + squadron_defs: dict[AircraftType, list[SquadronDef]], + aircraft: Optional[AircraftType], + ) -> None: + super().__init__() + self.setSizeAdjustPolicy(self.AdjustToContents) + self.squadron_defs = squadron_defs + self.set_aircraft_type(aircraft) + + def set_aircraft_type(self, aircraft: Optional[AircraftType]): + self.clear() + self.addItem("None (Random)", None) + if aircraft and aircraft in self.squadron_defs: + for squadron_def in sorted( + self.squadron_defs[aircraft], key=lambda squadron_def: squadron_def.name + ): + if not squadron_def.claimed: + squadron_name = squadron_def.name + if squadron_def.nickname: + squadron_name += " (" + squadron_def.nickname + ")" + self.addItem(squadron_name, squadron_def) + self.setCurrentText("None (Random)") + + +class SquadronConfigPopup(QDialog): + def __init__( + self, + selected_aircraft: Optional[str], + types: list[AircraftType], + bases: list[ControlPoint], + squadron_defs: dict[AircraftType, list[SquadronDef]], + ) -> None: + super().__init__() + + self.setWindowTitle(f"Add new Squadron") + + self.column = QVBoxLayout() + self.setLayout(self.column) + + self.bases = bases + + self.column.addWidget(QLabel("Aircraft:")) + self.aircraft_type_selector = SquadronAircraftTypeSelector( + types, selected_aircraft + ) + self.aircraft_type_selector.currentIndexChanged.connect( + self.on_aircraft_selection + ) + self.column.addWidget(self.aircraft_type_selector) + + self.column.addWidget(QLabel("Base:")) + self.squadron_base_selector = SquadronBaseSelector( + bases, None, self.aircraft_type_selector.currentData() + ) + self.column.addWidget(self.squadron_base_selector) + + self.column.addWidget(QLabel("Preset:")) + self.squadron_def_selector = SquadronDefSelector( + squadron_defs, self.aircraft_type_selector.currentData() + ) + self.column.addWidget(self.squadron_def_selector) + + self.column.addStretch() + + 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.button_layout.addWidget(self.accept_button) + + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(lambda state: self.reject()) + self.button_layout.addWidget(self.cancel_button) + + def on_aircraft_selection(self) -> None: + self.squadron_base_selector.set_aircraft_type( + self.aircraft_type_selector.currentData() + ) + self.squadron_def_selector.set_aircraft_type( + self.aircraft_type_selector.currentData() + ) + self.update() diff --git a/qt_ui/windows/SquadronConfigPopup.py b/qt_ui/windows/SquadronConfigPopup.py deleted file mode 100644 index f90c75d0..00000000 --- a/qt_ui/windows/SquadronConfigPopup.py +++ /dev/null @@ -1,145 +0,0 @@ -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() diff --git a/resources/ui/misc/dark/reload.png b/resources/ui/misc/dark/reload.png new file mode 100644 index 00000000..8d42d671 Binary files /dev/null and b/resources/ui/misc/dark/reload.png differ diff --git a/resources/ui/misc/light/reload.png b/resources/ui/misc/light/reload.png new file mode 100644 index 00000000..f5a4cf73 Binary files /dev/null and b/resources/ui/misc/light/reload.png differ diff --git a/resources/ui/misc/medium/reload.png b/resources/ui/misc/medium/reload.png new file mode 100644 index 00000000..8cbb23fd Binary files /dev/null and b/resources/ui/misc/medium/reload.png differ