From 1fa18447e17c3aa128b9a27a9f156faa91cdf513 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 9 Oct 2020 18:23:43 -0700 Subject: [PATCH] Show player slots in the overview. --- qt_ui/dialogs.py | 2 +- qt_ui/models.py | 6 ++ qt_ui/widgets/QLabeledWidget.py | 11 ++- qt_ui/widgets/QTopPanel.py | 31 +++--- qt_ui/widgets/ato.py | 108 ++++++++++++++++++++- qt_ui/widgets/clientslots.py | 28 ++++++ qt_ui/windows/QLiberationWindow.py | 2 +- qt_ui/windows/mission/QEditFlightDialog.py | 12 +-- 8 files changed, 177 insertions(+), 23 deletions(-) create mode 100644 qt_ui/widgets/clientslots.py diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py index e09dd92a..f88aa4b0 100644 --- a/qt_ui/dialogs.py +++ b/qt_ui/dialogs.py @@ -58,7 +58,7 @@ class Dialog: flight: Flight) -> None: """Opens the dialog to edit the given flight.""" cls.edit_flight_dialog = QEditFlightDialog( - cls.game_model.game, + cls.game_model, package_model.package, flight ) diff --git a/qt_ui/models.py b/qt_ui/models.py index 98515eab..428b4598 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -95,6 +95,8 @@ class NullListModel(QAbstractListModel): class PackageModel(QAbstractListModel): """The model for an ATO package.""" + FlightRole = Qt.UserRole + #: Emitted when this package is being deleted from the ATO. deleted = Signal() @@ -113,6 +115,8 @@ class PackageModel(QAbstractListModel): return self.text_for_flight(flight) if role == Qt.DecorationRole: return self.icon_for_flight(flight) + elif role == PackageModel.FlightRole: + return flight return None @staticmethod @@ -185,6 +189,8 @@ class AtoModel(QAbstractListModel): PackageRole = Qt.UserRole + client_slots_changed = Signal() + def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None: super().__init__() self.game = game diff --git a/qt_ui/widgets/QLabeledWidget.py b/qt_ui/widgets/QLabeledWidget.py index 88459896..f258f458 100644 --- a/qt_ui/widgets/QLabeledWidget.py +++ b/qt_ui/widgets/QLabeledWidget.py @@ -1,4 +1,6 @@ """A layout containing a widget with an associated label.""" +from typing import Optional + from PySide2.QtCore import Qt from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget @@ -10,8 +12,13 @@ class QLabeledWidget(QHBoxLayout): label is used to name the input. """ - def __init__(self, text: str, widget: QWidget) -> None: + def __init__(self, text: str, widget: QWidget, + tooltip: Optional[str]) -> None: super().__init__() - self.addWidget(QLabel(text)) + label = QLabel(text) + self.addWidget(label) self.addStretch() self.addWidget(widget, alignment=Qt.AlignRight) + if tooltip is not None: + label.setToolTip(tooltip) + widget.setToolTip(tooltip) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index f75826ad..dabaecd7 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -11,9 +11,11 @@ from PySide2.QtWidgets import ( import qt_ui.uiconstants as CONST from game import Game from game.event import CAP, CAS, FrontlineAttackEvent +from qt_ui.models import GameModel from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QFactionsInfos import QFactionsInfos from qt_ui.widgets.QTurnCounter import QTurnCounter +from qt_ui.widgets.clientslots import MaxPlayerCount from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.QWaitingForMissionResultWindow import \ QWaitingForMissionResultWindow @@ -23,14 +25,18 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow class QTopPanel(QFrame): - def __init__(self, game: Game): + def __init__(self, game_model: GameModel): super(QTopPanel, self).__init__() - self.game = game + self.game_model = game_model self.setMaximumHeight(70) self.init_ui() GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update) + @property + def game(self) -> Optional[Game]: + return self.game_model.game + def init_ui(self): self.turnCounter = QTurnCounter() @@ -68,6 +74,8 @@ class QTopPanel(QFrame): self.proceedBox = QGroupBox("Proceed") self.proceedBoxLayout = QHBoxLayout() + self.proceedBoxLayout.addLayout( + MaxPlayerCount(self.game_model.ato_model)) self.proceedBoxLayout.addWidget(self.passTurnButton) self.proceedBoxLayout.addWidget(self.proceedButton) self.proceedBox.setLayout(self.proceedBoxLayout) @@ -84,16 +92,17 @@ class QTopPanel(QFrame): self.setLayout(self.layout) def setGame(self, game: Optional[Game]): - self.game = game - if game is not None: - self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day) - self.budgetBox.setGame(self.game) - self.factionsInfos.setGame(self.game) + if game is None: + return - if self.game and self.game.turn == 0: - self.proceedButton.setEnabled(False) - else: - self.proceedButton.setEnabled(True) + self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day) + self.budgetBox.setGame(self.game) + self.factionsInfos.setGame(self.game) + + if self.game and self.game.turn == 0: + self.proceedButton.setEnabled(False) + else: + self.proceedButton.setEnabled(True) def openSettings(self): self.subwindow = QSettingsWindow(self.game) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index 8703f7e9..c978c1bc 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -10,7 +10,7 @@ from PySide2.QtCore import ( QSize, Qt, ) -from PySide2.QtGui import QFont, QFontMetrics, QPainter +from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter from PySide2.QtWidgets import ( QAbstractItemView, QGroupBox, @@ -18,15 +18,109 @@ from PySide2.QtWidgets import ( QListView, QPushButton, QSplitter, - QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout, + QStyle, QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout, ) +from game import db from gen.ato import Package from gen.flights.flight import Flight from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from ..models import AtoModel, GameModel, NullListModel, PackageModel +class FlightDelegate(QStyledItemDelegate): + FONT_SIZE = 10 + HMARGIN = 4 + VMARGIN = 4 + + 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: + flight = self.flight(index) + task = flight.flight_type.name + count = flight.count + name = db.unit_type_name(flight.unit_type) + delay = flight.scheduled_in + return f"[{task}] {count} x {name} in {delay} minutes" + + def second_row_text(self, index: QModelIndex) -> str: + flight = self.flight(index) + origin = flight.from_cp.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)) + + clients = self.num_clients(index) + if clients: + painter.drawText(rect, Qt.AlignRight, + f"Player Slots: {clients}") + + 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.""" @@ -34,6 +128,7 @@ class QFlightList(QListView): super().__init__() self.package_model = model self.set_package(model) + self.setItemDelegate(FlightDelegate()) self.setIconSize(QSize(91, 24)) self.setSelectionBehavior(QAbstractItemView.SelectItems) @@ -206,6 +301,15 @@ class PackageDelegate(QStyledItemDelegate): 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) + if clients: + painter.drawText(rect, Qt.AlignRight, + f"Player Slots: {clients}") + + 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)) diff --git a/qt_ui/widgets/clientslots.py b/qt_ui/widgets/clientslots.py new file mode 100644 index 00000000..1c9fff9b --- /dev/null +++ b/qt_ui/widgets/clientslots.py @@ -0,0 +1,28 @@ +"""Widgets for displaying client slots.""" +from PySide2.QtWidgets import QLabel + +from qt_ui.models import AtoModel +from qt_ui.widgets.QLabeledWidget import QLabeledWidget + + +class MaxPlayerCount(QLabeledWidget): + def __init__(self, ato_model: AtoModel) -> None: + self.ato_model = ato_model + self.slots_label = QLabel(str(self.count_client_slots)) + self.ato_model.client_slots_changed.connect(self.update_count) + super().__init__( + "Max Players:", self.slots_label, + ("Total number of client slots. To add client slots, edit a flight " + "using the panel on the left.") + ) + + @property + def count_client_slots(self) -> int: + slots = 0 + for package in self.ato_model.packages: + for flight in package.flights: + slots += flight.client_count + return slots + + def update_count(self) -> None: + self.slots_label.setText(str(self.count_client_slots)) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 7e203b98..db5caa10 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -77,7 +77,7 @@ class QLiberationWindow(QMainWindow): vbox = QVBoxLayout() vbox.setMargin(0) - vbox.addWidget(QTopPanel(self.game)) + vbox.addWidget(QTopPanel(self.game_model)) vbox.addWidget(hbox) central_widget = QWidget() diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py index 9f795b79..bfbcc5cb 100644 --- a/qt_ui/windows/mission/QEditFlightDialog.py +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -4,9 +4,9 @@ from PySide2.QtWidgets import ( QVBoxLayout, ) -from game import Game from gen.ato import Package from gen.flights.flight import Flight +from qt_ui.models import GameModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner @@ -15,22 +15,22 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner class QEditFlightDialog(QDialog): """Dialog window for editing flight plans and loadouts.""" - def __init__(self, game: Game, package: Package, flight: Flight) -> None: + def __init__(self, game_model: GameModel, package: Package, flight: Flight) -> None: super().__init__() - self.game = game + self.game_model = game_model self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) layout = QVBoxLayout() - self.flight_planner = QFlightPlanner(package, flight, game) + self.flight_planner = QFlightPlanner(package, flight, game_model.game) layout.addWidget(self.flight_planner) self.setLayout(layout) self.finished.connect(self.on_close) - @staticmethod - def on_close(_result) -> None: + def on_close(self, _result) -> None: GameUpdateSignal.get_instance().redraw_flight_paths() + self.game_model.ato_model.client_slots_changed.emit()