from collections import defaultdict from typing import Iterable, Iterator, Optional, Any import yaml from PySide6.QtCore import ( QItemSelection, QItemSelectionModel, QSize, Qt, Signal, ) from PySide6.QtGui import QIcon, QStandardItem, QStandardItemModel from PySide6.QtWidgets import ( QComboBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QListView, QScrollArea, QStackedLayout, QTabWidget, QTextEdit, QVBoxLayout, QWidget, QCheckBox, QPushButton, QGridLayout, QToolButton, QMessageBox, QSpinBox, QGroupBox, QFileDialog, ) from dcs.mapping import Point from game import Game from game.ato.flighttype import FlightType from game.campaignloader.campaignairwingconfig import ( DEFAULT_SQUADRON_SIZE, CampaignAirWingConfig, ) from game.coalition import Coalition from game.dcs.aircrafttype import AircraftType from game.persistency import airwing_dir from game.squadrons import AirWing, Pilot, Squadron from game.squadrons.squadrondef import SquadronDef from game.theater import ControlPoint, ParkingType, Airfield from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS from qt_ui.widgets.combos.QSquadronLiverySelector import SquadronLiverySelector from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector class QMissionType(QCheckBox): def __init__( self, mission_type: FlightType, allowed: bool, auto_assignable: bool ) -> None: super().__init__() self.flight_type = mission_type self.setEnabled(allowed) self.setChecked(auto_assignable) @property def auto_assignable(self) -> bool: return self.isChecked() class MissionTypeControls(QGridLayout): def __init__(self, squadron: Squadron) -> None: super().__init__() self.squadron = squadron self.mission_types: list[QMissionType] = [] self.addWidget(QLabel("Mission Type"), 0, 0) self.addWidget(QLabel("Auto-Assign"), 0, 1) for i, task in enumerate(FlightType): if task is FlightType.FERRY: # Not plannable so just skip it. continue auto_assignable = task in squadron.auto_assignable_mission_types mission_type = QMissionType( task, squadron.capable_of(task), auto_assignable ) self.mission_types.append(mission_type) self.addWidget(QLabel(task.value), i + 1, 0) self.addWidget(mission_type, i + 1, 1) @property def auto_assignable_mission_types(self) -> Iterator[FlightType]: for mission_type in self.mission_types: if mission_type.auto_assignable: yield mission_type.flight_type def replace_squadron(self, squadron: Squadron) -> None: self.squadron = squadron for mission_type in self.mission_types: mission_type.setChecked( mission_type.flight_type in self.squadron.auto_assignable_mission_types ) class SquadronBaseSelector(QComboBox): """A combo box for selecting a squadrons home air base. The combo box will automatically be populated with all air bases compatible with the squadron. """ def __init__( self, bases: Iterable[ControlPoint], selected_base: Optional[ControlPoint], aircraft_type: Optional[AircraftType], ) -> None: super().__init__() self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) self.bases = list(bases) self.set_aircraft_type(aircraft_type) 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) and not isinstance( base, Airfield ): 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 SquadronSizeSpinner(QSpinBox): def __init__(self, starting_size: int, parent: QWidget | None) -> None: super().__init__(parent) # Disable text editing, which wouldn't work in the first place, but also # obnoxiously selects the text on change (highlighting it) and leaves a flashing # cursor in the middle of the element when clicked. self.lineEdit().setEnabled(False) self.setMinimum(1) self.setValue(starting_size) # def sizeHint(self) -> QSize: # # The default size hinting fails to deal with label width, and will truncate # # "Paused". # size = super().sizeHint() # size.setWidth(86) # return size class AirWingConfigParkingTracker(QWidget): allocation_changed = Signal() def __init__(self, squadrons: Iterable[Squadron]) -> None: super().__init__() self.by_cp: dict[ControlPoint, set[Squadron]] = defaultdict(set) for squadron in squadrons: self.add_squadron(squadron) def add_squadron(self, squadron: Squadron) -> None: self.by_cp[squadron.location].add(squadron) self.signal_change() def remove_squadron(self, squadron: Squadron) -> None: self.by_cp[squadron.location].remove(squadron) self.signal_change() def relocate_squadron( self, squadron: Squadron, prior_location: ControlPoint, new_location: ControlPoint, ) -> None: self.by_cp[prior_location].remove(squadron) self.by_cp[new_location].add(squadron) squadron.relocate_to(new_location) self.signal_change() def used_parking_at(self, control_point: ControlPoint) -> int: return sum(s.max_size for s in self.by_cp[control_point]) def signal_change(self) -> None: self.allocation_changed.emit() class SquadronConfigurationBox(QGroupBox): remove_squadron_signal = Signal(Squadron) def __init__( self, game: Game, coalition: Coalition, squadron: Squadron, parking_tracker: AirWingConfigParkingTracker, aircraft_present: bool, ) -> None: super().__init__() self.game = game self.coalition = coalition self.squadron = squadron self.parking_tracker = parking_tracker self.aircraft_present = aircraft_present columns = QHBoxLayout() self.setLayout(columns) left_column = QVBoxLayout() columns.addLayout(left_column) left_column.addWidget(QLabel("Name:")) self.name_edit = QLineEdit(squadron.name) self.name_edit.textChanged.connect(lambda x: self.reset_title()) left_column.addWidget(self.name_edit) self.reset_title() 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) nickname_edit_layout.addWidget( self.nickname_edit, 1, 0, Qt.AlignmentFlag.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.AlignmentFlag.AlignTop ) left_column.addWidget(QLabel("Livery:")) self.livery_selector = SquadronLiverySelector(squadron) left_column.addWidget(self.livery_selector) task_and_size_row = QHBoxLayout() left_column.addLayout(task_and_size_row) size_column = QVBoxLayout() task_and_size_row.addLayout(size_column) size_column.addWidget(QLabel("Max size:")) self.max_size_selector = SquadronSizeSpinner(self.squadron.max_size, self) self.max_size_selector.valueChanged.connect(self.update_max_size) size_column.addWidget(self.max_size_selector) task_column = QVBoxLayout() task_and_size_row.addLayout(task_column) task_column.addWidget(QLabel("Primary task:")) self.primary_task_selector = PrimaryTaskSelector.for_squadron(self.squadron) task_column.addWidget(self.primary_task_selector) left_column.addWidget(QLabel("Base:")) self.base_selector = SquadronBaseSelector( game.theater.control_points_for(squadron.player), squadron.location, squadron.aircraft, ) self.base_selector.currentIndexChanged.connect(self.relocate_squadron) left_column.addWidget(self.base_selector) self.parking_label = QLabel() self.update_parking_label() self.parking_tracker.allocation_changed.connect(self.update_parking_label) left_column.addWidget(self.parking_label) if not squadron.player and squadron.aircraft.flyable: player_label = QLabel("Player slots not available for opfor") elif not squadron.aircraft.flyable: player_label = QLabel("Player slots not available for non-flyable aircraft") else: msg1 = "Player slots not available for opfor" msg2 = "Player slots not available for non-flyable aircraft" text = msg2 if squadron.player else msg1 player_label = QLabel(text) left_column.addWidget(player_label) self.player_list = QTextEdit( "
".join(p.name for p in self.claim_players_from_squadron()) ) self.player_list.setAcceptRichText(False) self.player_list.setEnabled(squadron.player and squadron.aircraft.flyable) left_column.addWidget(self.player_list) button_row = QHBoxLayout() left_column.addLayout(button_row) left_column.addStretch() delete_button = QPushButton("Remove Squadron") delete_button.setMaximumWidth(140) delete_button.clicked.connect(self.remove_from_squadron_config) button_row.addWidget(delete_button) replace_button = QPushButton("Replace with preset") replace_button.setMaximumWidth(140) replace_button.clicked.connect(self.replace_with_preset) button_row.addWidget(replace_button) button_row.addStretch() right_column = QVBoxLayout() self.mission_types = MissionTypeControls(squadron) right_column.addLayout(self.mission_types) right_column.addStretch() columns.addLayout(right_column) def bind_data(self) -> None: old_state = self.blockSignals(True) try: self.name_edit.setText(self.squadron.name) self.nickname_edit.setText(self.squadron.nickname) self.primary_task_selector.setCurrentText(self.squadron.primary_task.value) index = self.livery_selector.findText(self.squadron.livery) self.livery_selector.setCurrentIndex(index) self.max_size_selector.setValue(self.squadron.max_size) self.base_selector.setCurrentText(self.squadron.location.name) self.player_list.setText( "
".join(p.name for p in self.claim_players_from_squadron()) ) self.update_parking_label() finally: self.blockSignals(old_state) def update_parking_label(self) -> None: required_slots = self.parking_tracker.used_parking_at(self.squadron.location) required_slots_string = ( f"Parking slots required: {required_slots}
" if self.aircraft_present else "" ) total_slots = self.squadron.location.total_aircraft_parking( ParkingType().from_aircraft( self.squadron.aircraft, self.game.settings.ground_start_ai_planes ) ) slots = "N/A" if ap := self.squadron.location.dcs_airport: slots = len(ap.free_parking_slots(self.squadron.aircraft.dcs_unit_type)) self.parking_label.setText( f"{required_slots_string}" f"Total parking slots available: {total_slots}
" f"Number of parking slots for {self.squadron.aircraft.dcs_id} that fit: {slots}" ) def update_max_size(self) -> None: self.squadron.max_size = self.max_size_selector.value() self.parking_tracker.signal_change() def relocate_squadron(self) -> None: location = self.base_selector.currentData() self.parking_tracker.relocate_squadron( self.squadron, self.squadron.location, location ) def remove_from_squadron_config(self) -> None: self.remove_squadron_signal.emit(self.squadron) def pick_replacement_squadron(self) -> Optional[Squadron]: popup = PresetSquadronSelector( self.squadron.aircraft, self.coalition.air_wing.squadron_defs, ) if popup.exec_() != QDialog.DialogCode.Accepted: return None selected_def = popup.squadron_def_selector.currentData() self.squadron.coalition.air_wing.unclaim_squadron_def(self.squadron) squadron = Squadron.create_from( selected_def, self.squadron.primary_task, self.squadron.max_size, self.squadron.location, self.coalition, self.game, ) return squadron def claim_players_from_squadron(self) -> list[Pilot]: if not self.squadron.player: return [] players = [p for p in self.squadron.pilot_pool if p.player] for player in players: self.squadron.pilot_pool.remove(player) return players def return_players_to_squadron(self) -> None: if not self.squadron.player: return 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 def replace_with_preset(self) -> None: new_squadron = self.pick_replacement_squadron() if new_squadron is None: # The user canceled the dialog. return self.return_players_to_squadron() self.parking_tracker.remove_squadron(self.squadron) self.squadron = new_squadron self.parking_tracker.add_squadron(self.squadron) self.bind_data() self.mission_types.replace_squadron(self.squadron) self.parking_tracker.signal_change() def reset_title(self) -> None: self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}") def reroll_nickname(self) -> None: self.nickname_edit.setText( self.squadron.coalition.air_wing.squadron_def_generator.random_nickname() ) def apply(self) -> Squadron: self.squadron.name = self.name_edit.text() self.squadron.nickname = self.nickname_edit.text() self.squadron.max_size = self.max_size_selector.value() if (primary_task := self.primary_task_selector.selected_task) is not None: self.squadron.primary_task = primary_task else: raise RuntimeError("Primary task cannot be none") base = self.base_selector.currentData() if base is None: raise RuntimeError("Base cannot be none") self.squadron.assign_to_base(base) self.return_players_to_squadron() # Also update the auto assignable mission types self.squadron.set_auto_assignable_mission_types( set(self.mission_types.auto_assignable_mission_types) ) return self.squadron class SquadronConfigurationLayout(QVBoxLayout): config_changed = Signal(AircraftType) def __init__( self, game: Game, coalition: Coalition, squadrons: list[Squadron], parking_tracker: AirWingConfigParkingTracker, aircraft_present: bool, ) -> None: super().__init__() self.game = game self.coalition = coalition self.squadron_configs = [] self.parking_tracker = parking_tracker self.aircraft_present = aircraft_present for squadron in squadrons: self.add_squadron(squadron) def apply(self) -> list[Squadron]: keep_squadrons = [] for squadron_config in self.squadron_configs: keep_squadrons.append(squadron_config.apply()) return keep_squadrons def remove_squadron(self, squadron: Squadron) -> None: self.parking_tracker.remove_squadron(squadron) 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( self.game, self.coalition, squadron, self.parking_tracker, self.aircraft_present, ) squadron_config.remove_squadron_signal.connect(self.remove_squadron) self.squadron_configs.append(squadron_config) self.addWidget(squadron_config) self.parking_tracker.add_squadron(squadron) class AircraftSquadronsPage(QWidget): remove_squadron_page = Signal(AircraftType) def __init__( self, game: Game, coalition: Coalition, squadrons: list[Squadron], parking_tracker: AirWingConfigParkingTracker, aircraft_present: bool, ) -> None: super().__init__() layout = QVBoxLayout() self.setLayout(layout) self.squadrons_config = SquadronConfigurationLayout( game, coalition, squadrons, parking_tracker, aircraft_present ) self.squadrons_config.config_changed.connect(self.on_squadron_config_changed) scrolling_widget = QWidget() scrolling_widget.setLayout(self.squadrons_config) scrolling_area = QScrollArea() scrolling_area.setHorizontalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) scrolling_area.setWidgetResizable(True) scrolling_area.setWidget(scrolling_widget) layout.addWidget(scrolling_area) 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 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, game: Game, coalition: Coalition, parking_tracker: AirWingConfigParkingTracker, aircraft_present: bool, ) -> None: super().__init__() self.game = game self.coalition = coalition self.parking_tracker = parking_tracker self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} self.aircraft_present = aircraft_present for aircraft, squadrons in self.air_wing.squadrons.items(): self.new_page_for_type(aircraft, squadrons) @property def air_wing(self) -> AirWing: return self.coalition.air_wing 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( self.game, self.coalition, squadrons, self.parking_tracker, self.aircraft_present, ) 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() def revert(self) -> None: for _, page in self.squadrons_pages.items(): self.removeWidget(page) self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} for aircraft, squadrons in self.air_wing.squadrons.items(): self.new_page_for_type(aircraft, squadrons) self.update() class AircraftTypeList(QListView): page_index_changed = Signal(int) def __init__(self, air_wing: AirWing) -> None: super().__init__() self.air_wing = air_wing self.setIconSize(QSize(91, 24)) self.setMinimumWidth(300) self.item_model = QStandardItemModel(self) self.setModel(self.item_model) self.selectionModel().setCurrentIndex( self.item_model.index(0, 0), QItemSelectionModel.SelectionFlag.Select ) self.selectionModel().selectionChanged.connect(self.on_selection_changed) for aircraft in air_wing.squadrons: self.add_aircraft_type(aircraft) def remove_aircraft_type(self, aircraft: AircraftType): for item in self.item_model.findItems(aircraft.display_name): self.item_model.removeRow(item.row()) self.page_index_changed.emit(self.selectionModel().currentIndex().row()) def add_aircraft_type(self, aircraft: AircraftType): aircraft_item = QStandardItem(aircraft.display_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 ) -> None: indexes = selected.indexes() if len(indexes) > 1: raise RuntimeError("Aircraft list should not allow multi-selection") if not indexes: return self.page_index_changed.emit(indexes[0].row()) @staticmethod def icon_for(aircraft: AircraftType) -> Optional[QIcon]: # Replace slashes with underscores because slashes are not allowed in filenames name = aircraft.dcs_id.replace("/", "_") if name in AIRCRAFT_ICONS: return QIcon(AIRCRAFT_ICONS[name]) return None def revert(self) -> None: self.item_model.clear() for aircraft in self.air_wing.squadrons: self.add_aircraft_type(aircraft) class AirWingConfigurationTab(QWidget): def __init__( self, coalition: Coalition, game: Game, aircraft_present: bool ) -> None: super().__init__() layout = QGridLayout() self.setLayout(layout) self.game = game self.coalition = coalition self.parking_tracker = AirWingConfigParkingTracker( coalition.air_wing.iter_squadrons() ) self.type_list = AircraftTypeList(coalition.air_wing) layout.addWidget(self.type_list, 1, 1, 1, 2) add_button = QPushButton("Add Squadron") add_button.clicked.connect(lambda state: self.add_squadron()) layout.addWidget(add_button, 2, 1, 1, 1) self.squadrons_panel = AircraftSquadronsPanel( game, coalition, self.parking_tracker, aircraft_present ) 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 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() bases = list(self.game.theater.control_points_for(self.coalition.player)) # List of all Aircrafts possible to operate with the given bases possible_aircrafts = { aircraft for aircraft in self.coalition.faction.all_aircrafts if isinstance(aircraft, AircraftType) and any(base.can_operate(aircraft) for base in bases) } popup = SquadronConfigPopup( selected_aircraft, possible_aircrafts, bases, self.coalition.air_wing.squadron_defs, ) if popup.exec_() != QDialog.DialogCode.Accepted: return selected_type = popup.aircraft_type_selector.currentData() selected_base = popup.squadron_base_selector.currentData() selected_task = popup.primary_task_selector.selected_task 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_task, DEFAULT_SQUADRON_SIZE, selected_base, self.coalition, self.game, ) # Add Squadron if not self.type_list.item_model.findItems(selected_type.display_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() def revert(self) -> None: self.type_list.revert() self.squadrons_panel.revert() self.update() class AirWingConfigurationDialog(QDialog): """Dialog window for air wing configuration.""" def __init__(self, game: Game, aircraft_present: bool, parent) -> None: super().__init__(parent) self.setMinimumSize(1024, 768) self.setWindowTitle(f"Air Wing Configuration") # TODO: self.setWindowIcon() layout = QVBoxLayout() self.setLayout(layout) doc_label = QLabel( "Use this opportunity to customize the squadrons available to your " "coalition. You can make changes at a later stage if " 'the "Enable Air Wing adjustments" cheat option is enabled.' "

