From bce6a170b82005c03d3d9d82689d95d45aaa56cb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 19 Jul 2023 21:42:10 -0700 Subject: [PATCH] Improve UI for flight properties. Use the new data from pydcs to improve the properties UI: * Use human readable names * Use appropriate control types * Limit min and max values as appropriate for each property * Show labels Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3090. --- changelog.md | 3 + game/dcs/aircrafttype.py | 6 +- .../payload/missingpropertydataerror.py | 2 + .../flight/payload/propertycheckbox.py | 21 +++++++ .../flight/payload/propertycombobox.py | 29 +++++++++ .../mission/flight/payload/propertyeditor.py | 59 ++++++++++++++++++- .../mission/flight/payload/propertyspinbox.py | 28 +++++++++ requirements.txt | 2 +- 8 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 qt_ui/windows/mission/flight/payload/missingpropertydataerror.py create mode 100644 qt_ui/windows/mission/flight/payload/propertycheckbox.py create mode 100644 qt_ui/windows/mission/flight/payload/propertycombobox.py create mode 100644 qt_ui/windows/mission/flight/payload/propertyspinbox.py diff --git a/changelog.md b/changelog.md index c64c40ce..cbf34e14 100644 --- a/changelog.md +++ b/changelog.md @@ -212,6 +212,7 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions * **[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 @@ -221,6 +222,8 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions * **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to. * **[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 d441f0d9..e31582f4 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -11,11 +11,11 @@ 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 dcs.weapons_data import weapon_ids from game.data.units import UnitClass -from game.dcs.unitproperty import UnitProperty from game.dcs.unittype import UnitType from game.persistency import user_custom_weapon_injections_dir from game.radio.channels import ( @@ -329,8 +329,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 0b3beed9..d63a21d2 100644 --- a/qt_ui/windows/mission/flight/payload/propertyeditor.py +++ b/qt_ui/windows/mission/flight/payload/propertyeditor.py @@ -1,7 +1,18 @@ +import logging + from PySide2.QtWidgets import QGridLayout, QLabel +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/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 9ee3ea24..cb50a295 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ pluggy==1.2.0 pre-commit==3.3.3 pydantic==2.0.3 pydantic-settings==2.0.2 --e git+https://github.com/dcs-retribution/pydcs@b2617f0fbeacd2e3b6066383ea9fd15a155eacb2#egg=pydcs +-e git+https://github.com/dcs-retribution/pydcs@e0c28dd6d344e5d95066ec9b73e19ebbf34679c8#egg=pydcs pyinstaller==5.13.0 pyinstaller-hooks-contrib==2023.5 pyparsing==3.1.0