From 2d8c8c63c91d7e66da5e08312ce8f7f90d5c1718 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 9 Oct 2020 16:18:08 -0700 Subject: [PATCH] Improve flight path display options. Adds an option show only selected flight, and also changes the show all option to highlight the selected flight. --- qt_ui/displayoptions.py | 27 +++++++++-- qt_ui/widgets/ato.py | 22 +++++++++ qt_ui/widgets/map/QLiberationMap.py | 74 ++++++++++++++++++++++------- qt_ui/windows/GameUpdateSignal.py | 12 ++++- qt_ui/windows/QLiberationWindow.py | 47 ++++++++++++------ 5 files changed, 145 insertions(+), 37 deletions(-) diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py index a9b6320c..f6f2e6c6 100644 --- a/qt_ui/displayoptions.py +++ b/qt_ui/displayoptions.py @@ -1,6 +1,6 @@ """Visibility options for the game map.""" from dataclasses import dataclass -from typing import Iterator +from typing import Iterator, Optional, Union @dataclass @@ -27,17 +27,38 @@ class DisplayRule: return self.value +class DisplayGroup: + def __init__(self, name: Optional[str]) -> None: + self.name = name + + def __iter__(self) -> Iterator[DisplayRule]: + # Python 3.6 enforces that __dict__ is order preserving by default. + for value in self.__dict__.values(): + if isinstance(value, DisplayRule): + yield value + + +class FlightPathOptions(DisplayGroup): + def __init__(self) -> None: + super().__init__("Flight Paths") + self.hide = DisplayRule("Hide Flight Paths", True) + self.only_selected = DisplayRule("Show Selected Flight Path", False) + self.all = DisplayRule("Show All Flight Paths", False) + + class DisplayOptions: ground_objects = DisplayRule("Ground Objects", True) control_points = DisplayRule("Control Points", True) lines = DisplayRule("Lines", True) events = DisplayRule("Events", True) sam_ranges = DisplayRule("SAM Ranges", True) - flight_paths = DisplayRule("Flight Paths", False) + flight_paths = FlightPathOptions() @classmethod - def menu_items(cls) -> Iterator[DisplayRule]: + def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]: # Python 3.6 enforces that __dict__ is order preserving by default. for value in cls.__dict__.values(): if isinstance(value, DisplayRule): yield value + elif isinstance(value, DisplayGroup): + yield value diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index b39f3063..8703f7e9 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -109,6 +109,7 @@ class QFlightPanel(QGroupBox): """Sets the package model to display.""" self.package_model = model self.flight_list.set_package(model) + self.selection_changed.connect(self.on_selection_changed) self.on_selection_changed() @property @@ -122,6 +123,15 @@ class QFlightPanel(QGroupBox): enabled = index.isValid() self.edit_button.setEnabled(enabled) self.delete_button.setEnabled(enabled) + self.change_map_flight_selection(index) + + @staticmethod + def change_map_flight_selection(index: QModelIndex) -> None: + if not index.isValid(): + GameUpdateSignal.get_instance().select_flight(None) + return + + GameUpdateSignal.get_instance().select_flight(index.row()) def on_edit(self) -> None: """Opens the flight edit dialog.""" @@ -270,6 +280,18 @@ class QPackagePanel(QGroupBox): enabled = index.isValid() self.edit_button.setEnabled(enabled) self.delete_button.setEnabled(enabled) + self.change_map_package_selection(index) + + def change_map_package_selection(self, index: QModelIndex) -> None: + if not index.isValid(): + GameUpdateSignal.get_instance().select_package(None) + return + + package = self.ato_model.get_package_model(index) + if package.rowCount() == 0: + GameUpdateSignal.get_instance().select_package(None) + else: + GameUpdateSignal.get_instance().select_package(index.row()) def on_edit(self) -> None: """Opens the package edit dialog.""" diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 1f3fdd57..227e6c5a 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -42,6 +42,8 @@ class QLiberationMap(QGraphicsView): self.game: Optional[Game] = game_model.game self.flight_path_items: List[QGraphicsItem] = [] + # A tuple of (package index, flight index), or none. + self.selected_flight: Optional[Tuple[int, int]] = None self.setMinimumSize(800,600) self.setMaximumHeight(2160) @@ -56,6 +58,25 @@ class QLiberationMap(QGraphicsView): lambda: self.draw_flight_plans(self.scene()) ) + def update_package_selection(index: Optional[int]) -> None: + self.selected_flight = index, 0 + self.draw_flight_plans(self.scene()) + + GameUpdateSignal.get_instance().package_selection_changed.connect( + update_package_selection + ) + + def update_flight_selection(index: Optional[int]) -> None: + if self.selected_flight is None: + logging.error("Flight was selected with no package selected") + return + self.selected_flight = self.selected_flight[0], index + self.draw_flight_plans(self.scene()) + + GameUpdateSignal.get_instance().flight_selection_changed.connect( + update_flight_selection + ) + def init_scene(self): scene = QLiberationScene(self) self.setScene(scene) @@ -198,37 +219,44 @@ class QLiberationMap(QGraphicsView): # Something may have caused those items to already be removed. pass self.flight_path_items.clear() - if not DisplayOptions.flight_paths: + if DisplayOptions.flight_paths.hide: return - for package in self.game_model.ato_model.packages: - for flight in package.flights: - self.draw_flight_plan(scene, flight) + for p_idx, package in enumerate(self.game_model.ato_model.packages): + for f_idx, flight in enumerate(package.flights): + selected = (p_idx, f_idx) == self.selected_flight + if DisplayOptions.flight_paths.only_selected and not selected: + continue + highlight = selected and DisplayOptions.flight_paths.all + self.draw_flight_plan(scene, flight, highlight) - def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None: + def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight, + highlight: bool) -> None: is_player = flight.from_cp.captured pos = self._transform_point(flight.from_cp.position) - self.draw_waypoint(scene, pos, is_player) + self.draw_waypoint(scene, pos, is_player, highlight) prev_pos = tuple(pos) for point in flight.points: new_pos = self._transform_point(Point(point.x, point.y)) - self.draw_flight_path(scene, prev_pos, new_pos, is_player) - self.draw_waypoint(scene, new_pos, is_player) + self.draw_flight_path(scene, prev_pos, new_pos, is_player, + highlight) + self.draw_waypoint(scene, new_pos, is_player, highlight) prev_pos = tuple(new_pos) - self.draw_flight_path(scene, prev_pos, pos, is_player) + self.draw_flight_path(scene, prev_pos, pos, is_player, highlight) def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int], - player: bool) -> None: - waypoint_pen = self.waypoint_pen(player) - waypoint_brush = self.waypoint_brush(player) + player: bool, highlight: bool) -> None: + waypoint_pen = self.waypoint_pen(player, highlight) + waypoint_brush = self.waypoint_brush(player, highlight) self.flight_path_items.append(scene.addEllipse( position[0], position[1], self.WAYPOINT_SIZE, self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush )) def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], - pos1: Tuple[int, int], player: bool): - flight_path_pen = self.flight_path_pen(player) + pos1: Tuple[int, int], player: bool, + highlight: bool) -> None: + flight_path_pen = self.flight_path_pen(player, highlight) # Draw the line to the *middle* of the waypoint. offset = self.WAYPOINT_SIZE // 2 self.flight_path_items.append(scene.addLine( @@ -317,21 +345,31 @@ class QLiberationMap(QGraphicsView): return X > treshold and X or treshold, Y > treshold and Y or treshold + def highlight_color(self, transparent: Optional[bool] = False) -> QColor: + return QColor(255, 255, 0, 20 if transparent else 255) + def base_faction_color_name(self, player: bool) -> str: if player: return self.game.get_player_color() else: return self.game.get_enemy_color() - def waypoint_pen(self, player: bool) -> QPen: + def waypoint_pen(self, player: bool, highlight: bool) -> QColor: + if highlight: + return self.highlight_color() name = self.base_faction_color_name(player) - return QPen(brush=CONST.COLORS[name]) + return CONST.COLORS[name] - def waypoint_brush(self, player: bool) -> QColor: + def waypoint_brush(self, player: bool, highlight: bool) -> QColor: + if highlight: + return self.highlight_color(transparent=True) name = self.base_faction_color_name(player) return CONST.COLORS[f"{name}_transparent"] - def flight_path_pen(self, player: bool) -> QPen: + def flight_path_pen(self, player: bool, highlight: bool) -> QPen: + if highlight: + return self.highlight_color() + name = self.base_faction_color_name(player) color = CONST.COLORS[name] pen = QPen(brush=color) diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py index 8a52d555..3c112952 100644 --- a/qt_ui/windows/GameUpdateSignal.py +++ b/qt_ui/windows/GameUpdateSignal.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Tuple from PySide2.QtCore import QObject, Signal @@ -24,11 +24,21 @@ class GameUpdateSignal(QObject): debriefingReceived = Signal(DebriefingSignal) flight_paths_changed = Signal() + package_selection_changed = Signal(int) # Optional[int] + flight_selection_changed = Signal(int) # Optional[int] def __init__(self): super(GameUpdateSignal, self).__init__() GameUpdateSignal.instance = self + def select_package(self, index: Optional[int]) -> None: + # noinspection PyUnresolvedReferences + self.package_selection_changed.emit(index) + + def select_flight(self, index: Optional[int]) -> None: + # noinspection PyUnresolvedReferences + self.flight_selection_changed.emit(index) + def redraw_flight_paths(self) -> None: # noinspection PyUnresolvedReferences self.flight_paths_changed.emit() diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index acda1bb0..7e203b98 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,16 +1,16 @@ import logging import sys import webbrowser -from typing import Optional +from typing import Optional, Union from PySide2.QtCore import Qt from PySide2.QtGui import QIcon from PySide2.QtWidgets import ( QAction, - QDesktopWidget, + QActionGroup, QDesktopWidget, QFileDialog, QMainWindow, - QMessageBox, + QMenu, QMessageBox, QSplitter, QVBoxLayout, QWidget, @@ -19,7 +19,7 @@ from PySide2.QtWidgets import ( import qt_ui.uiconstants as CONST from game import Game, persistency from qt_ui.dialogs import Dialog -from qt_ui.displayoptions import DisplayOptions +from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule from qt_ui.models import GameModel from qt_ui.uiconstants import URLS from qt_ui.widgets.QTopPanel import QTopPanel @@ -139,17 +139,19 @@ class QLiberationWindow(QMainWindow): displayMenu = self.menu.addMenu("&Display") - for display_rule in DisplayOptions.menu_items(): - def make_check_closure(): - def closure(): - display_rule.value = action.isChecked() - return closure - - action = QAction(f"&{display_rule.menu_text}", displayMenu) - action.setCheckable(True) - action.setChecked(display_rule.value) - action.toggled.connect(make_check_closure()) - displayMenu.addAction(action) + last_was_group = True + for item in DisplayOptions.menu_items(): + if isinstance(item, DisplayRule): + displayMenu.addAction(self.make_display_rule_action(item)) + last_was_group = False + elif isinstance(item, DisplayGroup): + if not last_was_group: + displayMenu.addSeparator() + group = QActionGroup(displayMenu) + for display_rule in item: + displayMenu.addAction( + self.make_display_rule_action(display_rule, group)) + last_was_group = True help_menu = self.menu.addMenu("&Help") help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) @@ -162,6 +164,21 @@ class QLiberationWindow(QMainWindow): help_menu.addSeparator() help_menu.addAction(self.showAboutDialogAction) + @staticmethod + def make_display_rule_action( + display_rule, group: Optional[QActionGroup] = None) -> QAction: + def make_check_closure(): + def closure(): + display_rule.value = action.isChecked() + + return closure + + action = QAction(f"&{display_rule.menu_text}", group) + action.setCheckable(True) + action.setChecked(display_rule.value) + action.toggled.connect(make_check_closure()) + return action + def newGame(self): wizard = NewGameWizard(self) wizard.show()