Improve flight path display options.

Adds an option show only selected flight, and also changes the show
all option to highlight the selected flight.
This commit is contained in:
Dan Albert 2020-10-09 16:18:08 -07:00
parent 31d5e3151b
commit 2d8c8c63c9
5 changed files with 145 additions and 37 deletions

View File

@ -1,6 +1,6 @@
"""Visibility options for the game map.""" """Visibility options for the game map."""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator from typing import Iterator, Optional, Union
@dataclass @dataclass
@ -27,17 +27,38 @@ class DisplayRule:
return self.value 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: class DisplayOptions:
ground_objects = DisplayRule("Ground Objects", True) ground_objects = DisplayRule("Ground Objects", True)
control_points = DisplayRule("Control Points", True) control_points = DisplayRule("Control Points", True)
lines = DisplayRule("Lines", True) lines = DisplayRule("Lines", True)
events = DisplayRule("Events", True) events = DisplayRule("Events", True)
sam_ranges = DisplayRule("SAM Ranges", True) sam_ranges = DisplayRule("SAM Ranges", True)
flight_paths = DisplayRule("Flight Paths", False) flight_paths = FlightPathOptions()
@classmethod @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. # Python 3.6 enforces that __dict__ is order preserving by default.
for value in cls.__dict__.values(): for value in cls.__dict__.values():
if isinstance(value, DisplayRule): if isinstance(value, DisplayRule):
yield value yield value
elif isinstance(value, DisplayGroup):
yield value

View File

@ -109,6 +109,7 @@ class QFlightPanel(QGroupBox):
"""Sets the package model to display.""" """Sets the package model to display."""
self.package_model = model self.package_model = model
self.flight_list.set_package(model) self.flight_list.set_package(model)
self.selection_changed.connect(self.on_selection_changed)
self.on_selection_changed() self.on_selection_changed()
@property @property
@ -122,6 +123,15 @@ class QFlightPanel(QGroupBox):
enabled = index.isValid() enabled = index.isValid()
self.edit_button.setEnabled(enabled) self.edit_button.setEnabled(enabled)
self.delete_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: def on_edit(self) -> None:
"""Opens the flight edit dialog.""" """Opens the flight edit dialog."""
@ -270,6 +280,18 @@ class QPackagePanel(QGroupBox):
enabled = index.isValid() enabled = index.isValid()
self.edit_button.setEnabled(enabled) self.edit_button.setEnabled(enabled)
self.delete_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: def on_edit(self) -> None:
"""Opens the package edit dialog.""" """Opens the package edit dialog."""

View File

