diff --git a/changelog.md b/changelog.md index a44aa261..5bb763c4 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ Saves from 8.x are not compatible with 9.0.0. * **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate. * **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation. * **[UI]** Added cheats for instantly repairing and destroying runways. +* **[UI]** Improved usability of the flight properties UI. It now shows human-readable names and uses more appropriate UI elements. ## Fixes @@ -18,6 +19,7 @@ Saves from 8.x are not compatible with 9.0.0. * **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used. * **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes. * **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint. +* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes. # 8.1.0 diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 1b87e42d..979794c3 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -10,10 +10,10 @@ from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type import yaml from dcs.helicopters import helicopter_map from dcs.planes import plane_map +from dcs.unitpropertydescription import UnitPropertyDescription from dcs.unittype import FlyingType from game.data.units import UnitClass -from game.dcs.unitproperty import UnitProperty from game.dcs.unittype import UnitType from game.radio.channels import ( ApacheChannelNamer, @@ -322,8 +322,8 @@ class AircraftType(UnitType[Type[FlyingType]]): def channel_name(self, radio_id: int, channel_id: int) -> str: return self.channel_namer.channel_name(radio_id, channel_id) - def iter_props(self) -> Iterator[UnitProperty[Any]]: - return UnitProperty.for_aircraft(self.dcs_unit_type) + def iter_props(self) -> Iterator[UnitPropertyDescription]: + yield from self.dcs_unit_type.properties.values() def capable_of(self, task: FlightType) -> bool: return task in self.task_priorities diff --git a/qt_ui/windows/mission/flight/payload/missingpropertydataerror.py b/qt_ui/windows/mission/flight/payload/missingpropertydataerror.py new file mode 100644 index 00000000..d46f307f --- /dev/null +++ b/qt_ui/windows/mission/flight/payload/missingpropertydataerror.py @@ -0,0 +1,2 @@ +class MissingPropertyDataError(RuntimeError): + ... diff --git a/qt_ui/windows/mission/flight/payload/propertycheckbox.py b/qt_ui/windows/mission/flight/payload/propertycheckbox.py new file mode 100644 index 00000000..5f2623e3 --- /dev/null +++ b/qt_ui/windows/mission/flight/payload/propertycheckbox.py @@ -0,0 +1,21 @@ +from PySide6.QtWidgets import QCheckBox +from dcs.unitpropertydescription import UnitPropertyDescription + +from game.ato import Flight +from .missingpropertydataerror import MissingPropertyDataError + + +class PropertyCheckBox(QCheckBox): + def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: + super().__init__() + self.flight = flight + self.prop = prop + + if prop.default is None: + raise MissingPropertyDataError("default cannot be None") + + self.setChecked(self.flight.props.get(self.prop.identifier, self.prop.default)) + self.toggled.connect(self.on_toggle) + + def on_toggle(self, checked: bool) -> None: + self.flight.props[self.prop.identifier] = checked diff --git a/qt_ui/windows/mission/flight/payload/propertycombobox.py b/qt_ui/windows/mission/flight/payload/propertycombobox.py new file mode 100644 index 00000000..4780b881 --- /dev/null +++ b/qt_ui/windows/mission/flight/payload/propertycombobox.py @@ -0,0 +1,29 @@ +from PySide6.QtWidgets import QComboBox +from dcs.unitpropertydescription import UnitPropertyDescription + +from game.ato import Flight +from .missingpropertydataerror import MissingPropertyDataError + + +class PropertyComboBox(QComboBox): + def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: + super().__init__() + self.flight = flight + self.prop = prop + + if prop.values is None: + raise MissingPropertyDataError("values cannot be None") + if prop.default is None: + raise MissingPropertyDataError("default cannot be None") + + current_value = self.flight.props.get(self.prop.identifier, self.prop.default) + + for ident, text in self.prop.values.items(): + self.addItem(text, ident) + if ident == current_value: + self.setCurrentText(text) + + self.currentIndexChanged.connect(self.on_selection_changed) + + def on_selection_changed(self, _index: int) -> None: + self.flight.props[self.prop.identifier] = self.currentData() diff --git a/qt_ui/windows/mission/flight/payload/propertyeditor.py b/qt_ui/windows/mission/flight/payload/propertyeditor.py index 68982bde..8b5ce30f 100644 --- a/qt_ui/windows/mission/flight/payload/propertyeditor.py +++ b/qt_ui/windows/mission/flight/payload/propertyeditor.py @@ -1,7 +1,18 @@ -from PySide6.QtWidgets import QGridLayout, QLabel +import logging + +from PySide6.QtWidgets import QGridLayout, QLabel, QWidget +from dcs.unitpropertydescription import UnitPropertyDescription from game.ato import Flight -from .propertyselector import PropertySelector +from .missingpropertydataerror import MissingPropertyDataError +from .propertycheckbox import PropertyCheckBox +from .propertycombobox import PropertyComboBox +from .propertyspinbox import PropertySpinBox + + +class UnhandledControlTypeError(RuntimeError): + def __init__(self, control: str) -> None: + super().__init__(f"Unhandled control type {control}") class PropertyEditor(QGridLayout): @@ -10,5 +21,47 @@ class PropertyEditor(QGridLayout): self.flight = flight for row, prop in enumerate(flight.unit_type.iter_props()): - self.addWidget(QLabel(prop.id), row, 0) - self.addWidget(PropertySelector(self.flight, prop), row, 1) + if prop.label is None: + if prop.control != "label": + logging.error( + "Found non-label aircraft property with no display name." + ) + continue + + if prop.player_only and not flight.client_count: + continue + + try: + widget = self.control_for_property(prop) + except (MissingPropertyDataError, UnhandledControlTypeError): + logging.exception( + f"Cannot create property control for property %s of %s", + prop.identifier, + flight.unit_type, + ) + continue + + label = prop.label + if widget is None: + label = f"{label}" + self.addWidget(QLabel(label), row, 0) + + # If prop.control is "label", widget will be None. We only need to add the + # label, not the control. + if widget is not None: + self.addWidget(widget, row, 1) + + def control_for_property(self, prop: UnitPropertyDescription) -> QWidget | None: + # Valid values are: + # "checkbox", "comboList", "groupbox", "label", "slider", "spinbox" + match prop.control: + case "checkbox": + return PropertyCheckBox(self.flight, prop) + case "comboList": + return PropertyComboBox(self.flight, prop) + case "groupbox" | "label": + return None + case "slider" | "spinbox": + return PropertySpinBox(self.flight, prop) + case _: + raise UnhandledControlTypeError(prop.control) diff --git a/qt_ui/windows/mission/flight/payload/propertyselector.py b/qt_ui/windows/mission/flight/payload/propertyselector.py deleted file mode 100644 index 8b1ac499..00000000 --- a/qt_ui/windows/mission/flight/payload/propertyselector.py +++ /dev/null @@ -1,23 +0,0 @@ -from PySide6.QtWidgets import QComboBox - -from game.ato import Flight -from game.dcs.unitproperty import UnitProperty - - -class PropertySelector(QComboBox): - def __init__(self, flight: Flight, prop: UnitProperty) -> None: - super().__init__() - self.flight = flight - self.prop = prop - - current_value = self.flight.props.get(self.prop.id, self.prop.default) - - for value in self.prop.values: - self.addItem(value.id, value.value) - if value.value == current_value: - self.setCurrentText(value.id) - - self.currentIndexChanged.connect(self.on_selection_changed) - - def on_selection_changed(self, _index: int) -> None: - self.flight.props[self.prop.id] = self.currentData() diff --git a/qt_ui/windows/mission/flight/payload/propertyspinbox.py b/qt_ui/windows/mission/flight/payload/propertyspinbox.py new file mode 100644 index 00000000..71a0e41f --- /dev/null +++ b/qt_ui/windows/mission/flight/payload/propertyspinbox.py @@ -0,0 +1,28 @@ +from PySide6.QtWidgets import QSpinBox +from dcs.unitpropertydescription import UnitPropertyDescription + +from game.ato import Flight +from .missingpropertydataerror import MissingPropertyDataError + + +class PropertySpinBox(QSpinBox): + def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: + super().__init__() + self.flight = flight + self.prop = prop + + if prop.minimum is None: + raise MissingPropertyDataError("minimum cannot be None") + if prop.maximum is None: + raise MissingPropertyDataError("maximum cannot be None") + if prop.default is None: + raise MissingPropertyDataError("default cannot be None") + + self.setMinimum(prop.minimum) + self.setMaximum(prop.maximum) + self.setValue(self.flight.props.get(self.prop.identifier, self.prop.default)) + + self.valueChanged.connect(self.on_value_changed) + + def on_value_changed(self, value: int) -> None: + self.flight.props[self.prop.identifier] = value diff --git a/requirements.txt b/requirements.txt index 9cc41e5b..94eb0994 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ platformdirs==2.6.2 pluggy==1.0.0 pre-commit==2.21.0 pydantic==1.10.7 -git+https://github.com/pydcs/dcs@e006f0df6db933fa34b2d5cb04db41653537503e#egg=pydcs +git+https://github.com/pydcs/dcs@45ce9c50d888c1f905ab8ade69e59c1baba35444#egg=pydcs pyinstaller==5.12.0 pyinstaller-hooks-contrib==2022.14 pyproj==3.4.1