Show number of missing pilots in the UI.

https://github.com/dcs-liberation/dcs_liberation/issues/276
This commit is contained in:
Dan Albert 2021-05-26 15:53:41 -07:00
parent 4147d2f684
commit 9c2bad85d5
7 changed files with 166 additions and 156 deletions

View File

@ -280,6 +280,10 @@ class Flight:
self.squadron.return_pilot(current_pilot) self.squadron.return_pilot(current_pilot)
self.pilots[index] = 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): def __repr__(self):
name = db.unit_type_name(self.unit_type) name = db.unit_type_name(self.unit_type)
if self.custom_name: if self.custom_name:

View File

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

122
qt_ui/delegates.py Normal file
View File

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

View File

@ -10,10 +10,6 @@ from PySide2.QtCore import (
) )
from PySide2.QtGui import ( from PySide2.QtGui import (
QContextMenuEvent, QContextMenuEvent,
QFont,
QFontMetrics,
QIcon,
QPainter,
) )
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView, QAbstractItemView,
@ -25,9 +21,6 @@ from PySide2.QtWidgets import (
QMenu, QMenu,
QPushButton, QPushButton,
QSplitter, QSplitter,
QStyle,
QStyleOptionViewItem,
QStyledItemDelegate,
QVBoxLayout, QVBoxLayout,
) )
@ -35,111 +28,42 @@ from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from ..delegate_helpers import painter_context from ..delegates import TwoColumnRowDelegate
from ..models import AtoModel, GameModel, NullListModel, PackageModel from ..models import AtoModel, GameModel, NullListModel, PackageModel
class FlightDelegate(QStyledItemDelegate): class FlightDelegate(TwoColumnRowDelegate):
FONT_SIZE = 10
HMARGIN = 4
VMARGIN = 4
def __init__(self, package: Package) -> None: def __init__(self, package: Package) -> None:
super().__init__() super().__init__(rows=2, columns=2, font_size=10)
self.package = package self.package = package
def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font)
font.setPointSize(self.FONT_SIZE)
return font
@staticmethod @staticmethod
def flight(index: QModelIndex) -> Flight: def flight(index: QModelIndex) -> Flight:
return index.data(PackageModel.FlightRole) 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) flight = self.flight(index)
if (row, column) == (0, 0):
estimator = TotEstimator(self.package) estimator = TotEstimator(self.package)
delay = estimator.mission_start_time(flight) delay = estimator.mission_start_time(flight)
return f"{flight} in {delay}" return f"{flight} in {delay}"
elif (row, column) == (0, 1):
def second_row_text(self, index: QModelIndex) -> str: clients = self.num_clients(index)
flight = self.flight(index) return f"Player Slots: {clients}" if clients else ""
elif (row, column) == (1, 0):
origin = flight.from_cp.name origin = flight.from_cp.name
if flight.arrival != flight.departure: if flight.arrival != flight.departure:
return f"From {origin} to {flight.arrival.name}" return f"From {origin} to {flight.arrival.name}"
return f"From {origin}" return f"From {origin}"
elif (row, column) == (1, 1):
def paint( missing_pilots = flight.missing_pilots
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex return f"Missing pilots: {flight.missing_pilots}" if missing_pilots else ""
) -> None: return ""
# 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))
clients = self.num_clients(index)
if clients:
painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}")
def num_clients(self, index: QModelIndex) -> int: def num_clients(self, index: QModelIndex) -> int:
flight = self.flight(index) flight = self.flight(index)
return flight.client_count 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): class QFlightList(QListView):
"""List view for displaying the flights of a package.""" """List view for displaying the flights of a package."""
@ -310,62 +234,35 @@ class QFlightPanel(QGroupBox):
self.flight_list.delete_flight(index) self.flight_list.delete_flight(index)
class PackageDelegate(QStyledItemDelegate): class PackageDelegate(TwoColumnRowDelegate):
FONT_SIZE = 12 def __init__(self) -> None:
HMARGIN = 4 super().__init__(rows=2, columns=2)
VMARGIN = 4
def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font)
font.setPointSize(self.FONT_SIZE)
return font
@staticmethod @staticmethod
def package(index: QModelIndex) -> Package: def package(index: QModelIndex) -> Package:
return index.data(AtoModel.PackageRole) 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) package = self.package(index)
if (row, column) == (0, 0):
return f"{package.package_description} {package.target.name}" return f"{package.package_description} {package.target.name}"
elif (row, column) == (0, 1):
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))
clients = self.num_clients(index) clients = self.num_clients(index)
if clients: return f"Player Slots: {clients}" if clients else ""
painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}") 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: def num_clients(self, index: QModelIndex) -> int:
package = self.package(index) package = self.package(index)
return sum(f.client_count for f in package.flights) return sum(f.client_count for f in package.flights)
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: def missing_pilots(self, index: QModelIndex) -> int:
metrics = QFontMetrics(self.get_font(option)) package = self.package(index)
left = metrics.size(0, self.left_text(index)) return sum(f.missing_pilots for f in package.flights)
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,
)
class QPackageList(QListView): class QPackageList(QListView):
@ -376,7 +273,7 @@ class QPackageList(QListView):
self.ato_model = model self.ato_model = model
self.setModel(model) self.setModel(model)
self.setItemDelegate(PackageDelegate()) self.setItemDelegate(PackageDelegate())
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(0, 0))
self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.model().rowsInserted.connect(self.on_new_packages) self.model().rowsInserted.connect(self.on_new_packages)
self.doubleClicked.connect(self.on_double_click) self.doubleClicked.connect(self.on_double_click)

View File

@ -18,7 +18,7 @@ from PySide2.QtWidgets import (
) )
from game.squadrons import Squadron 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.models import GameModel, AirWingModel, SquadronModel
from qt_ui.windows.SquadronDialog import SquadronDialog from qt_ui.windows.SquadronDialog import SquadronDialog

View File

@ -23,7 +23,7 @@ from PySide2.QtWidgets import (
) )
from game.transfers import TransferOrder 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 from qt_ui.models import GameModel, TransferModel

View File

@ -18,7 +18,7 @@ from PySide2.QtWidgets import (
) )
from game.squadrons import Pilot 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 from qt_ui.models import SquadronModel