@ -42,6 +42,8 @@ class QLiberationMap(QGraphicsView):
self.game: Optional[Game] = game_model.game self.game: Optional[Game] = game_model.game
self.flight_path_items: List[QGraphicsItem] = [] 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.setMinimumSize(800,600)
self.setMaximumHeight(2160) self.setMaximumHeight(2160)
@ -56,6 +58,25 @@ class QLiberationMap(QGraphicsView):
lambda: self.draw_flight_plans(self.scene()) 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): def init_scene(self):
scene = QLiberationScene(self) scene = QLiberationScene(self)
self.setScene(scene) self.setScene(scene)
@ -198,37 +219,44 @@ class QLiberationMap(QGraphicsView):
# Something may have caused those items to already be removed. # Something may have caused those items to already be removed.
pass pass
self.flight_path_items.clear() self.flight_path_items.clear()
if not DisplayOptions.flight_paths: if DisplayOptions.flight_paths.hide:
return return
for package in self.game_model.ato_model.packages: for p_idx, package in enumerate(self.game_model.ato_model.packages):
for flight in package.flights: for f_idx, flight in enumerate(package.flights):
self.draw_flight_plan(scene, flight) 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 is_player = flight.from_cp.captured
pos = self._transform_point(flight.from_cp.position) 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) prev_pos = tuple(pos)
for point in flight.points: for point in flight.points:
new_pos = self._transform_point(Point(point.x, point.y)) new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player) self.draw_flight_path(scene, prev_pos, new_pos, is_player,
self.draw_waypoint(scene, new_pos, is_player) highlight)
self.draw_waypoint(scene, new_pos, is_player, highlight)
prev_pos = tuple(new_pos) 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], def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
player: bool) -> None: player: bool, highlight: bool) -> None:
waypoint_pen = self.waypoint_pen(player) waypoint_pen = self.waypoint_pen(player, highlight)
waypoint_brush = self.waypoint_brush(player) waypoint_brush = self.waypoint_brush(player, highlight)
self.flight_path_items.append(scene.addEllipse( self.flight_path_items.append(scene.addEllipse(
position[0], position[1], self.WAYPOINT_SIZE, position[0], position[1], self.WAYPOINT_SIZE,
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush
)) ))
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
pos1: Tuple[int, int], player: bool): pos1: Tuple[int, int], player: bool,
flight_path_pen = self.flight_path_pen(player) highlight: bool) -> None:
flight_path_pen = self.flight_path_pen(player, highlight)
# Draw the line to the *middle* of the waypoint. # Draw the line to the *middle* of the waypoint.
offset = self.WAYPOINT_SIZE // 2 offset = self.WAYPOINT_SIZE // 2
self.flight_path_items.append(scene.addLine( 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 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: def base_faction_color_name(self, player: bool) -> str:
if player: if player:
return self.game.get_player_color() return self.game.get_player_color()
else: else:
return self.game.get_enemy_color() 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) 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) name = self.base_faction_color_name(player)
return CONST.COLORS[f"{name}_transparent"] 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) name = self.base_faction_color_name(player)
color = CONST.COLORS[name] color = CONST.COLORS[name]
pen = QPen(brush=color) pen = QPen(brush=color)

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Tuple
from PySide2.QtCore import QObject, Signal from PySide2.QtCore import QObject, Signal
@ -24,11 +24,21 @@ class GameUpdateSignal(QObject):
debriefingReceived = Signal(DebriefingSignal) debriefingReceived = Signal(DebriefingSignal)
flight_paths_changed = Signal() flight_paths_changed = Signal()
package_selection_changed = Signal(int) # Optional[int]
flight_selection_changed = Signal(int) # Optional[int]
def __init__(self): def __init__(self):
super(GameUpdateSignal, self).__init__() super(GameUpdateSignal, self).__init__()
GameUpdateSignal.instance = self 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: def redraw_flight_paths(self) -> None:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.flight_paths_changed.emit() self.flight_paths_changed.emit()

View File

@ -1,16 +1,16 @@
import logging import logging
import sys import sys
import webbrowser import webbrowser
from typing import Optional from typing import Optional, Union
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtGui import QIcon from PySide2.QtGui import QIcon
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAction, QAction,
QDesktopWidget, QActionGroup, QDesktopWidget,
QFileDialog, QFileDialog,
QMainWindow, QMainWindow,
QMessageBox, QMenu, QMessageBox,
QSplitter, QSplitter,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@ -19,7 +19,7 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game import Game, persistency from game import Game, persistency
from qt_ui.dialogs import Dialog 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.models import GameModel
from qt_ui.uiconstants import URLS from qt_ui.uiconstants import URLS
from qt_ui.widgets.QTopPanel import QTopPanel from qt_ui.widgets.QTopPanel import QTopPanel
@ -139,17 +139,19 @@ class QLiberationWindow(QMainWindow):
displayMenu = self.menu.addMenu("&Display") displayMenu = self.menu.addMenu("&Display")
for display_rule in DisplayOptions.menu_items(): last_was_group = True
def make_check_closure(): for item in DisplayOptions.menu_items():
def closure(): if isinstance(item, DisplayRule):
display_rule.value = action.isChecked() displayMenu.addAction(self.make_display_rule_action(item))
return closure last_was_group = False
elif isinstance(item, DisplayGroup):
action = QAction(f"&{display_rule.menu_text}", displayMenu) if not last_was_group:
action.setCheckable(True) displayMenu.addSeparator()
action.setChecked(display_rule.value) group = QActionGroup(displayMenu)
action.toggled.connect(make_check_closure()) for display_rule in item:
displayMenu.addAction(action) displayMenu.addAction(
self.make_display_rule_action(display_rule, group))
last_was_group = True
help_menu = self.menu.addMenu("&Help") help_menu = self.menu.addMenu("&Help")
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) 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.addSeparator()
help_menu.addAction(self.showAboutDialogAction) 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): def newGame(self):
wizard = NewGameWizard(self) wizard = NewGameWizard(self)
wizard.show() wizard.show()