" "To accept your changes and continue, close this window." ) layout.addWidget(doc_label) self.tab_widget = QTabWidget() layout.addWidget(self.tab_widget) self.tabs = [] for coalition in game.coalitions: coalition_tab = AirWingConfigurationTab(coalition, game, aircraft_present) name = "Blue" if coalition.player else "Red" self.tab_widget.addTab(coalition_tab, name) self.tabs.append(coalition_tab) load_save_layout = QHBoxLayout() save_button = QPushButton("Save Config") save_button.setProperty("style", "btn-primary") save_button.clicked.connect(lambda state: self.save_config()) load_button = QPushButton("Load Config") load_button.setProperty("style", "btn-primary") load_button.clicked.connect(lambda state: self.load_config()) load_save_layout.addWidget(load_button) load_save_layout.addWidget(save_button) layout.addLayout(load_save_layout) buttons_layout = QHBoxLayout() apply_button = QPushButton("Accept Changes") apply_button.setProperty("style", "btn-accept") apply_button.clicked.connect(lambda state: self.accept()) discard_button = QPushButton("Reset Changes") discard_button.setProperty("style", "btn-danger") discard_button.clicked.connect(lambda state: self.revert()) buttons_layout.addWidget(discard_button) buttons_layout.addWidget(apply_button) layout.addLayout(buttons_layout) def save_config(self) -> None: result = QMessageBox.information( None, "Save Air Wing?", "Revert will not be possible after saving a different Air Wing.
" "Are you sure you want to continue?", QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No, ) if result == QMessageBox.StandardButton.No: return awd = airwing_dir() fd = QFileDialog( caption="Save Air Wing", directory=str(awd), filter="*.yaml;*.yml" ) fd.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) if fd.exec_(): for tab in self.tabs: tab.apply() airwing = self._build_air_wing() filename = fd.selectedFiles()[0] with open(filename, "w") as f: f.write(yaml.dump(airwing)) def _build_air_wing(self) -> dict: w = self.tab_widget.currentWidget() assert isinstance(w, AirWingConfigurationTab) squadrons = {} for ac, sqs in w.coalition.air_wing.squadrons.items(): for s in sqs: cp = s.location.at if isinstance(cp, Point): key = s.location.full_name else: key = cp.id name = ( s.name if s.name in [x.name for x in w.coalition.air_wing.squadron_defs[ac]] else s.aircraft.variant_id ) entry = { "primary": s.primary_task.value, "secondary": [ sec.value for sec in s.auto_assignable_mission_types if sec.value != s.primary_task.value ], "aircraft": [name], "size": s.max_size, } if squadrons.get(key): squadrons[key].append(entry) else: squadrons[key] = [entry] return squadrons def load_config(self) -> None: result = QMessageBox.information( None, "Load Air Wing?", "Revert will not be possible after loading a different Air Wing.
" "Are you sure you want to continue?", QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No, ) if result == QMessageBox.StandardButton.No: return awd = airwing_dir() fd = QFileDialog( caption="Load Air Wing", directory=str(awd), filter="*.yaml;*.yml" ) if fd.exec_(): filename = fd.selectedFiles()[0] with open(filename, "r") as f: airwing = yaml.safe_load(f) self._construct_air_wing_tab(airwing) def _construct_air_wing_tab(self, airwing: dict[str, Any]) -> None: w = self.tab_widget.currentWidget() assert isinstance(w, AirWingConfigurationTab) c = w.coalition c.air_wing.squadrons = defaultdict(list) config = CampaignAirWingConfig.from_campaign_data(airwing, c.game.theater) c.configure_default_air_wing(config) w.revert() if c.game.turn != 0: c.initialize_turn(False) def revert(self) -> None: for tab in self.tabs: tab.revert() def accept(self) -> None: for tab in self.tabs: tab.apply() if tab.coalition.game.turn != 0: tab.coalition.initialize_turn(False) super().accept() def reject(self) -> None: result = QMessageBox.information( None, "Discard changes?", "Are you sure you want to discard your changes?", QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No, ) if result == QMessageBox.StandardButton.No: return super().reject() class SquadronAircraftTypeSelector(QComboBox): def __init__( self, types: set[AircraftType], selected_aircraft: Optional[str] ) -> None: super().__init__() self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) for type in sorted(types, key=lambda type: type.display_name): self.addItem(type.display_name, type) if selected_aircraft: self.setCurrentText(selected_aircraft) class SquadronDefSelector(QComboBox): def __init__( self, squadron_defs: dict[AircraftType, list[SquadronDef]], aircraft: Optional[AircraftType], allow_random: bool = True, ) -> None: super().__init__() self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) self.squadron_defs = squadron_defs self.allow_random = allow_random self.set_aircraft_type(aircraft) def set_aircraft_type(self, aircraft: Optional[AircraftType]): self.clear() if self.allow_random: 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.setCurrentIndex(0) class SquadronConfigPopup(QDialog): def __init__( self, selected_aircraft: Optional[str], types: set[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.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("Primary task:")) self.primary_task_selector = PrimaryTaskSelector( self.aircraft_type_selector.currentData() ) self.column.addWidget(self.primary_task_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.update_accept_button() 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 update_accept_button(self) -> None: enabled = ( self.aircraft_type_selector.currentData() is not None and self.squadron_base_selector.currentData() is not None and self.primary_task_selector.selected_task is not None ) self.accept_button.setEnabled(enabled) 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.primary_task_selector.set_aircraft( self.aircraft_type_selector.currentData() ) self.update_accept_button() self.update() class PresetSquadronSelector(QDialog): def __init__( self, aircraft: AircraftType, squadron_defs: dict[AircraftType, list[SquadronDef]], ) -> None: super().__init__() self.setWindowTitle(f"Choose preset squadron") self.column = QVBoxLayout() self.setLayout(self.column) self.column.addWidget(QLabel("Preset:")) self.squadron_def_selector = SquadronDefSelector( squadron_defs, aircraft, allow_random=False ) 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)