mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
355 lines
14 KiB
Python
355 lines
14 KiB
Python
"""Dialogs for creating and editing ATO packages."""
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from PySide6.QtCore import QItemSelection, QTime, Qt, Signal
|
|
from PySide6.QtWidgets import (
|
|
QCheckBox,
|
|
QDialog,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QTimeEdit,
|
|
QVBoxLayout,
|
|
QLineEdit,
|
|
)
|
|
|
|
from game.ato.flight import Flight
|
|
from game.ato.flightplans.planningerror import PlanningError
|
|
from game.ato.package import Package
|
|
from game.game import Game
|
|
from game.radio.radios import RadioFrequency
|
|
from game.server import EventStream
|
|
from game.sim import GameUpdateEvents
|
|
from game.theater.missiontarget import MissionTarget
|
|
from qt_ui.models import GameModel, PackageModel
|
|
from qt_ui.uiconstants import EVENT_ICONS
|
|
from qt_ui.widgets.QFrequencyWidget import QFrequencyWidget
|
|
from qt_ui.widgets.ato import QFlightList
|
|
from qt_ui.windows.QRadioFrequencyDialog import QRadioFrequencyDialog
|
|
from qt_ui.windows.mission.QAutoCreateDialog import QAutoCreateDialog
|
|
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
|
|
|
|
|
class QPackageDialog(QDialog):
|
|
"""Base package management dialog.
|
|
|
|
The dialogs for creating a new package and editing an existing dialog are
|
|
very similar, and this implements the shared behavior.
|
|
"""
|
|
|
|
#: Emitted when a change is made to the package.
|
|
package_changed = Signal()
|
|
|
|
def __init__(self, game_model: GameModel, model: PackageModel, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self.game_model = game_model
|
|
self.package_model = model
|
|
self.add_flight_dialog: Optional[QFlightCreator] = None
|
|
|
|
self.setMinimumSize(1000, 440)
|
|
self.setWindowTitle(
|
|
f"Mission Package: {self.package_model.mission_target.name}"
|
|
)
|
|
self.setWindowIcon(EVENT_ICONS["strike"])
|
|
|
|
self.layout = QVBoxLayout()
|
|
|
|
self.summary_row = QHBoxLayout()
|
|
self.layout.addLayout(self.summary_row)
|
|
|
|
self.package_type_column = QVBoxLayout()
|
|
self.summary_row.addLayout(self.package_type_column)
|
|
|
|
package_type_row = QHBoxLayout()
|
|
self.package_type_label = QLabel("Package Type:")
|
|
self.package_type_text = QLabel(self.package_model.description)
|
|
# noinspection PyUnresolvedReferences
|
|
self.package_changed.connect(self.on_package_changed)
|
|
package_type_row.addWidget(self.package_type_label)
|
|
package_type_row.addWidget(self.package_type_text)
|
|
self.package_type_column.addLayout(package_type_row)
|
|
|
|
self.summary_row.addStretch(1)
|
|
|
|
self.package_name_column = QHBoxLayout()
|
|
self.summary_row.addLayout(self.package_name_column)
|
|
self.package_name_label = QLabel("Package Name:")
|
|
self.package_name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.package_name_text = QLineEdit(self.package_model.package.custom_name)
|
|
self.package_name_text.textChanged.connect(self.on_change_name)
|
|
self.package_name_column.addWidget(self.package_name_label)
|
|
self.package_name_column.addWidget(self.package_name_text)
|
|
|
|
self.summary_row.addStretch(1)
|
|
|
|
self.tot_column = QHBoxLayout()
|
|
self.summary_row.addLayout(self.tot_column)
|
|
|
|
self.tot_label = QLabel("Time Over Target:")
|
|
self.tot_column.addWidget(self.tot_label)
|
|
|
|
self.tot_spinner = QTimeEdit(self.tot_qtime())
|
|
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
|
self.tot_spinner.setDisplayFormat("hh:mm:ss")
|
|
self.tot_spinner.timeChanged.connect(self.save_tot)
|
|
self.tot_spinner.setToolTip("Package TOT relative to mission TOT")
|
|
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
|
self.tot_column.addWidget(self.tot_spinner)
|
|
|
|
self.auto_asap = QCheckBox("ASAP")
|
|
self.auto_asap.setToolTip(
|
|
"Sets the package TOT to the earliest time that all flights can "
|
|
"arrive at the target."
|
|
)
|
|
self.auto_asap.setChecked(self.package_model.package.auto_asap)
|
|
self.auto_asap.toggled.connect(self.set_asap)
|
|
self.tot_column.addWidget(self.auto_asap)
|
|
|
|
self.tot_help_label = QLabel(
|
|
'<a href="https://github.com/dcs-retribution/dcs-retribution/wiki/Mission-planning"><span style="color:#FFFFFF;">Help</span></a>'
|
|
)
|
|
self.tot_help_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.tot_help_label.setOpenExternalLinks(True)
|
|
self.tot_column.addWidget(self.tot_help_label)
|
|
|
|
self.package_view = QFlightList(self.game_model, self.package_model)
|
|
self.package_view.selectionModel().selectionChanged.connect(
|
|
self.on_selection_changed
|
|
)
|
|
self.layout.addWidget(self.package_view)
|
|
|
|
self.button_layout = QHBoxLayout()
|
|
self.layout.addLayout(self.button_layout)
|
|
|
|
self.add_flight_button = QPushButton("Add Flight")
|
|
self.add_flight_button.clicked.connect(self.on_add_flight)
|
|
self.button_layout.addWidget(self.add_flight_button)
|
|
|
|
self.delete_flight_button = QPushButton("Delete Selected")
|
|
self.delete_flight_button.setProperty("style", "btn-danger")
|
|
self.delete_flight_button.clicked.connect(self.on_delete_flight)
|
|
self.delete_flight_button.setEnabled(model.rowCount() > 0)
|
|
self.button_layout.addWidget(self.delete_flight_button)
|
|
|
|
self.auto_create_button = QPushButton("Auto Create")
|
|
self.auto_create_button.setDisabled(len(list(self.package_model.flights)) > 0)
|
|
self.auto_create_button.clicked.connect(self.on_auto_create)
|
|
self.button_layout.addWidget(self.auto_create_button)
|
|
|
|
self.button_layout.addStretch()
|
|
|
|
self.freq_widget = QFrequencyWidget(self.package_model.package, game_model)
|
|
self.button_layout.addWidget(self.freq_widget)
|
|
|
|
self.button_layout.addStretch()
|
|
|
|
self.setLayout(self.layout)
|
|
|
|
self.package_model.tot_changed.connect(self.update_tot)
|
|
|
|
self.accepted.connect(self.on_save)
|
|
self.finished.connect(self.on_close)
|
|
self.rejected.connect(self.on_cancel)
|
|
|
|
@property
|
|
def game(self) -> Game:
|
|
return self.game_model.game
|
|
|
|
def tot_qtime(self) -> QTime:
|
|
tot = self.package_model.package.time_over_target
|
|
return QTime(tot.hour, tot.minute, tot.second)
|
|
|
|
def on_cancel(self) -> None:
|
|
pass
|
|
|
|
def on_close(self, _result) -> None:
|
|
EventStream.put_nowait(
|
|
GameUpdateEvents().update_flights_in_package(self.package_model.package)
|
|
)
|
|
|
|
def on_save(self) -> None:
|
|
self.save_tot()
|
|
|
|
def save_tot(self) -> None:
|
|
# TODO: This is going to break horribly around midnight.
|
|
time = self.tot_spinner.time()
|
|
self.package_model.set_tot(
|
|
self.package_model.package.time_over_target.replace(
|
|
hour=time.hour(), minute=time.minute(), second=time.second()
|
|
)
|
|
)
|
|
|
|
def set_asap(self, checked: bool) -> None:
|
|
self.package_model.set_asap(checked)
|
|
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
|
self.update_tot()
|
|
|
|
def update_tot(self) -> None:
|
|
self.tot_spinner.setTime(self.tot_qtime())
|
|
|
|
def on_selection_changed(
|
|
self, selected: QItemSelection, _deselected: QItemSelection
|
|
) -> None:
|
|
"""Updates the state of the delete button."""
|
|
self.delete_flight_button.setEnabled(not selected.empty())
|
|
|
|
def on_add_flight(self) -> None:
|
|
"""Opens the new flight dialog."""
|
|
self.add_flight_dialog = QFlightCreator(
|
|
self.game,
|
|
self.package_model.package,
|
|
is_ownfor=self.game_model.is_ownfor,
|
|
parent=self.window(),
|
|
)
|
|
self.add_flight_dialog.created.connect(self.add_flight)
|
|
self.add_flight_dialog.show()
|
|
|
|
def add_flight(self, flight: Flight) -> None:
|
|
"""Adds the new flight to the package."""
|
|
self.package_model.add_flight(flight)
|
|
try:
|
|
flight.recreate_flight_plan()
|
|
self.package_model.update_tot()
|
|
EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
|
|
except PlanningError as ex:
|
|
self.package_model.delete_flight(flight)
|
|
logging.exception("Could not create flight")
|
|
QMessageBox.critical(
|
|
self, "Could not create flight", str(ex), QMessageBox.StandardButton.Ok
|
|
)
|
|
self.auto_create_button.setDisabled(True)
|
|
# noinspection PyUnresolvedReferences
|
|
self.package_changed.emit()
|
|
|
|
def on_delete_flight(self) -> None:
|
|
"""Removes the selected flight from the package."""
|
|
flight = self.package_view.selected_item
|
|
if flight is None:
|
|
logging.error(f"Cannot delete flight when no flight is selected.")
|
|
return
|
|
self.package_model.cancel_or_abort_flight(flight)
|
|
if len(list(self.package_model.flights)) == 0:
|
|
self.auto_create_button.setDisabled(False)
|
|
# noinspection PyUnresolvedReferences
|
|
self.package_changed.emit()
|
|
|
|
def on_auto_create(self) -> None:
|
|
"""Opens the new flight dialog."""
|
|
auto_create_dialog = QAutoCreateDialog(
|
|
self.game,
|
|
self.package_model,
|
|
self.game_model.is_ownfor,
|
|
parent=self.window(),
|
|
)
|
|
if auto_create_dialog.exec_() == QDialog.DialogCode.Accepted:
|
|
for f in self.package_model.package.flights:
|
|
EventStream.put_nowait(GameUpdateEvents().new_flight(f))
|
|
self.package_model.update_tot()
|
|
self.package_changed.emit()
|
|
self.auto_create_button.setDisabled(True)
|
|
|
|
def on_change_name(self) -> None:
|
|
self.package_model.package.custom_name = self.package_name_text.text()
|
|
|
|
def on_open_radio(self) -> None:
|
|
self.package_frequency_dialog = QRadioFrequencyDialog(
|
|
parent=self.window(), container=self.package_model.package
|
|
)
|
|
self.package_frequency_dialog.accepted.connect(self.assign_frequency)
|
|
self.package_frequency_dialog.show()
|
|
|
|
def assign_frequency(self):
|
|
hz = round(self.package_frequency_dialog.frequency_input.value() * 10**6)
|
|
self.package_model.package.frequency = RadioFrequency(hertz=hz)
|
|
self.package_changed.emit()
|
|
|
|
def on_package_changed(self):
|
|
self.package_type_text.setText(self.package_model.description)
|
|
self.freq_widget.check_freq()
|
|
|
|
def on_reset_radio(self):
|
|
self.package_model.package.frequency = None
|
|
self.package_freq_text.setText("AUTO")
|
|
|
|
|
|
class QNewPackageDialog(QPackageDialog):
|
|
"""Dialog window for creating a new package.
|
|
|
|
New packages do not affect the ATO model until they are saved.
|
|
"""
|
|
|
|
def __init__(
|
|
self, game_model: GameModel, target: MissionTarget, parent=None
|
|
) -> None:
|
|
super().__init__(
|
|
game_model,
|
|
PackageModel(
|
|
Package(target, game_model.game.db.flights, auto_asap=True), game_model
|
|
),
|
|
parent=parent,
|
|
)
|
|
self.ato_model = (
|
|
game_model.ato_model if game_model.is_ownfor else game_model.red_ato_model
|
|
)
|
|
|
|
# In the *new* package dialog, a package has been created and may have aircraft
|
|
# assigned to it, but it is not a part of the ATO until the user saves it.
|
|
#
|
|
# Other actions (modifying settings, closing some other dialogs like the base
|
|
# menu) can cause a Game update which will forcibly close this window without
|
|
# either accepting or rejecting it, so we neither save the package nor release
|
|
# any allocated units.
|
|
#
|
|
# While it would be preferable to be able to update this dialog as needed in the
|
|
# event of game updates, the quick fix is to just not allow interaction with
|
|
# other UI elements until the new package has either been finalized or canceled.
|
|
self.setModal(True)
|
|
|
|
self.save_button = QPushButton("Save")
|
|
self.save_button.setProperty("style", "start-button")
|
|
self.save_button.clicked.connect(self.accept)
|
|
self.button_layout.addWidget(self.save_button)
|
|
|
|
def on_save(self) -> None:
|
|
"""Saves the created package.
|
|
|
|
Empty packages may be created. They can be modified later, and will have
|
|
no effect if empty when the mission is generated.
|
|
"""
|
|
super().on_save()
|
|
self.ato_model.add_package(self.package_model.package)
|
|
|
|
def on_cancel(self) -> None:
|
|
super().on_cancel()
|
|
for flight in list(self.package_model.package.flights):
|
|
self.package_model.cancel_or_abort_flight(flight)
|
|
|
|
|
|
class QEditPackageDialog(QPackageDialog):
|
|
"""Dialog window for editing an existing package.
|
|
|
|
Changes to existing packages occur immediately.
|
|
"""
|
|
|
|
def __init__(self, gm: GameModel, package: PackageModel) -> None:
|
|
super().__init__(gm, package)
|
|
self.ato_model = gm.ato_model if gm.is_ownfor else gm.red_ato_model
|
|
|
|
self.delete_button = QPushButton("Delete package")
|
|
self.delete_button.setProperty("style", "btn-danger")
|
|
self.delete_button.clicked.connect(self.on_delete)
|
|
self.button_layout.addWidget(self.delete_button)
|
|
|
|
self.done_button = QPushButton("Done")
|
|
self.done_button.setProperty("style", "start-button")
|
|
self.done_button.clicked.connect(self.accept)
|
|
self.button_layout.addWidget(self.done_button)
|
|
|
|
def on_delete(self) -> None:
|
|
"""Removes the viewed package from the ATO."""
|
|
# The ATO model returns inventory for us when deleting a package.
|
|
self.ato_model.cancel_or_abort_package(self.package_model.package)
|
|
self.close()
|