From 9c2bad85d59a5efd23b87bba60e59e3e9e3ddd06 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 26 May 2021 15:53:41 -0700 Subject: [PATCH] Show number of missing pilots in the UI. https://github.com/dcs-liberation/dcs_liberation/issues/276 --- gen/flights/flight.py | 4 + qt_ui/delegate_helpers.py | 13 -- qt_ui/delegates.py | 122 ++++++++++++++++ qt_ui/widgets/ato.py | 177 +++++------------------- qt_ui/windows/AirWingDialog.py | 2 +- qt_ui/windows/PendingTransfersDialog.py | 2 +- qt_ui/windows/SquadronDialog.py | 2 +- 7 files changed, 166 insertions(+), 156 deletions(-) delete mode 100644 qt_ui/delegate_helpers.py create mode 100644 qt_ui/delegates.py diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 0ab7c7f4..e8b4c83d 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -280,6 +280,10 @@ class Flight: self.squadron.return_pilot(current_pilot) self.pilots[index] = pilot + @property + def missing_pilots(self) -> int: + return len([p for p in self.pilots if p is None]) + def __repr__(self): name = db.unit_type_name(self.unit_type) if self.custom_name: diff --git a/qt_ui/delegate_helpers.py b/qt_ui/delegate_helpers.py deleted file mode 100644 index 0c437310..00000000 --- a/qt_ui/delegate_helpers.py +++ /dev/null @@ -1,13 +0,0 @@ -from contextlib import contextmanager -from typing import ContextManager - -from PySide2.QtGui import QPainter - - -@contextmanager -def painter_context(painter: QPainter) -> ContextManager[None]: - try: - painter.save() - yield - finally: - painter.restore() diff --git a/qt_ui/delegates.py b/qt_ui/delegates.py new file mode 100644 index 00000000..050983f9 --- /dev/null +++ b/qt_ui/delegates.py @@ -0,0 +1,122 @@ +from contextlib import contextmanager +from typing import ContextManager, Optional + +from PySide2.QtCore import QModelIndex, Qt, QSize +from PySide2.QtGui import QPainter, QFont, QFontMetrics, QIcon +from PySide2.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem, QStyle + + +@contextmanager +def painter_context(painter: QPainter) -> ContextManager[None]: + try: + painter.save() + yield + finally: + painter.restore() + + +class TwoColumnRowDelegate(QStyledItemDelegate): + HMARGIN = 4 + VMARGIN = 4 + + def __init__(self, rows: int, columns: int, font_size: int = 12) -> None: + if columns not in (1, 2): + raise ValueError(f"Only one or two columns may be used, not {columns}") + super().__init__() + self.font_size = font_size + self.rows = rows + self.columns = columns + + def get_font(self, option: QStyleOptionViewItem) -> QFont: + font = QFont(option.font) + font.setPointSize(self.font_size) + return font + + def text_for(self, index: QModelIndex, row: int, column: int) -> str: + raise NotImplementedError + + def paint( + self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex + ) -> None: + # Draw the list item with all the default selection styling, but with an + # invalid index so text formatting is left to us. + super().paint(painter, option, QModelIndex()) + + rect = option.rect.adjusted( + self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN + ) + + with painter_context(painter): + painter.setFont(self.get_font(option)) + + icon: Optional[QIcon] = index.data(Qt.DecorationRole) + + if icon is not None: + icon.paint( + painter, + rect, + Qt.AlignLeft | Qt.AlignVCenter, + self.icon_mode(option), + self.icon_state(option), + ) + rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN, 0, 0, 0) + + row_height = rect.height() / self.rows + for row in range(self.rows): + y = row_height * row + location = rect.adjusted(0, y, 0, y) + painter.drawText(location, Qt.AlignLeft, self.text_for(index, row, 0)) + if self.columns == 2: + painter.drawText( + location, Qt.AlignRight, self.text_for(index, row, 1) + ) + + @staticmethod + def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode: + if not (option.state & QStyle.State_Enabled): + return QIcon.Disabled + elif option.state & QStyle.State_Selected: + return QIcon.Selected + elif option.state & QStyle.State_Active: + return QIcon.Active + return QIcon.Normal + + @staticmethod + def icon_state(option: QStyleOptionViewItem) -> QIcon.State: + return QIcon.On if option.state & QStyle.State_Open else QIcon.Off + + @staticmethod + def icon_size(option: QStyleOptionViewItem) -> QSize: + icon_size: Optional[QSize] = option.decorationSize + if icon_size is None: + return QSize(0, 0) + else: + return icon_size + + def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: + metrics = QFontMetrics(self.get_font(option)) + widths = [] + heights = [] + + icon_size = self.icon_size(option) + icon_width = 0 + icon_height = 0 + if icon_size.width(): + icon_width = icon_size.width() + self.HMARGIN + if icon_size.height(): + icon_height = icon_size.height() + self.VMARGIN + + for row in range(self.rows): + width = 0 + height = 0 + for column in range(self.columns): + size = metrics.size(0, self.text_for(index, row, column)) + width += size.width() + height = max(height, size.height()) + widths.append(width) + heights.append(height) + + return QSize( + icon_width + max(widths) + 2 * self.HMARGIN, + max(icon_height, sum(heights)) + 2 * self.VMARGIN, + ) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index 2305a25e..b7cdbdec 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -10,10 +10,6 @@ from PySide2.QtCore import ( ) from PySide2.QtGui import ( QContextMenuEvent, - QFont, - QFontMetrics, - QIcon, - QPainter, ) from PySide2.QtWidgets import ( QAbstractItemView, @@ -25,9 +21,6 @@ from PySide2.QtWidgets import ( QMenu, QPushButton, QSplitter, - QStyle, - QStyleOptionViewItem, - QStyledItemDelegate, QVBoxLayout, ) @@ -35,111 +28,42 @@ from gen.ato import Package from gen.flights.flight import Flight from gen.flights.traveltime import TotEstimator from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from ..delegate_helpers import painter_context +from ..delegates import TwoColumnRowDelegate from ..models import AtoModel, GameModel, NullListModel, PackageModel -class FlightDelegate(QStyledItemDelegate): - FONT_SIZE = 10 - HMARGIN = 4 - VMARGIN = 4 - +class FlightDelegate(TwoColumnRowDelegate): def __init__(self, package: Package) -> None: - super().__init__() + super().__init__(rows=2, columns=2, font_size=10) self.package = package - def get_font(self, option: QStyleOptionViewItem) -> QFont: - font = QFont(option.font) - font.setPointSize(self.FONT_SIZE) - return font - @staticmethod def flight(index: QModelIndex) -> Flight: return index.data(PackageModel.FlightRole) - def first_row_text(self, index: QModelIndex) -> str: + def text_for(self, index: QModelIndex, row: int, column: int) -> str: flight = self.flight(index) - estimator = TotEstimator(self.package) - delay = estimator.mission_start_time(flight) - return f"{flight} in {delay}" - - def second_row_text(self, index: QModelIndex) -> str: - flight = self.flight(index) - origin = flight.from_cp.name - if flight.arrival != flight.departure: - return f"From {origin} to {flight.arrival.name}" - return f"From {origin}" - - def paint( - self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex - ) -> None: - # Draw the list item with all the default selection styling, but with an - # invalid index so text formatting is left to us. - super().paint(painter, option, QModelIndex()) - - rect = option.rect.adjusted( - self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN - ) - - with painter_context(painter): - painter.setFont(self.get_font(option)) - - icon: Optional[QIcon] = index.data(Qt.DecorationRole) - if icon is not None: - icon.paint( - painter, - rect, - Qt.AlignLeft | Qt.AlignVCenter, - self.icon_mode(option), - self.icon_state(option), - ) - - rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN, 0, 0, 0) - painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index)) - line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2) - painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index)) - + if (row, column) == (0, 0): + estimator = TotEstimator(self.package) + delay = estimator.mission_start_time(flight) + return f"{flight} in {delay}" + elif (row, column) == (0, 1): clients = self.num_clients(index) - if clients: - painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}") + return f"Player Slots: {clients}" if clients else "" + elif (row, column) == (1, 0): + origin = flight.from_cp.name + if flight.arrival != flight.departure: + return f"From {origin} to {flight.arrival.name}" + return f"From {origin}" + elif (row, column) == (1, 1): + missing_pilots = flight.missing_pilots + return f"Missing pilots: {flight.missing_pilots}" if missing_pilots else "" + return "" def num_clients(self, index: QModelIndex) -> int: flight = self.flight(index) return flight.client_count - @staticmethod - def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode: - if not (option.state & QStyle.State_Enabled): - return QIcon.Disabled - elif option.state & QStyle.State_Selected: - return QIcon.Selected - elif option.state & QStyle.State_Active: - return QIcon.Active - return QIcon.Normal - - @staticmethod - def icon_state(option: QStyleOptionViewItem) -> QIcon.State: - return QIcon.On if option.state & QStyle.State_Open else QIcon.Off - - @staticmethod - def icon_size(option: QStyleOptionViewItem) -> QSize: - icon_size: Optional[QSize] = option.decorationSize - if icon_size is None: - return QSize(0, 0) - else: - return icon_size - - def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: - left = self.icon_size(option).width() + self.HMARGIN - metrics = QFontMetrics(self.get_font(option)) - first = metrics.size(0, self.first_row_text(index)) - second = metrics.size(0, self.second_row_text(index)) - text_width = max(first.width(), second.width()) - return QSize( - left + text_width + 2 * self.HMARGIN, - first.height() + second.height() + 2 * self.VMARGIN, - ) - class QFlightList(QListView): """List view for displaying the flights of a package.""" @@ -310,62 +234,35 @@ class QFlightPanel(QGroupBox): self.flight_list.delete_flight(index) -class PackageDelegate(QStyledItemDelegate): - FONT_SIZE = 12 - HMARGIN = 4 - VMARGIN = 4 - - def get_font(self, option: QStyleOptionViewItem) -> QFont: - font = QFont(option.font) - font.setPointSize(self.FONT_SIZE) - return font +class PackageDelegate(TwoColumnRowDelegate): + def __init__(self) -> None: + super().__init__(rows=2, columns=2) @staticmethod def package(index: QModelIndex) -> Package: return index.data(AtoModel.PackageRole) - def left_text(self, index: QModelIndex) -> str: + def text_for(self, index: QModelIndex, row: int, column: int) -> str: package = self.package(index) - return f"{package.package_description} {package.target.name}" - - def right_text(self, index: QModelIndex) -> str: - package = self.package(index) - return f"TOT T+{package.time_over_target}" - - def paint( - self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex - ) -> None: - # Draw the list item with all the default selection styling, but with an - # invalid index so text formatting is left to us. - super().paint(painter, option, QModelIndex()) - - rect = option.rect.adjusted( - self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN - ) - - with painter_context(painter): - painter.setFont(self.get_font(option)) - - painter.drawText(rect, Qt.AlignLeft, self.left_text(index)) - line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2) - painter.drawText(line2, Qt.AlignLeft, self.right_text(index)) - + if (row, column) == (0, 0): + return f"{package.package_description} {package.target.name}" + elif (row, column) == (0, 1): clients = self.num_clients(index) - if clients: - painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}") + return f"Player Slots: {clients}" if clients else "" + elif (row, column) == (1, 0): + return f"TOT T+{package.time_over_target}" + elif (row, column) == (1, 1): + unassigned_pilots = self.missing_pilots(index) + return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else "" + return "" def num_clients(self, index: QModelIndex) -> int: package = self.package(index) return sum(f.client_count for f in package.flights) - def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: - metrics = QFontMetrics(self.get_font(option)) - left = metrics.size(0, self.left_text(index)) - right = metrics.size(0, self.right_text(index)) - return QSize( - max(left.width(), right.width()) + 2 * self.HMARGIN, - left.height() + right.height() + 2 * self.VMARGIN, - ) + def missing_pilots(self, index: QModelIndex) -> int: + package = self.package(index) + return sum(f.missing_pilots for f in package.flights) class QPackageList(QListView): @@ -376,7 +273,7 @@ class QPackageList(QListView): self.ato_model = model self.setModel(model) self.setItemDelegate(PackageDelegate()) - self.setIconSize(QSize(91, 24)) + self.setIconSize(QSize(0, 0)) self.setSelectionBehavior(QAbstractItemView.SelectItems) self.model().rowsInserted.connect(self.on_new_packages) self.doubleClicked.connect(self.on_double_click) diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index 879b96f2..2fb25dbd 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -18,7 +18,7 @@ from PySide2.QtWidgets import ( ) from game.squadrons import Squadron -from qt_ui.delegate_helpers import painter_context +from qt_ui.delegates import painter_context from qt_ui.models import GameModel, AirWingModel, SquadronModel from qt_ui.windows.SquadronDialog import SquadronDialog diff --git a/qt_ui/windows/PendingTransfersDialog.py b/qt_ui/windows/PendingTransfersDialog.py index 9c75b45b..7242fa5b 100644 --- a/qt_ui/windows/PendingTransfersDialog.py +++ b/qt_ui/windows/PendingTransfersDialog.py @@ -23,7 +23,7 @@ from PySide2.QtWidgets import ( ) from game.transfers import TransferOrder -from qt_ui.delegate_helpers import painter_context +from qt_ui.delegates import painter_context from qt_ui.models import GameModel, TransferModel diff --git a/qt_ui/windows/SquadronDialog.py b/qt_ui/windows/SquadronDialog.py index 32def4ba..c1582c3d 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -18,7 +18,7 @@ from PySide2.QtWidgets import ( ) from game.squadrons import Pilot -from qt_ui.delegate_helpers import painter_context +from qt_ui.delegates import painter_context from qt_ui.models import SquadronModel