import logging from typing import Optional from PySide2.QtCore import Signal, QModelIndex from PySide2.QtWidgets import ( QLabel, QGroupBox, QSpinBox, QGridLayout, QComboBox, QHBoxLayout, QCheckBox, ) from game import Game from game.squadrons import Pilot from gen.flights.flight import Flight from qt_ui.models import PackageModel class PilotSelector(QComboBox): available_pilots_changed = Signal() def __init__(self, flight: Flight, idx: int) -> None: super().__init__() self.flight = flight self.pilot_index = idx self.rebuild(initial_build=True) @staticmethod def text_for(pilot: Pilot) -> str: return pilot.name def _do_rebuild(self) -> None: self.clear() if self.pilot_index >= self.flight.count: self.addItem("No aircraft", None) self.setDisabled(True) return self.setEnabled(True) self.addItem("Unassigned", None) choices = list(self.flight.squadron.available_pilots) current_pilot = self.flight.pilots[self.pilot_index] if current_pilot is not None: choices.append(current_pilot) # Put players first, otherwise alphabetically. for pilot in sorted(choices, key=lambda p: (not p.player, p.name)): self.addItem(self.text_for(pilot), pilot) if current_pilot is None: self.setCurrentText("Unassigned") return self.setCurrentText(self.text_for(current_pilot)) self.currentIndexChanged.connect(self.replace_pilot) def rebuild(self, initial_build: bool = False) -> None: current_selection = self.currentData() # The contents of the selector depend on the selection of the other selectors # for the flight, so changing the selection of one causes each selector to # rebuild. A rebuild causes a selection change, so if we don't block signals # during a rebuild we'll never stop rebuilding. Block signals during the rebuild # and emit signals if anything actually changes afterwards. self.blockSignals(True) try: self._do_rebuild() finally: self.blockSignals(False) new_selection = self.currentData() if not initial_build and current_selection != new_selection: self.currentIndexChanged.emit(self.currentIndex()) self.currentTextChanged.emit(self.currentText()) def replace_pilot(self, index: QModelIndex) -> None: if self.itemText(index) == "No aircraft": # The roster resize is handled separately, so we have no pilots to remove. return pilot = self.itemData(index) if pilot == self.flight.pilots[self.pilot_index]: return self.flight.set_pilot(self.pilot_index, pilot) self.available_pilots_changed.emit() class PilotControls(QHBoxLayout): def __init__(self, flight: Flight, idx: int) -> None: super().__init__() self.flight = flight self.pilot_index = idx self.selector = PilotSelector(flight, idx) self.addWidget(self.selector) self.player_checkbox = QCheckBox() self.player_checkbox.setToolTip("Checked if this pilot is a player.") self.addWidget(self.player_checkbox) self.reset() self.player_checkbox.toggled.connect(self.toggle_player_state) @property def pilot(self) -> Optional[Pilot]: if self.pilot_index >= self.flight.count: return None return self.flight.pilots[self.pilot_index] def toggle_player_state(self, checked: bool) -> None: pilot = self.pilot if pilot is None: logging.error("Cannot toggle state of a pilot when none is selected") return pilot.player = checked def reset(self, initial: bool = False) -> None: if not initial: self.selector.rebuild() self.blockSignals(True) try: pilot = self.pilot if pilot is None: self.player_checkbox.setChecked(False) self.player_checkbox.setEnabled(False) return self.player_checkbox.setEnabled(True) self.player_checkbox.setChecked(self.pilot.player) finally: self.blockSignals(False) class QFlightSlotEditor(QGroupBox): def __init__(self, package_model: PackageModel, flight: Flight, game: Game): super().__init__("Slots") self.package_model = package_model self.flight = flight self.game = game self.inventory = self.game.aircraft_inventory.for_control_point(flight.from_cp) available = self.inventory.available(self.flight.unit_type) max_count = self.flight.count + available if max_count > 4: max_count = 4 layout = QGridLayout() self.aircraft_count = QLabel("Aircraft count:") self.aircraft_count_spinner = QSpinBox() self.aircraft_count_spinner.setMinimum(1) self.aircraft_count_spinner.setMaximum(max_count) self.aircraft_count_spinner.setValue(flight.count) self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count) layout.addWidget(self.aircraft_count, 0, 0) layout.addWidget(self.aircraft_count_spinner, 0, 1) layout.addWidget(QLabel("Squadron:"), 1, 0) layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1) layout.addWidget(QLabel("Assigned pilots:"), 2, 0) self.pilot_controls = [] for pilot_idx, row in enumerate(range(2, 6)): controls = PilotControls(self.flight, pilot_idx) controls.selector.available_pilots_changed.connect( self.reset_pilot_selectors ) self.pilot_controls.append(controls) layout.addLayout(controls, row, 1) self.setLayout(layout) def reset_pilot_selectors(self) -> None: for controls in self.pilot_controls: controls.reset() def _changed_aircraft_count(self): self.game.aircraft_inventory.return_from_flight(self.flight) new_count = int(self.aircraft_count_spinner.value()) try: self.game.aircraft_inventory.claim_for_flight(self.flight) except ValueError: # The UI should have prevented this, but if we ran out of aircraft # then roll back the inventory change. difference = new_count - self.flight.count available = self.inventory.available(self.flight.unit_type) logging.error( f"Could not add {difference} additional aircraft to " f"{self.flight} because {self.flight.departure} has only " f"{available} {self.flight.unit_type} remaining" ) self.game.aircraft_inventory.claim_for_flight(self.flight) return self.flight.resize(new_count) self.reset_pilot_selectors()