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.
This commit is contained in:
Dan Albert 2023-07-19 21:42:10 -07:00
parent 0a82c2b3d1
commit f7d5db7f1e
9 changed files with 143 additions and 31 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
class MissingPropertyDataError(RuntimeError):
...

View File

@ -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

View File

@ -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()

View File

@ -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"<strong>{label}</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)

View File

@ -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()

View File

@ -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

View File

@ -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