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