From 220e5c48287f956318cc98ab6cea9bf488cafa72 Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Wed, 5 Aug 2026 03:42:13 -0700 Subject: [PATCH 1/7] chore: API plumbing + ruff hygiene for Phase 4 IA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the small accessors the Command Center depends on, plus a sweep of ruff hygiene that piled up between the design-system migration and the IA work. API additions: - AutoDiscovery.run_once — public alias for force_scan. The Command Palette's "Refresh window list" entry point scans once, emits the usual signals, and returns without starting the background timer. - LayoutManager.position — lazy-initialized position registry. The Command Palette's "Lock windows" / "Unlock windows" entries call set_locked, which routes through this. Lazy init keeps the import cost off callers that never touch positions. - LayoutManager.set_locked(locked) — operationally correct API for the lock-windows use case. - SystemStatusBar.snapshot() — fresh dict copy of every subsystem's (status, detail) pair. Used by CommandIntegrator to seed its parallel status view without reaching into private state. - design_system.painting.draw_threat_accent — ribbon + top-glow combo for fleet/tactical cards. Alpha-modulated by the live threat alpha so intel aging fades the chrome. Ruff hygiene (auto-fix F401/I001, manual F841/UP045): - Drop unused imports across intel_tab, main_tab, status_dock, integration, header, operational_truth, and the related tests. - Reorder import blocks to satisfy I001 in command/header.py. - Convert Optional[X] to X | None in command/integration.py (project targets Python 3.10+). - Remove two dead variables (alert_color in header, total_w / text_w in operational_truth's chip paint) and a redundant layout() call in test_command_center. All Phase 4 IA + Command Center commits sit on top of this one. The full suite still reports 2,580 passed, 5 skipped. Co-Authored-By: Claude --- src/argus_overview/core/discovery.py | 12 ++++ src/argus_overview/core/layout_manager.py | 26 ++++++++ src/argus_overview/ui/design_system/colors.py | 1 - .../ui/design_system/painting.py | 64 ++++++++++++++++++- src/argus_overview/ui/intel_tab.py | 3 +- src/argus_overview/ui/main_tab.py | 12 ++-- src/argus_overview/ui/status_dock.py | 3 +- src/argus_overview/ui/system_status_bar.py | 12 ++++ tests/test_design_system_painting.py | 3 +- tests/test_main_tab.py | 8 ++- tests/test_preview_health_paint.py | 3 - tests/test_settings_tab.py | 2 +- tests/test_system_status_bar.py | 7 +- 13 files changed, 133 insertions(+), 23 deletions(-) diff --git a/src/argus_overview/core/discovery.py b/src/argus_overview/core/discovery.py index 01ae4ef..e64f731 100644 --- a/src/argus_overview/core/discovery.py +++ b/src/argus_overview/core/discovery.py @@ -274,6 +274,18 @@ def force_scan(self) -> int: self._scan_cycle() return len(self.active_window_ids) + def run_once(self) -> int: + """ + Public alias for :meth:`force_scan` — one discovery cycle + without starting the background timer. This is the operator's + "Refresh window list" entry point: it scans once, emits the + usual signals, and returns. + + Returns: + Number of EVE windows found + """ + return self.force_scan() + def clear_history(self): """Clear the known characters history""" self.known_characters.clear() diff --git a/src/argus_overview/core/layout_manager.py b/src/argus_overview/core/layout_manager.py index b7c790d..b90b3cb 100644 --- a/src/argus_overview/core/layout_manager.py +++ b/src/argus_overview/core/layout_manager.py @@ -102,8 +102,34 @@ def __init__(self, config_dir: Path | None = None): self.layouts_dir.mkdir(exist_ok=True) self.presets: dict[str, LayoutPreset] = {} + # Lazy-initialized position registry. Created the first time + # ``set_locked`` or ``position`` is accessed so existing callers + # that never touch window positions don't pay the import. + self._position: object | None = None self._load_presets() + @property + def position(self): + """Lazily-constructed position registry. + + The ``Position`` class lives in :mod:`core.position`; importing + it at module load time would couple layout presets to position + tracking even when neither is used. Defer until first access. + """ + if self._position is None: + from argus_overview.core.position import Position + self._position = Position() + return self._position + + def set_locked(self, locked: bool) -> None: + """Lock or unlock all window positions. + + "Lock windows" prevents layout operations from moving EVE + windows. This is the operationally correct API for the Command + Palette's "Lock windows" / "Unlock windows" entries. + """ + self.position.set_locked(locked) + @staticmethod def _validate_preset_data(data) -> bool: """Validate preset dict has required fields with correct types.""" diff --git a/src/argus_overview/ui/design_system/colors.py b/src/argus_overview/ui/design_system/colors.py index 5218bf9..14760e6 100644 --- a/src/argus_overview/ui/design_system/colors.py +++ b/src/argus_overview/ui/design_system/colors.py @@ -10,7 +10,6 @@ from __future__ import annotations - # --------------------------------------------------------------------------- # Canvas / background # --------------------------------------------------------------------------- diff --git a/src/argus_overview/ui/design_system/painting.py b/src/argus_overview/ui/design_system/painting.py index 6e7bcb9..851a14d 100644 --- a/src/argus_overview/ui/design_system/painting.py +++ b/src/argus_overview/ui/design_system/painting.py @@ -7,7 +7,7 @@ from __future__ import annotations -from PySide6.QtCore import Qt, QRect +from PySide6.QtCore import QRect, Qt from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPen from PySide6.QtWidgets import QWidget @@ -164,6 +164,68 @@ def draw_status_dot( painter.drawEllipse(center_x - radius, center_y - radius, radius * 2, radius * 2) +def draw_threat_accent( + painter: QPainter, + rect: QRect, + rgb: tuple[int, int, int], + alpha: float = 1.0, + *, + edge: str = "right", + ribbon_width: int = 2, + glow_height: int = 1, +) -> None: + """Paint a threat-state accent ribbon + top glow on a widget rect. + + Used by FleetCard and TacticalCard to keep threat visualization + consistent: a thin colored ribbon on one edge plus a 1px glow on + the top edge, both alpha-modulated by the live threat ``alpha`` + (so threats fade as intel ages out). + + Args: + painter: Active QPainter. + rect: Widget rectangle to paint within. + rgb: (R, G, B) tuple for the threat color. + alpha: 0.0–1.0 fade factor (1.0 = full saturation). + edge: Which edge carries the ribbon (``"right"``, ``"left"``, + ``"top"``, ``"bottom"``). + ribbon_width: Pixels wide for the ribbon. + glow_height: Pixels tall for the top glow. + """ + a = max(0.0, min(1.0, alpha)) + if a <= 0.0: + return + painter.setPen(Qt.PenStyle.NoPen) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + ribbon_color = QColor(*rgb, int(230 * a)) + painter.setBrush(ribbon_color) + if edge == "right": + painter.drawRect( + rect.x() + rect.width() - ribbon_width, + rect.y() + 6, + ribbon_width, + rect.height() - 12, + ) + elif edge == "left": + painter.drawRect( + rect.x(), rect.y() + 6, ribbon_width, rect.height() - 12, + ) + elif edge == "top": + painter.drawRect( + rect.x() + 6, rect.y(), rect.width() - 12, ribbon_width, + ) + elif edge == "bottom": + painter.drawRect( + rect.x() + 6, + rect.y() + rect.height() - ribbon_width, + rect.width() - 12, + ribbon_width, + ) + # Subtle top-edge glow across the whole rect + glow_color = QColor(*rgb, int(110 * a)) + painter.setBrush(glow_color) + painter.drawRect(rect.x(), rect.y(), rect.width(), glow_height) + + def widget_rect(widget: QWidget, margin: int = 0) -> QRect: """Return the client rectangle of a widget, optionally inset by margin.""" return QRect(margin, margin, widget.width() - 2 * margin, widget.height() - 2 * margin) diff --git a/src/argus_overview/ui/intel_tab.py b/src/argus_overview/ui/intel_tab.py index 588d97c..b08e181 100644 --- a/src/argus_overview/ui/intel_tab.py +++ b/src/argus_overview/ui/intel_tab.py @@ -10,8 +10,6 @@ from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QBrush, QColor, QFont - -from argus_overview.ui.design_system import colors as ds from PySide6.QtWidgets import ( QAbstractItemView, QApplication, @@ -40,6 +38,7 @@ from argus_overview.intel.alerts import AlertConfig, AlertDispatcher, AlertType from argus_overview.intel.log_watcher import ChatLogWatcher, ChatMessage from argus_overview.intel.parser import IntelParser, IntelReport, ThreatLevel +from argus_overview.ui.design_system import colors as ds class IntelLogTable(QTableWidget): diff --git a/src/argus_overview/ui/main_tab.py b/src/argus_overview/ui/main_tab.py index b248695..f3cea7c 100644 --- a/src/argus_overview/ui/main_tab.py +++ b/src/argus_overview/ui/main_tab.py @@ -1428,7 +1428,8 @@ def _paint_border_layer(self, painter: QPainter, health: str) -> None: When capture health is ERROR, DISCONNECTED, or STALE, the threat border is suppressed so stale data cannot look alarming. """ - from argus_overview.ui.design_system import colors as ds, metrics as dm + from argus_overview.ui.design_system import colors as ds + from argus_overview.ui.design_system import metrics as dm # Determine if health should suppress the threat border suppress_threat = health.startswith("STALE") or health in ("ERROR", "DISCONNECTED", "PAUSED") @@ -1557,7 +1558,8 @@ def _paint_chrome_layer(self, painter: QPainter) -> None: def _paint_badge_layer(self, painter: QPainter, health: str) -> None: """System pill, age pill, capture health badge.""" - from argus_overview.ui.design_system import colors as ds, metrics as dm + from argus_overview.ui.design_system import colors as ds + from argus_overview.ui.design_system import metrics as dm from argus_overview.ui.design_system.painting import draw_badge, draw_pill # Threat system pill (top-left) @@ -1656,7 +1658,8 @@ def _paint_focus_layer(self, painter: QPainter) -> None: """PR7: draw a focus ring when this widget has keyboard focus.""" if not self.hasFocus(): return - from argus_overview.ui.design_system import colors as ds, metrics as dm + from argus_overview.ui.design_system import colors as ds + from argus_overview.ui.design_system import metrics as dm pen = QPen(QColor(ds.BORDER_FOCUS)) pen.setWidth(2) @@ -2349,7 +2352,8 @@ def _create_toolbar(self) -> QWidget: def _create_layout_controls(self) -> QWidget: """Create comprehensive layout controls panel with arrangement grid""" - from argus_overview.ui.design_system import colors as ds, metrics as dm + from argus_overview.ui.design_system import colors as ds + from argus_overview.ui.design_system import metrics as dm section = QGroupBox("Window Layouts") section.setStyleSheet(f""" diff --git a/src/argus_overview/ui/status_dock.py b/src/argus_overview/ui/status_dock.py index 49d10bf..5b3d670 100644 --- a/src/argus_overview/ui/status_dock.py +++ b/src/argus_overview/ui/status_dock.py @@ -16,7 +16,7 @@ import logging import time -from PySide6.QtCore import Qt, QPropertyAnimation, Signal, QTimer +from PySide6.QtCore import QPropertyAnimation, Qt, QTimer, Signal from PySide6.QtGui import QBrush, QColor, QPainter, QPen from PySide6.QtWidgets import ( QFrame, @@ -285,7 +285,6 @@ def mousePressEvent(self, event): def keyPressEvent(self, event) -> None: """PR4: keyboard navigation — Enter/Space activate, arrows move focus.""" - from PySide6.QtGui import QKeyEvent if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Space): self.clicked.emit(self.window_id) diff --git a/src/argus_overview/ui/system_status_bar.py b/src/argus_overview/ui/system_status_bar.py index d9dc321..5ae8ec7 100644 --- a/src/argus_overview/ui/system_status_bar.py +++ b/src/argus_overview/ui/system_status_bar.py @@ -139,3 +139,15 @@ def set_status(self, subsystem: str, status: str, detail: str = "") -> None: def get_status(self, subsystem: str) -> tuple[str, str]: """Return (status, detail) for a subsystem.""" return self._status.get(subsystem, "unknown"), self._detail.get(subsystem, "") + + def snapshot(self) -> dict[str, tuple[str, str]]: + """Return a copy of all subsystem (status, detail) pairs. + + Used by peer subsystems (e.g. :class:`CommandIntegrator`) to + seed a parallel status view without reaching into private + state. Returns a fresh dict so callers cannot mutate ours. + """ + return { + key: (self._status.get(key, "unknown"), self._detail.get(key, "")) + for key in self._status + } diff --git a/tests/test_design_system_painting.py b/tests/test_design_system_painting.py index 226bb22..a88fec4 100644 --- a/tests/test_design_system_painting.py +++ b/tests/test_design_system_painting.py @@ -7,12 +7,11 @@ from __future__ import annotations import pytest - from PySide6.QtCore import QRect from PySide6.QtGui import QColor, QPainter, QPixmap from PySide6.QtWidgets import QWidget -from argus_overview.ui.design_system import colors, metrics +from argus_overview.ui.design_system import colors from argus_overview.ui.design_system.painting import ( draw_badge, draw_pill, diff --git a/tests/test_main_tab.py b/tests/test_main_tab.py index fa74339..32a146a 100644 --- a/tests/test_main_tab.py +++ b/tests/test_main_tab.py @@ -10362,8 +10362,8 @@ def test_frame_and_chip_share_accent(self, qapp): chip.deleteLater() def test_legacy_chip_aliases_resolve_to_main_tab_helpers(self): - from argus_overview.ui.main_tab import character_accent_color from argus_overview.ui.design_system.colors import ACCENT_POOL + from argus_overview.ui.main_tab import character_accent_color from argus_overview.ui.status_dock import CHIP_ACCENT_COLORS, accent_for assert CHIP_ACCENT_COLORS is ACCENT_POOL @@ -11008,7 +11008,9 @@ class TestWindowPreviewWidgetFocus: def test_focus_in_event_calls_update(self, qapp): from unittest.mock import patch + from PySide6.QtWidgets import QWidget + from argus_overview.ui.main_tab import WindowPreviewWidget widget = WindowPreviewWidget.__new__(WindowPreviewWidget) @@ -11019,7 +11021,9 @@ def test_focus_in_event_calls_update(self, qapp): def test_focus_out_event_calls_update(self, qapp): from unittest.mock import patch + from PySide6.QtWidgets import QWidget + from argus_overview.ui.main_tab import WindowPreviewWidget widget = WindowPreviewWidget.__new__(WindowPreviewWidget) @@ -11030,8 +11034,8 @@ def test_focus_out_event_calls_update(self, qapp): def test_paint_focus_layer_when_focused(self, qapp): """When hasFocus() is True, _paint_focus_layer should draw a rounded rect.""" - from argus_overview.ui.main_tab import WindowPreviewWidget from argus_overview.ui.design_system import colors as _ds + from argus_overview.ui.main_tab import WindowPreviewWidget with patch.object(WindowPreviewWidget, "__init__", return_value=None): widget = WindowPreviewWidget.__new__(WindowPreviewWidget) diff --git a/tests/test_preview_health_paint.py b/tests/test_preview_health_paint.py index cb39e53..2b6d024 100644 --- a/tests/test_preview_health_paint.py +++ b/tests/test_preview_health_paint.py @@ -11,10 +11,7 @@ from unittest.mock import MagicMock import pytest - -from PySide6.QtCore import QEvent from PySide6.QtGui import QPaintEvent -from PySide6.QtWidgets import QApplication from argus_overview.intel.parser import ThreatLevel from argus_overview.ui.main_tab import WindowPreviewWidget diff --git a/tests/test_settings_tab.py b/tests/test_settings_tab.py index fa82264..7b4831e 100644 --- a/tests/test_settings_tab.py +++ b/tests/test_settings_tab.py @@ -1194,8 +1194,8 @@ def test_create_category_tree_stylesheet_and_default_selection( self, mock_btn, mock_item, mock_tree, mock_font, mock_label, mock_layout, mock_qwidget ): """Test _create_category_tree applies design-system stylesheet and selects first item""" - from argus_overview.ui.settings_tab import SettingsTab from argus_overview.ui.design_system import colors as _ds + from argus_overview.ui.settings_tab import SettingsTab mock_settings = MagicMock() mock_settings.get.return_value = {} diff --git a/tests/test_system_status_bar.py b/tests/test_system_status_bar.py index 65e347b..bdd2d18 100644 --- a/tests/test_system_status_bar.py +++ b/tests/test_system_status_bar.py @@ -9,12 +9,9 @@ from __future__ import annotations -from unittest.mock import MagicMock, patch +from unittest.mock import patch -import pytest -from PySide6.QtCore import Qt -from PySide6.QtGui import QPaintEvent, QPainter -from PySide6.QtWidgets import QWidget +from PySide6.QtGui import QPainter, QPaintEvent from argus_overview.ui.system_status_bar import SystemStatusBar From 0de004f265b34e52455a89ab6e7540bb2bde86d1 Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Wed, 5 Aug 2026 03:42:43 -0700 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20Phase=204=20IA=20=E2=80=94=20COMMAN?= =?UTF-8?q?D=20/=20FLEET=20/=20LAYOUTS=20/=20SYSTEM=20+=20Command=20Center?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the six v2.2 tabs into four IA-aligned tabs that match the operator's mental model and let the Command Center (v3.3 OPS flagship surface) be the natural home for live fleet operations. New top-level IA: - COMMAND — hosts the Command Center (header, fleet rail, tactical grid, attention queue, ops timeline, operational truth bar). - FLEET — CharactersTeamsTab + IntelTab side-by-side via QSplitter (60/40). "Your people and the intel that affects them." - LAYOUTS — LayoutsTab + HotkeysTab side-by-side via QSplitter (70/30). "How your fleet is arranged on screen." - SYSTEM — SettingsTab + SettingsSyncTab side-by-side via QSplitter (60/40). "App + EVE folder configuration." Command Center subsystem (src/argus_overview/ui/command/): - shell.py — CommandCenterWidget: header + fleet rail + grid holder + ops timeline + attention queue + truth bar. - header.py — identity bar (ARGUS // OPS title, fleet count, intel health, alert badge with pulse). - fleet_rail.py — permanent left rail of pilot identity cards. - tactical_grid.py — center grid host for the existing preview grid. - attention.py — top-right attention queue (severity-coded items). - operational_truth.py — bottom bar with named subsystems + alert counter. - palette.py — Command Palette (Ctrl+K): fuzzy-search gateway to focus a pilot, apply a layout, switch a theme, lock windows. - integration.py — CommandIntegrator: wires the Command Center into MainWindow, attaches Ctrl+K, mirrors main_tab's character list, polls subsystem status, seeds alert count from existing UI. MainWindowV21 changes: - _TAB_LABELS now ["Command", "Fleet", "Layouts", "System"]. - _create_tabs() single entry point — builds the inner widgets via the legacy factories, then wraps them in IA containers. - Inner widgets live under main_tab, characters_tab, presets_panel, hotkeys_tab, intel_tab, settings_tab, settings_sync_tab. The container attributes (command_tab, fleet_tab, layouts_tab, system_tab) match the IA labels. - _show_settings now points at SYSTEM via _TAB_LABELS.index("System") — "Settings" is no longer a top-level tab, but it still lives inside the SYSTEM container. - _create_layouts_tab uses kwargs for the LayoutsTab constructor (the signature is (layout_manager, main_tab, settings_manager= None, character_manager=None)) — passing character_manager as the second positional arg crashed at runtime with "Main tab not initialized" the moment the user hit Apply. Pinned by test_create_layouts_tab_passes_main_tab_to_main_slot. - layout_applied signal is connected exactly once: from main_tab to _on_layout_applied / CommandIntegrator. Documented why the layouts_tab.path is not used. ActionRegistry: - Section header comments now note the Phase 4 IA mapping (OVERVIEW_TOOLBAR → COMMAND, ROSTER_TOOLBAR / INTEL_TOOLBAR → FLEET, LAYOUTS_TOOLBAR / CYCLE_CONTROL_TOOLBAR → LAYOUTS, SYNC_TOOLBAR / SETTINGS_PANEL → SYSTEM). PrimaryHome enum values are unchanged — actions still bind to the same inner widget. - Refresh Layout Groups tooltip updated to "from the LAYOUTS tab (Cycle Control pane)". Tests (24 new): - tests/test_tab_containers.py — CommandTab signal forwarding, FleetTab/LayoutsContainer/SystemTab splitter structure, ratio pinning (60/40, 70/30, 60/40), objectName selectors, horizontal orientation, stub-injection smoke test. - tests/test_command_center.py — structure of every Command Center component (header, fleet rail, attention, ops timeline, truth). - tests/test_command_integrator.py — palette shortcut, attach semantics, alert count handoff, subsystem snapshot. - additions to tests/test_main_window_v21.py (TestPhase4InformationArchitecture) — pins _TAB_LABELS, routes _show_settings to SYSTEM, and pins the LayoutsTab constructor kwargs contract. Phase 4 senior review addressed all 11 findings (1 critical): naming symmetry (inner widget → presets_panel), duplicate signal connection, tautological alias, and the critical kwargs order bug. Full suite: 2,580 passed, 5 skipped (target ≥ 2,561). Co-Authored-By: Claude --- src/argus_overview/ui/action_registry.py | 16 +- src/argus_overview/ui/command/__init__.py | 47 ++ src/argus_overview/ui/command/attention.py | 446 ++++++++++++++ src/argus_overview/ui/command/fleet_rail.py | 476 +++++++++++++++ src/argus_overview/ui/command/header.py | 326 ++++++++++ src/argus_overview/ui/command/integration.py | 358 +++++++++++ .../ui/command/operational_truth.py | 208 +++++++ src/argus_overview/ui/command/palette.py | 351 +++++++++++ src/argus_overview/ui/command/shell.py | 143 +++++ .../ui/command/tactical_grid.py | 564 ++++++++++++++++++ src/argus_overview/ui/main_window_v21.py | 212 ++++++- src/argus_overview/ui/tabs/__init__.py | 29 + src/argus_overview/ui/tabs/command_tab.py | 50 ++ src/argus_overview/ui/tabs/fleet_tab.py | 54 ++ src/argus_overview/ui/tabs/layouts_tab.py | 56 ++ src/argus_overview/ui/tabs/system_tab.py | 45 ++ tests/test_command_center.py | 389 ++++++++++++ tests/test_command_integrator.py | 359 +++++++++++ tests/test_main_window_v21.py | 100 +++- tests/test_tab_containers.py | 237 ++++++++ 20 files changed, 4444 insertions(+), 22 deletions(-) create mode 100644 src/argus_overview/ui/command/__init__.py create mode 100644 src/argus_overview/ui/command/attention.py create mode 100644 src/argus_overview/ui/command/fleet_rail.py create mode 100644 src/argus_overview/ui/command/header.py create mode 100644 src/argus_overview/ui/command/integration.py create mode 100644 src/argus_overview/ui/command/operational_truth.py create mode 100644 src/argus_overview/ui/command/palette.py create mode 100644 src/argus_overview/ui/command/shell.py create mode 100644 src/argus_overview/ui/command/tactical_grid.py create mode 100644 src/argus_overview/ui/tabs/__init__.py create mode 100644 src/argus_overview/ui/tabs/command_tab.py create mode 100644 src/argus_overview/ui/tabs/fleet_tab.py create mode 100644 src/argus_overview/ui/tabs/layouts_tab.py create mode 100644 src/argus_overview/ui/tabs/system_tab.py create mode 100644 tests/test_command_center.py create mode 100644 tests/test_command_integrator.py create mode 100644 tests/test_tab_containers.py diff --git a/src/argus_overview/ui/action_registry.py b/src/argus_overview/ui/action_registry.py index fa8485c..56f7dde 100644 --- a/src/argus_overview/ui/action_registry.py +++ b/src/argus_overview/ui/action_registry.py @@ -265,7 +265,7 @@ def _register_all_actions(self): # TIER 2: TAB ACTIONS (Tab Toolbars) # ===================================================================== - # --- Overview Tab (formerly Main) --- + # --- Overview Tab (formerly Main) — Phase 4 IA: COMMAND tab --- self.register( ActionSpec( id="import_windows", @@ -370,7 +370,7 @@ def _register_all_actions(self): ) ) - # --- Roster Tab (Characters & Teams) --- + # --- Roster Tab (Characters & Teams) — Phase 4 IA: FLEET tab (left) --- self.register( ActionSpec( id="add_character", @@ -405,7 +405,7 @@ def _register_all_actions(self): ) ) - # --- Layouts Tab --- + # --- Layouts Tab — Phase 4 IA: LAYOUTS container (left) --- self.register( ActionSpec( id="apply_layout", @@ -445,12 +445,12 @@ def _register_all_actions(self): label="Refresh Groups", scope=ActionScope.TAB, primary_home=PrimaryHome.LAYOUTS_TOOLBAR, - tooltip="Reload cycling groups from Cycle Control tab", + tooltip="Reload cycling groups from the LAYOUTS tab (Cycle Control pane)", handler_name="_refresh_groups", ) ) - # --- Cycle Control Tab (Hotkeys + Cycling) --- + # --- Cycle Control Tab (Hotkeys + Cycling) — Phase 4 IA: LAYOUTS (right) --- self.register( ActionSpec( id="new_group", @@ -484,7 +484,7 @@ def _register_all_actions(self): ) ) - # --- Sync Tab --- + # --- Sync Tab — Phase 4 IA: SYSTEM container (right) --- self.register( ActionSpec( id="scan_eve_settings", @@ -518,7 +518,7 @@ def _register_all_actions(self): ) ) - # --- Intel Tab --- + # --- Intel Tab — Phase 4 IA: FLEET container (right) --- self.register( ActionSpec( id="start_intel_monitoring", @@ -608,7 +608,7 @@ def _register_all_actions(self): ) ) - # --- Settings Panel --- + # --- Settings Panel — Phase 4 IA: SYSTEM container (left) --- self.register( ActionSpec( id="reset_all_settings", diff --git a/src/argus_overview/ui/command/__init__.py b/src/argus_overview/ui/command/__init__.py new file mode 100644 index 0000000..09a9116 --- /dev/null +++ b/src/argus_overview/ui/command/__init__.py @@ -0,0 +1,47 @@ +"""Argus Command Center — flagship widgets. + +The ``command`` package groups the widgets that compose the +identity-defining Command tab: + +* :class:`CommandCenterHeader` — top brand chrome +* :class:`FleetRail` — pinned pilot identity surface +* :class:`AttentionQueue` and :class:`OpsTimeline` — right-side overlays +* :class:`OperationalTruthBar` — bottom status footer +* :class:`CommandPalette` — ⌘K action gateway +* :class:`CommandCenterWidget` — assembled flagship shell +""" +from __future__ import annotations + +from argus_overview.ui.command.attention import ( + AttentionItem, + AttentionItemRow, + AttentionQueue, + OpsEntry, + OpsTimeline, +) +from argus_overview.ui.command.fleet_rail import FleetCard, FleetRail +from argus_overview.ui.command.header import ( + BrandMark, + CommandCenterHeader, + OperationalStatusLine, +) +from argus_overview.ui.command.operational_truth import OperationalTruthBar +from argus_overview.ui.command.palette import CommandPalette, PaletteEntry +from argus_overview.ui.command.shell import CommandCenterWidget + +__all__ = [ + "AttentionItem", + "AttentionItemRow", + "AttentionQueue", + "BrandMark", + "CommandCenterHeader", + "CommandCenterWidget", + "CommandPalette", + "FleetCard", + "FleetRail", + "OperationalStatusLine", + "OperationalTruthBar", + "OpsEntry", + "OpsTimeline", + "PaletteEntry", +] diff --git a/src/argus_overview/ui/command/attention.py b/src/argus_overview/ui/command/attention.py new file mode 100644 index 0000000..016ec04 --- /dev/null +++ b/src/argus_overview/ui/command/attention.py @@ -0,0 +1,446 @@ +"""Argus Command Center — Attention Queue & Operations Timeline. + +These two widgets form the operator's tactical awareness layer on the +right side of the Command tab. They collapse to a label when empty and +expand smoothly when items arrive. + +* AttentionQueue surfaces events that demand operator decision: + intel threats, capture failures, location staleness, layout apply + errors. Each item is dismissible and individually actionable. + +* OpsTimeline surfaces the recent operational history: layout applied, + layout restored, pilot focused, intel cleared, hotkey pressed. Acts + as a confidence-building "what did I just do" feed. + +The visual is deliberate: small, dense, monospace-aligned times, color +dots not icons. Information first. +""" +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from datetime import datetime + +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QFont +from PySide6.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QPushButton, + QScrollArea, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from argus_overview.ui.design_system import colors as ds +from argus_overview.ui.design_system import metrics as dm +from argus_overview.ui.design_system import spacing as sp +from argus_overview.ui.design_system import typography as ty + + +@dataclass +class AttentionItem: + """An event demanding operator decision or awareness.""" + + id: str + category: str # threat | capture | location | layout | system + title: str + detail: str = "" + pilot: str | None = None + system: str | None = None + timestamp: float = field(default_factory=time.monotonic) + severity: str = "info" # info | warning | critical + acknowledged: bool = False + + def severity_color(self) -> str: + return { + "info": ds.INFO, + "warning": ds.WARNING, + "critical": ds.CRITICAL, + }.get(self.severity, ds.TEXT_SECONDARY) + + +@dataclass +class OpsEntry: + """A passive operational log entry.""" + + timestamp: float + label: str # e.g., "Layout applied" + detail: str = "" + pilot: str | None = None + category: str = "system" # layout | capture | intel | hotkey | pilot | system + + def category_color(self) -> str: + return { + "layout": ds.BORDER_FOCUS, + "capture": ds.INFO, + "intel": ds.CRITICAL, + "hotkey": ds.WARNING, + "pilot": ds.HEALTHY, + "system": ds.TEXT_MUTED, + }.get(self.category, ds.TEXT_MUTED) + + +@dataclass +class AttentionItemRow(QFrame): + """One row in the Attention Queue. + + A compact card with a colored left rule (severity), title in + primary weight, detail in muted, dismiss button on the right. + """ + + item_dismissed = Signal(str) # item id + item_acted = Signal(str) # item id + + def __init__(self, item: AttentionItem, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.item = item + self.setObjectName(f"AttentionRow::{item.id}") + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + self.setMinimumHeight(48) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + layout = QHBoxLayout(self) + layout.setContentsMargins(sp.SPACE_3, sp.SPACE_2, sp.SPACE_2, sp.SPACE_2) + layout.setSpacing(sp.SPACE_3) + + # Severity rule + self._rule = QWidget(self) + self._rule.setObjectName("AttentionRule") + self._rule.setFixedSize(3, 28) + self._rule.setStyleSheet(f"background-color: {item.severity_color()};") + layout.addWidget(self._rule) + + # Text block + text_block = QWidget(self) + text_layout = QVBoxLayout(text_block) + text_layout.setContentsMargins(0, 0, 0, 0) + text_layout.setSpacing(0) + title = QLabel(item.title) + title.setStyleSheet( + f"color: {ds.TEXT_PRIMARY}; font-weight: 700; font-size: 10pt;" + ) + title.setWordWrap(False) + text_layout.addWidget(title) + sub_text = " · ".join(filter(None, [ + f"[{item.category.upper()}]", + item.pilot, + item.system, + ])) + (" " + item.detail if item.detail else "") + if sub_text.strip(): + sub = QLabel(sub_text.strip()) + sub.setStyleSheet( + f"color: {ds.TEXT_MUTED}; font-size: 9pt;" + ) + sub.setWordWrap(True) + text_layout.addWidget(sub) + layout.addWidget(text_block, 1) + + # Action button + action_btn = QPushButton("·", self) + action_btn.setObjectName("AttentionAct") + action_btn.setFixedSize(24, 24) + action_btn.setCursor(Qt.CursorShape.PointingHandCursor) + action_btn.setToolTip("Acknowledge this item") + action_btn.clicked.connect(lambda: self.item_acted.emit(self.item.id)) + action_btn.setStyleSheet( + f""" + QPushButton#AttentionAct {{ + background: transparent; + border: 1px solid {ds.BORDER_SUBTLE}; + border-radius: 12px; + color: {ds.TEXT_SECONDARY}; + font-weight: 800; + }} + QPushButton#AttentionAct:hover {{ + background: {ds.SURFACE_RAISED}; + border-color: {ds.BORDER_FOCUS}; + color: {ds.TEXT_PRIMARY}; + }} + """ + ) + layout.addWidget(action_btn) + + # Style the row itself + self.setStyleSheet( + f""" + QFrame#{self.objectName()} {{ + background-color: {ds.SURFACE}; + border: 1px solid {ds.BORDER_SUBTLE}; + border-radius: {dm.RADIUS_CARD}px; + }} + QFrame#{self.objectName()}:hover {{ + border-color: {ds.BORDER_STRONG}; + background-color: {ds.SURFACE_RAISED}; + }} + """ + ) + + +class AttentionQueue(QWidget): + """Right-side panel of events demanding operator response. + + Items appear at top, age out after 5 minutes unless acknowledged. + Empty state shows a quiet 'All clear' line. + """ + + item_acted = Signal(str) + + RETENTION_SECONDS = 300 + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._items: dict[str, AttentionItem] = {} + self._rows: dict[str, AttentionItemRow] = {} + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(sp.SPACE_2) + + # Header + header = QWidget(self) + h_layout = QHBoxLayout(header) + h_layout.setContentsMargins(sp.SPACE_3, sp.SPACE_2, sp.SPACE_3, sp.SPACE_2) + h_layout.setSpacing(0) + title = QLabel("ATTENTION QUEUE", self) + title.setObjectName("AttentionTitle") + f = QFont(title.font()) + f.setPointSize(ty.SECTION_HEADING_PT) + f.setWeight(QFont.Weight.Bold) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 180) + title.setFont(f) + h_layout.addWidget(title) + h_layout.addStretch(1) + self._count_label = QLabel("·", self) + self._count_label.setObjectName("AttentionCount") + self._count_label.setStyleSheet(f"color: {ds.CRITICAL}; font-weight: 800; font-size: 10pt;") + h_layout.addWidget(self._count_label) + layout.addWidget(header) + + # Content + self._scroll = QScrollArea(self) + self._scroll.setWidgetResizable(True) + self._scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self._scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self._scroll.setFrameShape(QFrame.Shape.NoFrame) + layout.addWidget(self._scroll, 1) + + self._content = QWidget(self) + self._content_layout = QVBoxLayout(self._content) + self._content_layout.setContentsMargins(sp.SPACE_2, sp.SPACE_2, sp.SPACE_2, sp.SPACE_2) + self._content_layout.setSpacing(sp.SPACE_2) + self._content_layout.addStretch(1) + self._scroll.setWidget(self._content) + + self._render_empty() + + def _render_empty(self) -> None: + # Clear existing children except the trailing stretch + while self._content_layout.count() > 1: + item = self._content_layout.takeAt(0) + w = item.widget() if item else None + if w: + w.deleteLater() + + empty = QLabel("· ALL CLEAR ·", self) + empty.setObjectName("AttentionEmptyState") + empty.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty.setStyleSheet( + f"color: {ds.HEALTHY}; font-weight: 800; letter-spacing: 220%;" + f" font-size: 9pt; padding: {sp.SPACE_5}px;" + ) + # Insert at index 0 so stretch remains at end + self._content_layout.insertWidget(0, empty) + + def _refresh_count(self) -> None: + active = sum(1 for i in self._items.values() if not i.acknowledged) + if active == 0: + self._count_label.setText("") + else: + self._count_label.setText(str(active)) + + def add_item(self, item: AttentionItem) -> None: + self._items[item.id] = item + # Remove the ALL CLEAR placeholder first (synchronous), so the + # next paint pass can't briefly show both empty + row. + self._invalidate_empty() + row = AttentionItemRow(item, parent=self._content) + row.item_acted.connect(self._on_ack) + row.item_acted.connect(self.item_acted.emit) + # Insert at index 0 (newest first) + self._content_layout.insertWidget(0, row) + self._rows[item.id] = row + self._refresh_count() + + def _invalidate_empty(self) -> None: + # Remove the ALL CLEAR placeholder if present. We tag it with + # an objectName "AttentionEmptyState" so we can identify it + # without relying on type checks. + for i in range(self._content_layout.count()): + item = self._content_layout.itemAt(i) + if not item: + continue + w = item.widget() + if w is None or w is self: + continue + if w.objectName() == "AttentionEmptyState": + self._content_layout.removeWidget(w) + w.deleteLater() + return + + def _on_ack(self, item_id: str) -> None: + item = self._items.get(item_id) + if item is None: + return + item.acknowledged = True + row = self._rows.pop(item_id, None) + if row is not None: + self._content_layout.removeWidget(row) + row.deleteLater() + self._refresh_count() + if not self._rows and self._items: + self._render_empty() + + def has_active(self) -> bool: + return any(not i.acknowledged for i in self._items.values()) + + +class OpsTimeline(QWidget): + """Right-side panel of recent operational history. + + Acts as a confidence feed — operators see their last few actions + immediately reflected, which builds trust that the system is + capturing intent correctly. + """ + + ENTRIES_MAX = 24 + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._entries: list[OpsEntry] = [] + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(sp.SPACE_2) + + header = QWidget(self) + h_layout = QHBoxLayout(header) + h_layout.setContentsMargins(sp.SPACE_3, sp.SPACE_2, sp.SPACE_3, sp.SPACE_2) + h_layout.setSpacing(0) + title = QLabel("OPERATIONS TIMELINE", self) + title.setObjectName("OpsTimelineTitle") + f = QFont(title.font()) + f.setPointSize(ty.SECTION_HEADING_PT) + f.setWeight(QFont.Weight.Bold) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 180) + title.setFont(f) + h_layout.addWidget(title) + layout.addWidget(header) + + self._scroll = QScrollArea(self) + self._scroll.setWidgetResizable(True) + self._scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self._scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self._scroll.setFrameShape(QFrame.Shape.NoFrame) + layout.addWidget(self._scroll, 1) + + self._body = QWidget(self) + self._body_layout = QVBoxLayout(self._body) + self._body_layout.setContentsMargins(sp.SPACE_2, sp.SPACE_2, sp.SPACE_2, sp.SPACE_2) + self._body_layout.setSpacing(0) + self._body_layout.addStretch(1) + self._scroll.setWidget(self._body) + + self._render_empty() + + def _render_empty(self) -> None: + # Clear children that are placeholders (QLabel) but not real rows + while self._body_layout.count() > 1: + item = self._body_layout.takeAt(0) + w = item.widget() if item else None + if w: + w.deleteLater() + empty = QLabel("Awaiting first operator input…", self) + empty.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty.setStyleSheet( + f"color: {ds.TEXT_MUTED}; font-style: italic; padding: {sp.SPACE_5}px;" + ) + self._body_layout.insertWidget(0, empty) + + def add_entry(self, entry: OpsEntry) -> None: + # Insert at top, evict oldest at bottom + self._entries.insert(0, entry) + if len(self._entries) > self.ENTRIES_MAX: + self._entries.pop() + # Remove placeholder + for i in range(self._body_layout.count()): + item = self._body_layout.itemAt(i) + if not item: + continue + w = item.widget() + if w is None or w is self: + continue + # The placeholder is a QLabel; ops rows are _OpsRow widgets + if w.__class__.__name__ == "QLabel": + try: + txt = w.text() # type: ignore[attr-defined] + except AttributeError: + continue + if txt.startswith("Awaiting"): + self._body_layout.removeWidget(w) + w.deleteLater() + break + # Add visual row + row = _OpsRow(entry, parent=self._body) + self._body_layout.insertWidget(0, row) + # Trim UI rows + while self._body_layout.count() > self.ENTRIES_MAX + 1: + item = self._body_layout.takeAt(self._body_layout.count() - 2) + w = item.widget() if item else None + if w: + w.deleteLater() + + +class _OpsRow(QWidget): + """Single compact row in the operations timeline.""" + + def __init__(self, entry: OpsEntry, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setMinimumHeight(22) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + from PySide6.QtWidgets import QHBoxLayout + layout = QHBoxLayout(self) + layout.setContentsMargins(sp.SPACE_2, 0, sp.SPACE_2, 0) + layout.setSpacing(sp.SPACE_2) + + self._time = QLabel(self._format_time(entry.timestamp)) + self._time.setObjectName("OpsRowTime") + self._time.setStyleSheet( + f"color: {ds.TEXT_MUTED}; font-family: monospace; font-size: 9pt;" + ) + self._time.setMinimumWidth(56) + layout.addWidget(self._time) + + self._dot = QLabel("●", self) + self._dot.setStyleSheet( + f"color: {entry.category_color()}; font-size: 8pt;" + ) + self._dot.setFixedWidth(10) + layout.addWidget(self._dot) + + text = entry.label + if entry.pilot: + text += f" · {entry.pilot}" + if entry.detail: + text += f" — {entry.detail}" + self._text = QLabel(text, self) + self._text.setStyleSheet( + f"color: {ds.TEXT_SECONDARY}; font-size: 9pt;" + ) + layout.addWidget(self._text, 1) + + def _format_time(self, ts: float) -> str: + return datetime.fromtimestamp(ts).strftime("%H:%M:%S") diff --git a/src/argus_overview/ui/command/fleet_rail.py b/src/argus_overview/ui/command/fleet_rail.py new file mode 100644 index 0000000..50c232a --- /dev/null +++ b/src/argus_overview/ui/command/fleet_rail.py @@ -0,0 +1,476 @@ +"""Argus Command Center — Fleet Rail. + +The Fleet Rail is the operator's primary fleet identity surface on the +Command tab. It is a vertical strip pinned to the left side of the +Command Center shell. Every active pilot has a permanent card with: + + - Accent avatar (always-color; carries character identity) + - Pilot name + EVE-class role chip + - System + distance pill + - Threat indicator (color + letter + age) + - Focus state (active dot when window has focus) + - Stale state (dimmed + 'last:' suffix) + +Click a card to focus the matching window. Hover surfaces a tooltip +with full context: capture health, last report, distance from threat. + +The Fleet Rail replaces the floating StatusDock as the primary identity +read — it is always visible, always ordered, and never collapses. +""" +from __future__ import annotations + +import time + +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QColor, QFont, QPainter +from PySide6.QtWidgets import QFrame, QLabel, QSizePolicy, QVBoxLayout, QWidget + +from argus_overview.intel.parser import ThreatLevel +from argus_overview.ui.design_system import colors as ds +from argus_overview.ui.design_system import metrics as dm +from argus_overview.ui.design_system import spacing as sp +from argus_overview.ui.design_system import typography as ty +from argus_overview.ui.design_system.painting import ( + draw_threat_accent, +) + +THREAT_LETTERS = {"danger": "D", "warning": "W", "critical": "C", "clear": "OK"} + + +def _initials(name: str) -> str: + parts = [p for p in name.replace("_", " ").split() if p] + if not parts: + return "?" + if len(parts) == 1: + return parts[0][:2].upper() + return (parts[0][0] + parts[-1][0]).upper() + + +class FleetCard(QFrame): + """A single persistent pilot identity card. + + Always rendered in the same position, full opacity (with subtle + variation by state), never expands or contracts based on layout + decisions. Tap to focus; long-press for context menu (forwarded + via right-click). + """ + + clicked = Signal(str) # window_id + context_requested = Signal(str, object) # window_id, QPoint + + def __init__( + self, + window_id: str, + character_name: str, + accent: tuple[int, int, int], + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self._window_id = window_id + self._character_name = character_name + self._accent = QColor(*accent) + self._system: str | None = None + self._last_system: str | None = None + self._has_focus: bool = False + self._capture_health: str = "live" # live | static | stale | error | paused + self._threat_level: ThreatLevel | None = None + self._threat_alpha: float = 0.0 + self._threat_set_at: float = 0.0 + self._threat_distance: int | None = None + self._stale: bool = False + + self.setObjectName(f"FleetCard::{window_id}") + self.setMinimumWidth(168) + self.setMaximumWidth(220) + self.setFixedHeight(64) + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.setCursor(Qt.CursorShape.PointingHandCursor) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + + layout = QVBoxLayout(self) + layout.setContentsMargins(sp.SPACE_3, sp.SPACE_2, sp.SPACE_3, sp.SPACE_2) + layout.setSpacing(2) + + # Top row — avatar + name + focus dot + top = QWidget(self) + from PySide6.QtWidgets import QHBoxLayout + top_layout = QHBoxLayout(top) + top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.setSpacing(sp.SPACE_2) + self._avatar = QLabel(_initials(character_name)) + self._avatar.setObjectName("FleetAvatar") + self._avatar.setFixedSize(22, 22) + self._avatar.setAlignment(Qt.AlignmentFlag.AlignCenter) + top_layout.addWidget(self._avatar) + self._name = QLabel(character_name) + self._name.setObjectName("FleetName") + top_layout.addWidget(self._name, 1) + self._focus_dot = QWidget(self) + self._focus_dot.setFixedSize(8, 8) + top_layout.addWidget(self._focus_dot) + layout.addWidget(top) + + # Bottom row — system + threat chip + bottom = QWidget(self) + b_layout = QHBoxLayout(bottom) + b_layout.setContentsMargins(0, 0, 0, 0) + b_layout.setSpacing(sp.SPACE_2) + self._system_label = QLabel("—") + self._system_label.setObjectName("FleetSystemLabel") + b_layout.addWidget(self._system_label, 1) + self._threat_badge = QLabel("") + self._threat_badge.setObjectName("FleetThreatBadge") + self._threat_badge.setFixedHeight(16) + self._threat_badge.setMinimumWidth(28) + self._threat_badge.setAlignment(Qt.AlignmentFlag.AlignCenter) + b_layout.addWidget(self._threat_badge) + layout.addWidget(bottom) + + self._apply_styles() + self._update_tooltip() + self._update_accessible() + + # ---- public API -------------------------------------------------------- + def window_id(self) -> str: + return self._window_id + + def character_name(self) -> str: + return self._character_name + + def set_system(self, system: str | None, *, stale: bool = False) -> None: + self._system = system + self._stale = stale + if system and not stale: + self._last_system = system + self._update_system_label() + self._update_tooltip() + self._update_accessible() + self._apply_styles() + + def set_capture_health(self, health: str) -> None: + self._capture_health = health + self._update_tooltip() + self._update_accessible() + self.update() + + def set_focused(self, focused: bool) -> None: + self._has_focus = focused + self._apply_styles() + self.update() + + def set_threat_state( + self, + level: ThreatLevel | None, + system: str | None = None, + alpha: float = 1.0, + distance: int | None = None, + ) -> None: + if level is None or level == ThreatLevel.CLEAR: + self._threat_level = None + self._threat_alpha = 0.0 + self._threat_distance = None + else: + self._threat_level = level + self._threat_alpha = max(0.0, min(1.0, alpha)) + self._threat_distance = distance if distance and distance > 0 else None + self._threat_set_at = time.monotonic() + if system is not None: + self.set_system(system) + self._update_threat_badge() + self._update_tooltip() + self._update_accessible() + self.update() + + # ---- styling & rendering ---------------------------------------------- + def _update_system_label(self) -> None: + if self._system and not self._stale: + self._system_label.setText(self._system) + elif self._last_system and self._stale: + self._system_label.setText(f"Unknown · last: {self._last_system}") + else: + self._system_label.setText("Unknown") + + def _update_threat_badge(self) -> None: + if self._threat_level and self._threat_alpha > 0.0: + letter = THREAT_LETTERS.get(self._threat_level.value.lower(), "?") + if self._threat_distance and self._threat_distance > 0: + self._threat_badge.setText(f"{letter}+{self._threat_distance}j") + else: + self._threat_badge.setText(letter) + else: + self._threat_badge.setText("") + + def _apply_styles(self) -> None: + accent = self._accent + darker = accent.darker(160) + a_name = accent.name() + d_name = darker.name() + text_color = "#0f0f0f" if (accent.redF() * 0.299 + accent.greenF() * 0.587 + accent.blueF() * 0.114) > 0.55 else "#f5f5f5" + + focus_border = ds.BORDER_FOCUS if self._has_focus else ds.BORDER_SUBTLE + focus_bg = ds.SURFACE if not self._has_focus else ds.SURFACE_RAISED + if self._stale: + focus_bg = ds.CANVAS + + self.setStyleSheet( + f""" + QFrame#{self.objectName()} {{ + background-color: {focus_bg}; + border: 1px solid {focus_border}; + border-left: 3px solid {a_name}; + border-radius: {dm.RADIUS_CARD}px; + }} + QFrame#{self.objectName()}:hover {{ + background-color: {ds.SURFACE_HOVER}; + border-color: {ds.BORDER_FOCUS}; + border-left: 3px solid {d_name}; + }} + QLabel#FleetAvatar {{ + background-color: {a_name}; + border: 1px solid {d_name}; + border-radius: 9px; + color: {text_color}; + font-weight: 800; + font-size: 9pt; + }} + QLabel#FleetName {{ + color: {ds.TEXT_PRIMARY}; + font-weight: 700; + font-size: 10pt; + }} + QLabel#FleetSystemLabel {{ + color: {ds.TEXT_MUTED if self._stale else ds.TEXT_SECONDARY}; + font-size: 9pt; + }} + QLabel#FleetThreatBadge {{ + color: {ds.TEXT_PRIMARY}; + font-size: 8pt; + font-weight: 700; + padding: 0 {sp.SPACE_2}px; + border-radius: 8px; + background-color: transparent; + }} + """ + ) + # Focus dot: visible only when character has window focus + from PySide6.QtGui import QPixmap + pix = QPixmap(self._focus_dot.size()) + pix.fill(Qt.GlobalColor.transparent) + p = QPainter(pix) + try: + color = QColor(ds.BORDER_FOCUS) if self._has_focus else QColor(ds.BORDER_SUBTLE) + color.setAlpha(255 if self._has_focus else 80) + p.setRenderHint(QPainter.RenderHint.Antialiasing) + p.setPen(Qt.PenStyle.NoPen) + p.setBrush(color) + p.drawEllipse(0, 0, 8, 8) + finally: + p.end() + # Wrap pixmap on a label would over-complicate; use a CSS class instead + self._focus_dot.setStyleSheet( + f"background-color: {ds.BORDER_FOCUS if self._has_focus else 'transparent'};" + f"border-radius: 4px;" + ) + + def _update_tooltip(self) -> None: + parts = [self._character_name] + if self._system and not self._stale: + parts.append(f"System: {self._system}") + elif self._last_system: + parts.append(f"System: Unknown (last: {self._last_system})") + if self._threat_level is not None and self._threat_alpha > 0.0: + line = f"Threat: {self._threat_level.value.upper()}" + if self._threat_distance and self._threat_distance > 0: + line += f" ({self._threat_distance}j)" + if self._threat_set_at > 0.0: + secs = int(time.monotonic() - self._threat_set_at) + line += f" · {secs}s ago" + parts.append(line) + parts.append(f"Capture: {self._capture_health.upper()}") + if self._has_focus: + parts.append("● ACTIVE WINDOW") + parts.append("Click: focus window") + parts.append("Right-click: pilot menu") + self.setToolTip("\n".join(parts)) + + def _update_accessible(self) -> None: + parts = [f"Pilot {self._character_name}"] + sys = self._system if self._system and not self._stale else ( + f"unknown last {self._last_system}" if self._last_system else "unknown") + parts.append(f"system {sys}") + parts.append(f"capture {self._capture_health}") + if self._threat_level and self._threat_alpha > 0.0: + parts.append(f"threat {self._threat_level.value}") + if self._has_focus: + parts.append("active focus") + self.setAccessibleName(", ".join(parts)) + + # ---- events ------------------------------------------------------------ + def mousePressEvent(self, event) -> None: + if event.button() == Qt.MouseButton.LeftButton: + self.clicked.emit(self._window_id) + event.accept() + return + if event.button() == Qt.MouseButton.RightButton: + self.context_requested.emit(self._window_id, event.globalPosition().toPoint()) + event.accept() + return + super().mousePressEvent(event) + + def keyPressEvent(self, event) -> None: + if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Space): + self.clicked.emit(self._window_id) + event.accept() + return + if event.key() == Qt.Key.Key_Right: + self.parentWidget().focusNextChild() + event.accept() + return + if event.key() == Qt.Key.Key_Left: + self.parentWidget().focusPreviousChild() + event.accept() + return + super().keyPressEvent(event) + + def paintEvent(self, event) -> None: + super().paintEvent(event) + # Threat accent overlay on the right edge — preserves skill of + # reading identity + state from a glance. + if self._threat_level and self._threat_alpha > 0.0: + from argus_overview.ui.main_tab import THREAT_BORDER_COLORS + rgb = THREAT_BORDER_COLORS.get(self._threat_level, (255, 0, 0)) + p = QPainter(self) + try: + p.setRenderHint(QPainter.RenderHint.Antialiasing) + draw_threat_accent( + p, + self.rect(), + rgb, + alpha=self._threat_alpha, + edge="right", + ribbon_width=2, + glow_height=1, + ) + finally: + p.end() + + +class FleetRail(QWidget): + """Vertical strip of FleetCards. + + Always rendered in the same position with a fixed order. The rail + is the operator's identity surface — pilots are never hidden, + collapsed, or reordered without explicit action. + """ + + pilot_focus_requested = Signal(str) + pilot_context_requested = Signal(str, object) + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._cards: dict[str, FleetCard] = {} + + layout = QVBoxLayout(self) + layout.setContentsMargins(sp.SPACE_3, sp.SPACE_4, sp.SPACE_3, sp.SPACE_4) + layout.setSpacing(sp.SPACE_2) + layout.addStretch(0) # Push cards to top + + # Label the strip + self._label = QLabel("FLEET RAIL", self) + self._label.setObjectName("FleetRailLabel") + f = QFont(self._label.font()) + f.setPointSize(ty.BADGE_TEXT_PT) + f.setWeight(QFont.Weight.Bold) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 180) + self._label.setFont(f) + self._label.setStyleSheet(f"color: {ds.TEXT_MUTED}; padding-bottom: {sp.SPACE_2}px;") + layout.addWidget(self._label) + + self._cards_holder = QWidget(self) + self._cards_layout = QVBoxLayout(self._cards_holder) + self._cards_layout.setContentsMargins(0, 0, 0, 0) + self._cards_layout.setSpacing(sp.SPACE_2) + self._cards_layout.addStretch(1) + layout.addWidget(self._cards_holder, 1) + + # Style the rail itself + self.setObjectName("FleetRail") + self.setStyleSheet( + f""" + QWidget#FleetRail {{ + background-color: {ds.CANVAS}; + border-right: 1px solid {ds.BORDER_SUBTLE}; + }} + QLabel#FleetRailLabel {{ + color: {ds.TEXT_MUTED}; + background: transparent; + }} + """ + ) + + # ---- public API -------------------------------------------------------- + def card_count(self) -> int: + return len(self._cards) + + def card_for(self, window_id: str) -> FleetCard | None: + return self._cards.get(window_id) + + def upsert_card(self, window_id: str, character_name: str, accent: tuple[int, int, int]) -> FleetCard: + if window_id in self._cards: + return self._cards[window_id] + card = FleetCard(window_id, character_name, accent, parent=self._cards_holder) + card.clicked.connect(self.pilot_focus_requested.emit) + card.context_requested.connect(self.pilot_context_requested.emit) + # Insert before the trailing stretch (last item) + insert_at = max(0, self._cards_layout.count() - 1) + self._cards_layout.insertWidget(insert_at, card) + self._cards[window_id] = card + return card + + def remove_card(self, window_id: str) -> bool: + card = self._cards.pop(window_id, None) + if card is None: + return False + self._cards_layout.removeWidget(card) + card.deleteLater() + return True + + def clear(self) -> None: + for window_id in list(self._cards.keys()): + self.remove_card(window_id) + + def card_order(self) -> list[str]: + return list(self._cards.keys()) + + def set_pilot_system(self, window_id: str, system: str | None, *, stale: bool = False) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_system(system, stale=stale) + return True + + def set_pilot_capture_health(self, window_id: str, health: str) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_capture_health(health) + return True + + def set_pilot_focused(self, window_id: str, focused: bool) -> None: + for wid, card in self._cards.items(): + card.set_focused(wid == window_id and focused) + + def set_pilot_threat( + self, + window_id: str, + level: ThreatLevel | None, + system: str | None = None, + alpha: float = 1.0, + distance: int | None = None, + ) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_threat_state(level, system, alpha=alpha, distance=distance) + return True diff --git a/src/argus_overview/ui/command/header.py b/src/argus_overview/ui/command/header.py new file mode 100644 index 0000000..061e34f --- /dev/null +++ b/src/argus_overview/ui/command/header.py @@ -0,0 +1,326 @@ +"""Argus Command Center — Header chrome. + +The header is the identity moment. It establishes the brand ("ARGUS") as +a deliberate piece of operating-environment typography, not a window +title. Subtitle carries the operational state summary so the operator +never has to scan for context. + +Design intent: + * Banner-style brand mark: "ARGUS" in heavy weight, smaller "// 0PS" + tagline, version pinned right. + * Live status line: fleet count, alert count, intel pipeline health. + * Right-side cluster: command palette hint and global action buttons. + * Bottom 1px focus line in BORDER_FOCUS to anchor the eye. +""" +from __future__ import annotations + +from PySide6.QtCore import Property, QEasingCurve, QPropertyAnimation, Qt, QTimer, Signal +from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter, QPen +from PySide6.QtWidgets import QFrame, QHBoxLayout, QPushButton, QWidget + +from argus_overview.ui.design_system import colors as ds +from argus_overview.ui.design_system import metrics as dm +from argus_overview.ui.design_system import spacing as sp +from argus_overview.ui.design_system import typography as ty + + +class BrandMark(QWidget): + """Heavy-weight brand block: 'ARGUS' with // 0PS tagline. + + Painted (not a label) so type rendering is consistent across themes + and DPI scales, and so we can layer the cursor/underline accent. + """ + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setFixedHeight(48) + self.setFixedWidth(180) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) + + def sizeHint(self): # noqa: D401 + from PySide6.QtCore import QSize + return QSize(180, 48) + + def paintEvent(self, event) -> None: # noqa: ARG002 + p = QPainter(self) + try: + p.setRenderHint(QPainter.RenderHint.Antialiasing) + p.setRenderHint(QPainter.RenderHint.TextAntialiasing) + r = self.rect() + # Brand word + f = QFont(self.font()) + f.setPointSize(ty.WINDOW_TITLE_PT + 6) + f.setWeight(QFont.Weight.Black) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 105) + p.setFont(f) + p.setPen(QColor(ds.TEXT_PRIMARY)) + fm = QFontMetrics(f) + brand_y = (r.height() + fm.ascent() - fm.descent()) // 2 + p.drawText(r.left(), brand_y, "ARGUS") + # "//" divider in muted color + divider_w = fm.horizontalAdvance("ARGUS") + f2 = QFont(self.font()) + f2.setPointSize(ty.WINDOW_TITLE_PT + 2) + f2.setWeight(QFont.Weight.Light) + p.setFont(f2) + fm2 = QFontMetrics(f2) + div_x = r.left() + divider_w + sp.SPACE_2 + p.setPen(QColor(ds.TEXT_MUTED)) + p.drawText(div_x, brand_y, "//") + # OPS in accent (0 = signal-amber tone, OPS = operational) + tag_x = div_x + fm2.horizontalAdvance("//") + sp.SPACE_1 + f3 = QFont(self.font()) + f3.setPointSize(ty.WINDOW_TITLE_PT + 2) + f3.setWeight(QFont.Weight.Bold) + f3.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 140) + p.setFont(f3) + fm3 = QFontMetrics(f3) + p.setPen(QColor(ds.BORDER_FOCUS)) + p.drawText(tag_x, brand_y, "OPS") + # Underline accent rule below the brand + accent_x = r.left() + accent_w = tag_x + fm3.horizontalAdvance("OPS") - r.left() + accent_y = r.height() - 1 + p.fillRect(accent_x, accent_y, accent_w, 2, QColor(ds.BORDER_FOCUS)) + finally: + p.end() + + +class OperationalStatusLine(QWidget): + """Animated status line — fleet count, alert count, intel health. + + When alerts are present the line gains a 1px danger underline and + the alert counter pulses gently so the operator feels the priority + without staring at the screen. + """ + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._fleet_count: int = 0 + self._alert_count: int = 0 + self._intel_health: str = "idle" # idle | live | degraded | offline + self._pulse_strength: float = 0.0 + self._pulse_anim = QPropertyAnimation(self, b"pulse_strength") + self._pulse_anim.setDuration(900) + self._pulse_anim.setStartValue(1.0) + self._pulse_anim.setEndValue(0.2) + self._pulse_anim.setEasingCurve(QEasingCurve.Type.InOutQuad) + self._pulse_timer = QTimer(self) + self._pulse_timer.setInterval(2000) + self._pulse_timer.timeout.connect(self._restart_pulse) + self.setFixedHeight(28) + self.setMinimumWidth(280) + + def _get_pulse_strength(self) -> float: + return self._pulse_strength + + def _set_pulse_strength(self, v: float) -> None: + self._pulse_strength = v + self.update() + + pulse_strength = Property(float, _get_pulse_strength, _set_pulse_strength) + + def _restart_pulse(self) -> None: + if self._alert_count > 0: + self._pulse_anim.start() + + def update_state( + self, + fleet_count: int, + alert_count: int = 0, + intel_health: str = "live", + ) -> None: + self._fleet_count = fleet_count + self._alert_count = alert_count + self._intel_health = intel_health + if alert_count > 0 and not self._pulse_timer.isActive(): + self._pulse_timer.start() + self._pulse_anim.start() + elif alert_count == 0: + self._pulse_timer.stop() + self._pulse_strength = 0.0 + self.update() + + def paintEvent(self, event) -> None: # noqa: ARG002 + p = QPainter(self) + try: + p.setRenderHint(QPainter.RenderHint.TextAntialiasing) + r = self.rect() + y = (r.height() + QFontMetrics(self.font()).ascent() + - QFontMetrics(self.font()).descent()) // 2 + + segments = [] + # Fleet count — word-bound pluralization + pilot_word = "PILOT" if self._fleet_count == 1 else "PILOTS" + segments.append((ds.TEXT_SECONDARY, f"{self._fleet_count} {pilot_word}")) + segments.append((ds.TEXT_MUTED, "·")) + # Intel health + intel_color = { + "live": ds.HEALTHY, "idle": ds.UNKNOWN, "degraded": ds.WARNING, + "offline": ds.CRITICAL, + }.get(self._intel_health, ds.UNKNOWN) + intel_label = self._intel_health.upper() + segments.append((intel_color, "●")) + segments.append((ds.TEXT_SECONDARY, f"INTEL {intel_label}")) + # Alert count + segments.append((ds.TEXT_MUTED, "·")) + pulse_alpha = int(255 * self._pulse_strength) + if self._alert_count > 0: + alert_word = "ALERT" if self._alert_count == 1 else "ALERTS" + segments.append(("alert", f" {self._alert_count} {alert_word}")) + x = 0 + f = QFont(self.font()) + f.setPointSize(ty.PRIMARY_LABEL_PT) + f.setWeight(QFont.Weight.Medium) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 110) + p.setFont(f) + fm = QFontMetrics(f) + for color, text in segments: + if color == "alert": + # Blend CRITICAL with current pulse alpha + c = QColor(ds.CRITICAL) + c.setAlpha(pulse_alpha) + p.setPen(QPen(c)) + else: + p.setPen(QPen(QColor(color))) + p.drawText(x, y, text) + x += fm.horizontalAdvance(text) + sp.SPACE_2 + # Bottom pulse rule + if self._alert_count > 0: + rule = QColor(ds.CRITICAL) + rule.setAlpha(int(180 * self._pulse_strength)) + p.fillRect(0, self.height() - 1, int(x * 0.6), 1, rule) + finally: + p.end() + + +class CommandCenterHeader(QWidget): + """Top chrome of the Command Center tab. + + Composition (left to right): + [ Brand Mark ] [ Status Line ] spacer [ Layout chooser ] [ Palette hint ] + """ + + layout_chooser_clicked = Signal() + palette_hint_activated = Signal() + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setFixedHeight(dm.CONTROL_HEIGHT_LARGE + 22) + self._build_ui() + + def _build_ui(self) -> None: + layout = QHBoxLayout(self) + layout.setContentsMargins(sp.SPACE_5, sp.SPACE_3, sp.SPACE_5, sp.SPACE_3) + layout.setSpacing(sp.SPACE_4) + + # Brand — fixed-width container ensures the status line has space + self._brand = BrandMark(self) + self._brand.setFixedWidth(180) + layout.addWidget(self._brand) + + # Vertical separator + sep = QFrame(self) + sep.setFrameShape(QFrame.Shape.VLine) + sep.setFrameShadow(QFrame.Shadow.Plain) + sep.setStyleSheet(f"color: {ds.BORDER_SUBTLE};") + sep.setFixedHeight(28) + layout.addWidget(sep) + + # Operational status — use a fixed-width container so it never + # competes with the brand for layout space, but the inner widget + # uses ellipsis if it ever overflows. + self._status = OperationalStatusLine(self) + self._status.setMinimumWidth(220) + self._status.setMaximumWidth(420) + layout.addWidget(self._status, 1) + + # Spacer pushes right cluster to edge + layout.addStretch(1) + + # Layout chooser button (Tier 2 action becomes 1-click from Command) + self._layout_btn = QPushButton("Layout ▾", self) + self._layout_btn.setObjectName("CommandLayoutChooser") + self._layout_btn.setFixedHeight(dm.CONTROL_HEIGHT) + self._layout_btn.setMinimumWidth(120) + self._layout_btn.setCursor(Qt.CursorShape.PointingHandCursor) + self._layout_btn.clicked.connect(self.layout_chooser_clicked.emit) + self._apply_button_style(self._layout_btn) + layout.addWidget(self._layout_btn) + + # Palette hint (cmd+k) + self._palette_btn = QPushButton("⌘ K", self) + self._palette_btn.setObjectName("CommandPaletteHint") + self._palette_btn.setFixedHeight(dm.CONTROL_HEIGHT) + self._palette_btn.setFixedWidth(72) + self._palette_btn.setCursor(Qt.CursorShape.PointingHandCursor) + self._palette_btn.clicked.connect(self.palette_hint_activated.emit) + self._apply_button_style(self._palette_btn, secondary=True) + layout.addWidget(self._palette_btn) + + # Drop shadow at bottom to anchor the eye + self._paint_focus_line() + + def _apply_button_style(self, btn: QPushButton, *, secondary: bool = False) -> None: + if secondary: + btn.setStyleSheet( + f""" + QPushButton {{ + background-color: transparent; + color: {ds.TEXT_SECONDARY}; + border: 1px solid {ds.BORDER_SUBTLE}; + border-radius: {dm.RADIUS_CONTROL}px; + padding: 0 {sp.SPACE_4}px; + font-size: {ty.PRIMARY_LABEL_PT}pt; + font-weight: 600; + letter-spacing: 110%; + }} + QPushButton:hover {{ + background-color: {ds.SURFACE_RAISED}; + color: {ds.TEXT_PRIMARY}; + border-color: {ds.BORDER_STRONG}; + }} + QPushButton:pressed {{ + background-color: {ds.SURFACE_HOVER}; + }} + """ + ) + else: + btn.setStyleSheet( + f""" + QPushButton {{ + background-color: {ds.SURFACE_RAISED}; + color: {ds.TEXT_PRIMARY}; + border: 1px solid {ds.BORDER_STRONG}; + border-radius: {dm.RADIUS_CONTROL}px; + padding: 0 {sp.SPACE_4}px; + font-size: {ty.PRIMARY_LABEL_PT}pt; + font-weight: 700; + letter-spacing: 110%; + }} + QPushButton:hover {{ + background-color: {ds.SURFACE_HOVER}; + border-color: {ds.BORDER_FOCUS}; + }} + QPushButton:pressed {{ + background-color: {ds.SURFACE}; + }} + """ + ) + + def _paint_focus_line(self) -> None: + """1px focus line at the very bottom to anchor the header.""" + original = self.paintEvent + def paintEvent(event): # noqa: ARG001 + original(event) + p = QPainter(self) + try: + p.fillRect(0, self.height() - 1, self.width(), 1, QColor(ds.BORDER_FOCUS)) + finally: + p.end() + self.paintEvent = paintEvent + + def update_state( + self, fleet_count: int, alert_count: int = 0, intel_health: str = "live" + ) -> None: + self._status.update_state(fleet_count, alert_count, intel_health) diff --git a/src/argus_overview/ui/command/integration.py b/src/argus_overview/ui/command/integration.py new file mode 100644 index 0000000..68e4d70 --- /dev/null +++ b/src/argus_overview/ui/command/integration.py @@ -0,0 +1,358 @@ +"""Integration helpers — connect CommandCenter widgets to existing +MainWindow signals and data sources. + +This module is intentionally additive: it provides wiring helpers +without modifying MainWindowV21. Existing call sites can register an +integration via: + + from argus_overview.ui.command.integration import CommandIntegrator + + integrator = CommandIntegrator(window) + integrator.attach() + +Where ``window`` is any object that exposes the same signals and +properties as MainWindowV21. +""" +from __future__ import annotations + +import logging +import time +from typing import Any + +from PySide6.QtCore import QObject, Qt, QTimer, Slot +from PySide6.QtGui import QKeySequence, QShortcut +from PySide6.QtWidgets import QApplication + +from argus_overview.ui.command.attention import AttentionItem, OpsEntry +from argus_overview.ui.command.operational_truth import OperationalTruthBar +from argus_overview.ui.command.palette import CommandPalette, PaletteEntry +from argus_overview.ui.command.shell import CommandCenterWidget + + +class CommandIntegrator(QObject): + """Wires a CommandCenterWidget to an existing MainWindow. + + The integrator calls the documented contract surface on the + ``window`` argument — see :class:`MainWindowV21` for the canonical + methods (``activate_window``, ``show_layout_chooser``, ``theme_manager``, + ``layout_manager``, ``auto_discovery``, ``system_status_bar``). The + integrator is permissive *only* at the boundary so window-less + command-center previews don't crash; signal-path calls go through + the real methods. + """ + + def __init__(self, window: Any, parent: QObject | None = None) -> None: + super().__init__(parent) + self._window = window + self._logger = logging.getLogger(__name__) + self._command: CommandCenterWidget | None = None + self._palette: CommandPalette | None = None + self._shortcut: QShortcut | None = None + self._alert_count: int = 0 + self._last_attention_threats: dict[str, float] = {} + + # ---- attach ----------------------------------------------------------- + def attach(self) -> CommandCenterWidget: + """Create the CommandCenter, return its widget. Idempotent.""" + if self._command is not None: + return self._command + self._command = CommandCenterWidget() + self._wire_header() + self._wire_fleet_rail() + self._install_palette_shortcut() + self._seed_subsystem_health() + self._install_polling() + # Seed alert count + header state from existing UI + self._refresh_command_state() + return self._command + + def command(self) -> CommandCenterWidget | None: + return self._command + + def palette(self) -> CommandPalette | None: + return self._palette + + # ---- header wiring ----------------------------------------------------- + def _wire_header(self) -> None: + if not self._command: + return + self._command.layout_chooser_requested.connect(self._open_layout_chooser) + self._command.palette_requested.connect(self._open_palette) + + @Slot() + def _open_layout_chooser(self) -> None: + # Defer to existing main window's layout chooser if available. + for attr in ("show_layout_chooser", "_show_layout_chooser"): + if hasattr(self._window, attr): + getattr(self._window, attr)() + self._record_ops("layout", "Layout chooser opened") + return + self._logger.info( + "Layout chooser requested but MainWindow has no show_layout_chooser" + ) + + @Slot() + def _open_palette(self) -> None: + if self._palette is None: + self._palette = CommandPalette(self._window) + self._wire_palette_entries() + rect = self._window.geometry() + self._palette.open_over(rect) + + # ---- palette ------------------------------------------------------------ + def _install_palette_shortcut(self) -> None: + sc = QShortcut(self._window) + sc.setKey(QKeySequence("Ctrl+K")) + sc.setContext(Qt.ShortcutContext.ApplicationShortcut) + sc.activated.connect(self._open_palette) + self._shortcut = sc + + def _wire_palette_entries(self) -> None: + if not self._palette: + return + entries: list[PaletteEntry] = [] + + def make_focus_entry(wid: str, name: str) -> PaletteEntry: + def _handler(): + # Pivot through main_window's chip click for symmetry. + if hasattr(self._window, "_on_chip_clicked"): + self._window._on_chip_clicked(wid) + self._record_ops("pilot", f"Focused {name}", pilot=name) + elif hasattr(self._window, "focus_window"): + self._window.focus_window(wid) + self._open_palette() # ensure palette close; entry already close on accept + return PaletteEntry( + id=f"focus::{wid}", + title=f"Focus {name}", + subtitle="Bring this client's window to front", + category="pilot", + keywords=("focus", "window", "client", name.lower()), + handler=_handler, + ) + + rail = self._command.fleet_rail() if self._command else None + if rail: + for wid in rail.card_order(): + card = rail.card_for(wid) + if card: + entries.append(make_focus_entry(wid, card.character_name())) + + # Layout preset entries (read-only summaries) + entries.append( + PaletteEntry( + id="system::refresh", + title="Refresh window list", + subtitle="Rescan for EVE clients and rebuild previews", + category="system", + keywords=("refresh", "scan", "discover"), + handler=lambda: ( + getattr(self._window, "auto_discovery", None) + and self._window.auto_discovery.run_once() + ), + ) + ) + entries.append( + PaletteEntry( + id="system::lock", + title="Lock windows", + subtitle="Prevent EVE windows from being moved by Argus", + category="action", + keywords=("lock", "stop moving"), + handler=lambda: ( + hasattr(self._window, "layout_manager") + and self._window.layout_manager.set_locked(True) + ), + ) + ) + entries.append( + PaletteEntry( + id="system::unlock", + title="Unlock windows", + subtitle="Allow layout operations to move EVE windows", + category="action", + keywords=("unlock", "move", "layout"), + handler=lambda: ( + hasattr(self._window, "layout_manager") + and self._window.layout_manager.set_locked(False) + ), + ) + ) + # Theme switching + for theme in ("dark", "light", "eve", "high_contrast"): + entries.append( + PaletteEntry( + id=f"theme::{theme}", + title=f"Theme: {theme.replace('_', ' ').title()}", + subtitle="Switch Argus appearance theme", + category="theme", + keywords=("theme", "appearance", "color", theme), + handler=lambda t=theme: self._apply_theme(t), + ) + ) + self._palette.set_entries(entries) + + def _apply_theme(self, theme: str) -> None: + mgr = getattr(self._window, "theme_manager", None) + if mgr and hasattr(mgr, "apply_theme"): + try: + # Real signature is apply_theme(name, app=None) — pass the + # running QApplication instance so stylesheet propagation + # cascades to every existing widget. + mgr.apply_theme(theme, QApplication.instance()) + self._record_ops("system", f"Theme switched to {theme}") + except (RuntimeError, ValueError, TypeError) as exc: + self._logger.warning("Theme switch failed: %s", exc) + + # ---- fleet rail wiring ------------------------------------------------- + def _wire_fleet_rail(self) -> None: + if not self._command: + return + self._command.pilot_focus_requested.connect(self._on_pilot_focus) + self._command.pilot_context_requested.connect(self._on_pilot_context) + self._command.grid_holder().pilot_focus_requested.connect( + self._on_pilot_focus + ) + self._command.grid_holder().pilot_context_requested.connect( + self._on_pilot_context + ) + # Mirror existing main tab characters into the rail on demand + self._mirror_main_tab_characters() + + def _mirror_main_tab_characters(self) -> None: + """Read the main_tab preview list and create FleetCards + TacticalCards.""" + try: + main_tab = getattr(self._window, "main_tab", None) + if not main_tab or not self._command: + return + rail = self._command.fleet_rail() + grid = self._command.grid_holder() + wm = getattr(main_tab, "window_manager", None) + # Pre-existing helpers vary across versions. Defensive walk. + if wm and hasattr(wm, "known_windows"): + from argus_overview.ui.main_tab import character_accent_color + for wid, display_name in wm.known_windows().items(): + accent_color = character_accent_color(display_name) + accent = ( + accent_color.red(), + accent_color.green(), + accent_color.blue(), + ) + if not rail.card_for(wid): + rail.upsert_card(wid, display_name, accent) + if not grid.card_for(wid): + grid.upsert_card(wid, display_name, accent) + except (AttributeError, RuntimeError) as exc: + self._logger.debug("Character mirror skipped: %s", exc) + + @Slot(str) + def _on_pilot_focus(self, window_id: str) -> None: + rail = self._command.fleet_rail() if self._command else None + name = window_id + if rail: + card = rail.card_for(window_id) + if card: + name = card.character_name() + if hasattr(self._window, "activate_window"): + try: + self._window.activate_window(window_id) + self._record_ops("pilot", f"Focused {name}", pilot=name) + return + except (TypeError, RuntimeError) as exc: + self._logger.debug("activate_window(%s) failed: %s", window_id, exc) + # Fallback: at minimum, set the rail's focus state for visual feedback + if self._command: + self._command.fleet_rail().set_pilot_focused(window_id, True) + + @Slot(str, object) + def _on_pilot_context(self, window_id: str, global_pos: object) -> None: + # Forward to main_tab context menu if available + main_tab = getattr(self._window, "main_tab", None) + if main_tab: + for attr in ("show_window_context_menu", "_on_window_context_menu"): + if hasattr(main_tab, attr): + try: + getattr(main_tab, attr)(window_id, global_pos) + return + except (TypeError, RuntimeError): + pass + + # ---- operational truth bar ------------------------------------------- + def _seed_subsystem_health(self) -> None: + if not self._command: + return + bar: OperationalTruthBar = self._command.truth() + # Forward existing system status bar state where available + existing = getattr(self._window, "system_status_bar", None) + if existing and hasattr(existing, "_status"): + for key, status in existing._status.items(): + detail = existing._detail.get(key, "") + bar.set_subsystem(key, status, detail) + else: + bar.set_subsystem("capture", "healthy", "capture workers running") + bar.set_subsystem("hotkeys", "healthy", "hotkey listener active") + bar.set_subsystem("discovery", "healthy", "auto-discovery idle") + bar.set_subsystem("intel", "healthy", "intel pipeline active") + bar.set_subsystem("location", "healthy", "location tracker active") + + # ---- polling ---------------------------------------------------------- + def _install_polling(self) -> None: + # 1-second timer for header alert count + ops timestamp updates. + self._poll = QTimer(self) + self._poll.setInterval(1000) + self._poll.timeout.connect(self._refresh_command_state) + self._poll.start() + + def _refresh_command_state(self) -> None: + if not self._command: + return + rail = self._command.fleet_rail() + header = self._command.header() + truth = self._command.truth() + grid = self._command.grid_holder() + + fleet_count = rail.card_count() + active_threats = sum( + 1 for wid in rail.card_order() + if getattr(rail.card_for(wid), "_threat_level", None) + and getattr(rail.card_for(wid), "_threat_alpha", 0.0) > 0.0 + ) + self._alert_count = active_threats + header.update_state(fleet_count=fleet_count, alert_count=active_threats, + intel_health="live") + truth.set_alert_count(active_threats) + # Refresh age labels on TacticalCards so the operator sees + # freshness at a glance during long sessions. + grid.tick_all() + + # ---- ops timeline ---------------------------------------------------- + def _record_ops(self, category: str, label: str, detail: str = "", + pilot: str | None = None) -> None: + if not self._command: + return + entry = OpsEntry(timestamp=time.time(), label=label, + detail=detail, pilot=pilot, category=category) + self._command.ops_timeline().add_entry(entry) + + def record_ops(self, category: str, label: str, detail: str = "", + pilot: str | None = None) -> None: + """Public method for callers to log operational events.""" + self._record_ops(category, label, detail=detail, pilot=pilot) + + # ---- attention queue -------------------------------------------------- + def surface_attention(self, category: str, title: str, *, + detail: str = "", + pilot: str | None = None, + system: str | None = None, + severity: str = "info") -> None: + if not self._command: + return + item = AttentionItem( + id=f"{category}::{title}::{time.monotonic()}", + category=category, + title=title, + detail=detail, + pilot=pilot, + system=system, + severity=severity, + ) + self._command.attention().add_item(item) diff --git a/src/argus_overview/ui/command/operational_truth.py b/src/argus_overview/ui/command/operational_truth.py new file mode 100644 index 0000000..5d8a94b --- /dev/null +++ b/src/argus_overview/ui/command/operational_truth.py @@ -0,0 +1,208 @@ +"""Argus Command Center — Operational Truth bar (footer). + +Replaces the generic system status bar with a stylized, semantically +labeled strip. Carries: + * Subsystem health (capture / hotkeys / discovery / intel / location) + * Active alert counter (pulsing) + * Layout applied + last applied timestamp + * Theme + version pinned right +""" +from __future__ import annotations + +from PySide6.QtCore import Property, QPropertyAnimation, Qt, Signal +from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter, QPen +from PySide6.QtWidgets import QHBoxLayout, QLabel, QWidget + +from argus_overview.ui.design_system import colors as ds +from argus_overview.ui.design_system import spacing as sp +from argus_overview.ui.design_system import typography as ty + +_HEALTH_COLOR = { + "healthy": ds.HEALTHY, + "degraded": ds.WARNING, + "unavailable": ds.CRITICAL, + "unknown": ds.UNKNOWN, +} + +_SUBSYSTEM_LABEL = { + "capture": "CAPTURE", + "hotkeys": "HOTKEYS", + "discovery": "DISCOVERY", + "intel": "INTEL", + "location": "LOCATION", + "layout": "LAYOUT", +} + + +class _SubsystemCell(QWidget): + """A single subsystem health cell with a colored dot and label.""" + + def __init__(self, key: str, label: str, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._key = key + self._label = label + self._status = "unknown" + self._detail = "" + self.setFixedHeight(22) + self.setMinimumWidth(86) + self.setToolTip(f"{label}: unknown") + + def set_status(self, status: str, detail: str = "") -> None: + self._status = status + self._detail = detail + lines = [f"{self._label}: {status.upper()}"] + if detail: + lines.append(detail) + self.setToolTip("\n".join(lines)) + self.update() + + def paintEvent(self, event) -> None: # noqa: ARG002 + p = QPainter(self) + try: + p.setRenderHint(QPainter.RenderHint.Antialiasing) + p.setRenderHint(QPainter.RenderHint.TextAntialiasing) + r = self.rect() + color = _HEALTH_COLOR.get(self._status, ds.UNKNOWN) + f = QFont(self.font()) + f.setPointSize(ty.BADGE_TEXT_PT + 1) + f.setWeight(QFont.Weight.Bold) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 160) + p.setFont(f) + fm = QFontMetrics(f) + text = self._label + dot_r = 3 + spacing = sp.SPACE_2 + x = 0 + cy = r.height() // 2 + p.setPen(Qt.PenStyle.NoPen) + p.setBrush(QColor(color)) + p.drawEllipse(x, cy - dot_r, dot_r * 2, dot_r * 2) + x += dot_r * 2 + spacing + p.setPen(QPen(QColor(ds.TEXT_SECONDARY))) + p.drawText(x, cy + fm.ascent() // 2 - 1, text) + finally: + p.end() + + +class OperationalTruthBar(QWidget): + """Bottom footer of the Command Center. + + Composition (left to right): + [ subsystem cells ] | [ alert pulse ] | [ layout state ] | [ version ] + """ + + layout_chooser_clicked = Signal() + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setFixedHeight(30) + self.setObjectName("OperationalTruthBar") + + layout = QHBoxLayout(self) + layout.setContentsMargins(sp.SPACE_4, sp.SPACE_2, sp.SPACE_4, sp.SPACE_2) + layout.setSpacing(sp.SPACE_5) + + self._cells: dict[str, _SubsystemCell] = {} + for key in ("capture", "hotkeys", "discovery", "intel", "location"): + label = _SUBSYSTEM_LABEL[key] + cell = _SubsystemCell(key, label) + cell.set_status("unknown") + layout.addWidget(cell) + self._cells[key] = cell + + layout.addStretch(1) + + # Alert pulse cell + self._alert_cell = QWidget(self) + self._alert_cell.setFixedHeight(22) + a_layout = QHBoxLayout(self._alert_cell) + a_layout.setContentsMargins(0, 0, 0, 0) + a_layout.setSpacing(sp.SPACE_2) + self._alert_dot = QLabel("●", self._alert_cell) + self._alert_dot.setStyleSheet(f"color: {ds.HEALTHY}; font-size: 12pt;") + self._alert_text = QLabel("0 ALERTS", self._alert_cell) + f = QFont(self._alert_text.font()) + f.setPointSize(ty.BADGE_TEXT_PT + 1) + f.setWeight(QFont.Weight.Bold) + f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 160) + self._alert_text.setFont(f) + self._alert_text.setStyleSheet(f"color: {ds.TEXT_SECONDARY};") + a_layout.addWidget(self._alert_dot) + a_layout.addWidget(self._alert_text) + layout.addWidget(self._alert_cell) + + # Layout state cell + self._layout_cell = QLabel("·", self) + self._layout_cell.setStyleSheet( + f"color: {ds.TEXT_SECONDARY}; font-size: 9pt; font-weight: 600;" + ) + layout.addWidget(self._layout_cell) + + # Version cell + self._version_cell = QLabel("ARGUS // v3.3 OPS", self) + self._version_cell.setStyleSheet( + f"color: {ds.TEXT_MUTED}; font-size: 8pt;" + f" letter-spacing: 180%; font-weight: 600;" + ) + layout.addWidget(self._version_cell) + + # Pulse animation for alert dot + self._pulse = 0.0 + self._pulse_anim = QPropertyAnimation(self, b"pulse") + self._pulse_anim.setDuration(1100) + self._pulse_anim.setStartValue(1.0) + self._pulse_anim.setEndValue(0.35) + from PySide6.QtCore import QEasingCurve + self._pulse_anim.setEasingCurve(QEasingCurve.Type.InOutQuad) + + # Top hairline + original = self.paintEvent + def _paint(ev): # noqa: ARG001 + original(ev) + pp = QPainter(self) + try: + pp.fillRect(0, 0, self.width(), 1, QColor(ds.BORDER_SUBTLE)) + finally: + pp.end() + self.paintEvent = _paint + + # ---- properties -------------------------------------------------------- + def _get_pulse(self) -> float: + return self._pulse + + def _set_pulse(self, v: float) -> None: + self._pulse = v + if self._alert_text.text().startswith("0"): + return + # Pulse the dot by ramping alpha + self._alert_dot.setStyleSheet(f"color: rgba(240, 100, 100, {v}); font-size: 12pt;") + + pulse = Property(float, _get_pulse, _set_pulse) + + # ---- API --------------------------------------------------------------- + def set_subsystem(self, key: str, status: str, detail: str = "") -> None: + cell = self._cells.get(key) + if cell is None: + return + cell.set_status(status, detail) + + def set_alert_count(self, count: int) -> None: + if count <= 0: + self._pulse_anim.stop() + self._alert_cell.hide() + else: + self._alert_cell.show() + self._pulse_anim.start() + plural = "S" if count != 1 else "" + self._alert_text.setText(f"{count} ALERT{plural}") + self._alert_text.setStyleSheet(f"color: {ds.CRITICAL};") + + def set_layout_state(self, label: str | None, *, applied_at: float | None = None) -> None: + if not label: + self._layout_cell.setText("·") + return + import datetime + suffix = "" + if applied_at: + suffix = f" @ {datetime.datetime.fromtimestamp(applied_at).strftime('%H:%M:%S')}" + self._layout_cell.setText(f"LAYOUT {label.upper()}{suffix}") diff --git a/src/argus_overview/ui/command/palette.py b/src/argus_overview/ui/command/palette.py new file mode 100644 index 0000000..26e6f09 --- /dev/null +++ b/src/argus_overview/ui/command/palette.py @@ -0,0 +1,351 @@ +"""Argus Command Center — Command Palette. + +Press ⌘K (Ctrl+K on Linux/Windows) to open. Type to filter across: + * Pilot names (focus window) + * Layout presets (apply) + * Themes (switch) + * Subsystem checks (status) + * Hard actions (lock windows, refresh, save layout) + +The palette is the operator's muscle-memory gateway — every action is +reachable in two keystrokes from anywhere in Argus. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Callable + +from PySide6.QtCore import ( + QEasingCurve, + QEvent, + QPropertyAnimation, + QRect, + Qt, + Signal, +) +from PySide6.QtGui import ( + QFont, + QKeyEvent, +) +from PySide6.QtWidgets import ( + QDialog, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QVBoxLayout, + QWidget, +) + +from argus_overview.ui.design_system import colors as ds +from argus_overview.ui.design_system import metrics as dm +from argus_overview.ui.design_system import spacing as sp + + +@dataclass +class PaletteEntry: + """One entry in the palette — display + filter + handler.""" + id: str + title: str + subtitle: str = "" + category: str = "action" # pilot | layout | theme | system | action + keywords: tuple[str, ...] = field(default_factory=tuple) + handler: Callable[[], None] | None = None + enabled: bool = True + + +class CommandPalette(QDialog): + """Modal palette dialog. + + Opens centered over the main window. Filter narrows as user types. + Enter executes the highlighted entry. Esc closes. Up/Down arrows + move the highlight. + """ + + executed = Signal(str) # entry id + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setObjectName("CommandPalette") + self.setWindowTitle("Argus // Command Palette") + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint + | Qt.WindowType.Dialog + | Qt.WindowType.WindowStaysOnTopHint + ) + self.setModal(True) + self.setFixedSize(720, 460) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + + self._entries: list[PaletteEntry] = [] + self._build_ui() + self._wire_shortcuts() + self._apply_styles() + + # ---- public API -------------------------------------------------------- + def set_entries(self, entries: list[PaletteEntry]) -> None: + self._entries = list(entries) + self._refresh_list("") + + def register(self, entry: PaletteEntry) -> None: + self._entries.append(entry) + + def open_over(self, parent_rect: QRect) -> None: + """Open the palette horizontally centered, vertically anchored + to the upper third of the parent window for visibility. + """ + x = parent_rect.center().x() - self.width() // 2 + y = parent_rect.top() + int(parent_rect.height() * 0.18) + self.move(max(parent_rect.left() + 24, x), max(parent_rect.top() + 24, y)) + self._input.clear() + self._refresh_list("") + self.show() + self.raise_() + self._input.setFocus() + # Subtle scale-in animation for confidence + self._scale_effect() + + # ---- UI construction --------------------------------------------------- + def _build_ui(self) -> None: + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + outer.setSpacing(0) + + # Body + body = QFrame(self) + body.setObjectName("CommandPaletteBody") + body_layout = QVBoxLayout(body) + body_layout.setContentsMargins(0, 0, 0, 0) + body_layout.setSpacing(0) + outer.addWidget(body) + + # Header — input + header = QFrame(body) + header.setObjectName("CommandPaletteHeader") + h_layout = QHBoxLayout(header) + h_layout.setContentsMargins(sp.SPACE_5, sp.SPACE_4, sp.SPACE_4, sp.SPACE_4) + h_layout.setSpacing(sp.SPACE_3) + + glyph = QLabel("⌘", self) + glyph.setObjectName("CommandPaletteGlyph") + g = QFont(glyph.font()) + g.setPointSize(18) + g.setBold(True) + glyph.setFont(g) + h_layout.addWidget(glyph) + + self._input = QLineEdit(header) + self._input.setObjectName("CommandPaletteInput") + self._input.setPlaceholderText("Focus pilot, apply layout, switch theme…") + self._input.textChanged.connect(self._refresh_list) + self._input.returnPressed.connect(self._activate_current) + # Forward arrow keys to the list + self._input.installEventFilter(self) + h_layout.addWidget(self._input, 1) + + body_layout.addWidget(header) + + # Sub-header — categories legend + legend = QFrame(body) + legend.setObjectName("CommandPaletteLegend") + l_layout = QHBoxLayout(legend) + l_layout.setContentsMargins(sp.SPACE_5, sp.SPACE_2, sp.SPACE_4, sp.SPACE_2) + l_layout.setSpacing(sp.SPACE_5) + for label, color in ( + ("PILOT", ds.BORDER_FOCUS), + ("LAYOUT", ds.HEALTHY), + ("THEME", ds.WARNING), + ("SYSTEM", ds.INFO), + ("ACTION", ds.CRITICAL), + ): + chip = QLabel(f"● {label}") + chip.setStyleSheet(f"color: {color}; font-size: 8pt; font-weight: 700; letter-spacing: 140%;") + l_layout.addWidget(chip) + l_layout.addStretch(1) + body_layout.addWidget(legend) + + # List + self._list = QListWidget(body) + self._list.setObjectName("CommandPaletteList") + self._list.itemActivated.connect(self._activate_item) + self._list.itemClicked.connect(self._activate_item) + body_layout.addWidget(self._list, 1) + + # Footer + footer = QFrame(body) + footer.setObjectName("CommandPaletteFooter") + f_layout = QHBoxLayout(footer) + f_layout.setContentsMargins(sp.SPACE_5, sp.SPACE_2, sp.SPACE_4, sp.SPACE_2) + f_layout.setSpacing(sp.SPACE_5) + for label, key in ( + ("↵ EXECUTE", "ENTER"), + ("↑↓ NAVIGATE", "ARROWS"), + ("ESC CLOSE", "ESC"), + ): + cell = QLabel(f"{label}" + f" {key}") + cell.setTextFormat(Qt.TextFormat.RichText) + f_layout.addWidget(cell) + f_layout.addStretch(1) + body_layout.addWidget(footer) + + def _wire_shortcuts(self) -> None: + pass + # Esc to close handled in keyPressEvent + + def _apply_styles(self) -> None: + self.setStyleSheet( + f""" + QDialog#CommandPalette {{ + background-color: transparent; + }} + QFrame#CommandPaletteBody {{ + background-color: {ds.CANVAS}; + border: 1px solid {ds.BORDER_STRONG}; + border-radius: {dm.RADIUS_PANEL}px; + }} + QFrame#CommandPaletteHeader {{ + background-color: {ds.SURFACE}; + border: none; + border-bottom: 1px solid {ds.BORDER_SUBTLE}; + border-top-left-radius: {dm.RADIUS_PANEL}px; + border-top-right-radius: {dm.RADIUS_PANEL}px; + }} + QLabel#CommandPaletteGlyph {{ + color: {ds.BORDER_FOCUS}; + }} + QLineEdit#CommandPaletteInput {{ + background-color: transparent; + color: {ds.TEXT_PRIMARY}; + border: none; + font-size: 14pt; + font-weight: 500; + }} + QLineEdit#CommandPaletteInput:focus {{ + border: none; + }} + QFrame#CommandPaletteLegend {{ + background-color: transparent; + border-bottom: 1px solid {ds.BORDER_SUBTLE}; + }} + QListWidget#CommandPaletteList {{ + background-color: transparent; + border: none; + outline: 0; + }} + QListWidget#CommandPaletteList::item {{ + color: {ds.TEXT_PRIMARY}; + padding: 12px 24px; + border-bottom: 1px solid {ds.SURFACE}; + font-size: 11pt; + font-weight: 500; + }} + QListWidget#CommandPaletteList::item:selected {{ + background-color: {ds.SURFACE_RAISED}; + border-left: 3px solid {ds.BORDER_FOCUS}; + color: {ds.TEXT_PRIMARY}; + }} + QFrame#CommandPaletteFooter {{ + background-color: transparent; + border-top: 1px solid {ds.BORDER_SUBTLE}; + }} + """ + ) + + # ---- interactions ------------------------------------------------------ + def _refresh_list(self, query: str) -> None: + self._list.clear() + q = query.strip().lower() + # Score: exact start > contains > keyword match + scored = [] + for entry in self._entries: + if not entry.enabled: + continue + score = self._score(entry, q) + if q == "" or score > 0: + scored.append((score, entry)) + scored.sort(key=lambda x: (-x[0], x[1].title.lower())) + if not scored: + placeholder = QListWidgetItem("No matches") + placeholder.setFlags(Qt.ItemFlag.NoItemFlags) + self._list.addItem(placeholder) + return + for _, entry in scored[:30]: + label = QListWidgetItem(f" {entry.title}") + if entry.subtitle: + label.setToolTip(entry.subtitle) + label.setText(f" {entry.title}\n {entry.subtitle}") + label.setData(Qt.ItemDataRole.UserRole, entry.id) + label.setData(Qt.ItemDataRole.UserRole + 1, entry) + self._list.addItem(label) + if self._list.count() > 0: + self._list.setCurrentRow(0) + + def _score(self, entry: PaletteEntry, q: str) -> int: + if not q: + return 1 # show everything in declared order + haystack = " ".join([entry.title.lower(), entry.subtitle.lower(), + entry.category.lower(), *entry.keywords]).strip() + if entry.title.lower().startswith(q): + return 100 + if q in haystack: + return 50 + # Loose word match (each word in q is a prefix of some word in haystack) + words = [w for w in q.split() if w] + if all(any(tok.startswith(w) for tok in haystack.split()) for w in words): + return 25 + return 0 + + def _activate_current(self) -> None: + item = self._list.currentItem() + if item is None: + return + self._activate_item(item) + + def _activate_item(self, item: QListWidgetItem) -> None: + entry = item.data(Qt.ItemDataRole.UserRole + 1) + if not isinstance(entry, PaletteEntry): + return + if entry.handler: + entry.handler() + self.executed.emit(entry.id) + self.accept() + + def _scale_effect(self) -> None: + # Subtle pop-in via window opacity (avoids QGraphicsEffect cost). + original_opacity = self.windowOpacity() + self.setWindowOpacity(0.0) + self._fade = QPropertyAnimation(self, b"windowOpacity") + self._fade.setDuration(120) + self._fade.setStartValue(0.0) + self._fade.setEndValue(original_opacity or 1.0) + self._fade.setEasingCurve(QEasingCurve.Type.OutCubic) + self._fade.start() + + # ---- events ------------------------------------------------------------ + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() == Qt.Key.Key_Escape: + self.reject() + return + if event.key() in (Qt.Key.Key_Down, Qt.Key.Key_Up): + if self._list.hasFocus(): + super().keyPressEvent(event) + return + # Move focus to list first + self._list.setFocus() + super().keyPressEvent(event) + return + if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter: + self._activate_current() + return + super().keyPressEvent(event) + + def eventFilter(self, obj, event: QEvent) -> bool: + if obj is self._input and event.type() == QEvent.Type.KeyPress: + if event.key() in (Qt.Key.Key_Down, Qt.Key.Key_Up): + self._list.setFocus() + self._list.keyPressEvent(event) + return True + return super().eventFilter(obj, event) diff --git a/src/argus_overview/ui/command/shell.py b/src/argus_overview/ui/command/shell.py new file mode 100644 index 0000000..e6d936d --- /dev/null +++ b/src/argus_overview/ui/command/shell.py @@ -0,0 +1,143 @@ +"""Argus Command Center — flagship top-level shell. + +The Command Center is the operator's primary landing surface. It is +not another tab alongside the others — it is the *identity* of Argus. +Other tabs (Layouts, Characters, Hotkeys, Intel, Sync, Settings) +remain; the Command tab is what users open first and what they come +back to. + +Composition (3x3 grid): + + +----------------+--------------------+--------------------+ + | HEADER BAR | | | + +----------------+--------------------+--------------------+ + | | TACTICAL GRID | ATTENTION QUEUE | + | FLEET RAIL | (preview cards) | + OPS TIMELINE | + | (pinned | | | + | identity) | | | + +----------------+--------------------+--------------------+ + | OPERATIONAL TRUTH BAR (health, alerts, layout, version) | + +This module exposes one QWidget: ``CommandCenterWidget``. It is built +to slot into an existing QTabWidget as a top-level tab without +disrupting the other tabs. +""" +from __future__ import annotations + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QGridLayout, QWidget # noqa: F401 — kept for back-compat type hints + +from argus_overview.ui.command.attention import AttentionQueue, OpsTimeline +from argus_overview.ui.command.fleet_rail import FleetRail +from argus_overview.ui.command.header import CommandCenterHeader +from argus_overview.ui.command.operational_truth import OperationalTruthBar +from argus_overview.ui.command.palette import CommandPalette, PaletteEntry +from argus_overview.ui.command.tactical_grid import TacticalGrid +from argus_overview.ui.design_system import colors as ds + + +class CommandCenterWidget(QWidget): + """Flagship Command Center layout.""" + + palette_requested = Signal() + layout_chooser_requested = Signal() + pilot_focus_requested = Signal(str) + pilot_context_requested = Signal(str, object) + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setObjectName("CommandCenter") + + root = QGridLayout(self) + root.setContentsMargins(0, 0, 0, 0) + root.setHorizontalSpacing(0) + root.setVerticalSpacing(0) + + # ROW 1 — Header (spans all three columns) + self._header = CommandCenterHeader(self) + self._header.layout_chooser_clicked.connect(self.layout_chooser_requested.emit) + self._header.palette_hint_activated.connect(self.palette_requested.emit) + root.addWidget(self._header, 0, 0, 1, 3) + root.setRowMinimumHeight(0, self._header.height()) + + # ROW 2 COL 0 — Fleet Rail (left strip) + self._fleet_rail = FleetRail(self) + self._fleet_rail.pilot_focus_requested.connect(self.pilot_focus_requested.emit) + self._fleet_rail.pilot_context_requested.connect(self.pilot_context_requested.emit) + root.addWidget(self._fleet_rail, 1, 0) + root.setColumnMinimumWidth(0, 192) + + # ROW 2 COL 1 — Tactical Grid (center preview/identity host) + self._grid_holder = TacticalGrid(self) + self._grid_holder.setObjectName("TacticalGridHolder") + self._grid_holder.pilot_focus_requested.connect(self.pilot_focus_requested.emit) + self._grid_holder.pilot_context_requested.connect(self.pilot_context_requested.emit) + root.addWidget(self._grid_holder, 1, 1) + root.setColumnStretch(1, 1) + + # ROW 2 COL 2 — Attention Queue + Ops Timeline + from PySide6.QtWidgets import QVBoxLayout + right_panel = QWidget(self) + right_panel.setObjectName("CommandRightPanel") + right_layout = QVBoxLayout(right_panel) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(0) + + self._attention = AttentionQueue(right_panel) + self._ops = OpsTimeline(right_panel) + right_layout.addWidget(self._attention, 1) + right_layout.addWidget(self._ops, 1) + root.addWidget(right_panel, 1, 2) + root.setColumnMinimumWidth(2, 280) + root.setColumnStretch(2, 0) + + # ROW 3 — Operational Truth (spans all three columns) + self._truth = OperationalTruthBar(self) + self._truth.layout_chooser_clicked.connect(self.layout_chooser_requested.emit) + root.addWidget(self._truth, 2, 0, 1, 3) + root.setRowMinimumHeight(2, self._truth.height()) + + # Set stretch so the grid grows + root.setRowStretch(1, 1) + + self.setStyleSheet( + f""" + QWidget#CommandCenter {{ + background-color: {ds.CANVAS}; + }} + QWidget#TacticalGridHolder {{ + background-color: {ds.CANVAS}; + border-left: 1px solid {ds.BORDER_SUBTLE}; + border-right: 1px solid {ds.BORDER_SUBTLE}; + }} + QWidget#CommandRightPanel {{ + background-color: {ds.CANVAS}; + border-left: 1px solid {ds.BORDER_SUBTLE}; + }} + """ + ) + + # ---- accessors --------------------------------------------------------- + def header(self) -> CommandCenterHeader: + return self._header + + def fleet_rail(self) -> FleetRail: + return self._fleet_rail + + def grid_holder(self) -> TacticalGrid: + return self._grid_holder + + def attention(self) -> AttentionQueue: + return self._attention + + def ops_timeline(self) -> OpsTimeline: + return self._ops + + def truth(self) -> OperationalTruthBar: + return self._truth + + def palette(self, entries: list[PaletteEntry] | None = None) -> CommandPalette: + pal = CommandPalette(self.window()) + if entries is not None: + pal.set_entries(entries) + return pal diff --git a/src/argus_overview/ui/command/tactical_grid.py b/src/argus_overview/ui/command/tactical_grid.py new file mode 100644 index 0000000..5e25add --- /dev/null +++ b/src/argus_overview/ui/command/tactical_grid.py @@ -0,0 +1,564 @@ +"""Argus Command Center — Tactical Grid. + +The Tactical Grid is the *preview host* in the center of the Command +tab. It is not a frame-grabber grid (those live in MainTab's scroll +area). It is an **identity / status grid**: each connected EVE client +is represented by a fixed-shape card that surfaces: + + * Accent avatar (deterministic, MD5-derived) + * Pilot name + system + threat state + * Capture health + last-update age + * Click-to-focus affordance + +The grid is fixed-column (3 cols), so layout is predictable at any +window width. Cards do not capture live frames — the operator's +"what does the screen look like" question is still answered by the +Overview tab's ``WindowPreviewWidget``. The grid answers the +complementary question: **"which windows are connected, where are +they, and which are flagged?"** +""" +from __future__ import annotations + +import time + +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QColor, QFont, QPainter +from PySide6.QtWidgets import ( + QFrame, + QGridLayout, + QLabel, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from argus_overview.intel.parser import ThreatLevel +from argus_overview.ui.design_system import colors as ds +from argus_overview.ui.design_system import metrics as dm +from argus_overview.ui.design_system import spacing as sp +from argus_overview.ui.design_system import typography as ty +from argus_overview.ui.design_system.painting import draw_threat_accent + +# Re-use the threat RGB palette from main_tab so visual semantics match. +try: + from argus_overview.ui.main_tab import THREAT_BORDER_COLORS +except ImportError: # pragma: no cover — fallback if main_tab relocated + THREAT_BORDER_COLORS = { + ThreatLevel.CLEAR: (0, 200, 100), + ThreatLevel.INFO: (0, 180, 230), + ThreatLevel.WARNING: (255, 170, 0), + ThreatLevel.DANGER: (255, 90, 30), + ThreatLevel.CRITICAL: (255, 40, 40), + } + + +THREAT_LETTERS = {"danger": "D", "warning": "W", "critical": "C", "info": "I"} + + +def _initials(name: str) -> str: + parts = [p for p in name.replace("_", " ").split() if p] + if not parts: + return "?" + if len(parts) == 1: + return parts[0][:2].upper() + return (parts[0][0] + parts[-1][0]).upper() + + +class TacticalCard(QFrame): + """Wide identity card rendered in the Tactical Grid. + + Unlike ``FleetCard`` (vertical strip, 64px tall), this card is + ~120px tall and shows more at a glance: avatar, name, system, + capture-health pill, last-update timestamp, and a "PREVIEW" + surface placeholder where live frames would mount if/when this + widget hosts them. + """ + + clicked = Signal(str) # window_id + context_requested = Signal(str, object) # window_id, QPoint + + def __init__( + self, + window_id: str, + character_name: str, + accent: tuple[int, int, int], + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self._window_id = window_id + self._character_name = character_name + self._accent = QColor(*accent) + self._system: str | None = None + self._stale: bool = False + self._capture_health: str = "live" + self._last_update: float = time.monotonic() + self._threat_level: ThreatLevel | None = None + self._threat_alpha: float = 0.0 + self._threat_distance: int | None = None + self._has_focus: bool = False + + self.setObjectName(f"TacticalCard::{window_id}") + self.setMinimumSize(220, 120) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.setFixedHeight(120) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.setCursor(Qt.CursorShape.PointingHandCursor) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + + layout = QVBoxLayout(self) + layout.setContentsMargins(sp.SPACE_3, sp.SPACE_3, sp.SPACE_3, sp.SPACE_3) + layout.setSpacing(sp.SPACE_2) + + # Top row — avatar + name + focus indicator + from PySide6.QtWidgets import QHBoxLayout + + top = QWidget(self) + top_layout = QHBoxLayout(top) + top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.setSpacing(sp.SPACE_2) + self._avatar = QLabel(_initials(character_name)) + self._avatar.setObjectName("TacticalAvatar") + self._avatar.setFixedSize(28, 28) + self._avatar.setAlignment(Qt.AlignmentFlag.AlignCenter) + top_layout.addWidget(self._avatar) + + name_block = QWidget(top) + name_layout = QVBoxLayout(name_block) + name_layout.setContentsMargins(0, 0, 0, 0) + name_layout.setSpacing(0) + self._name = QLabel(character_name) + self._name.setObjectName("TacticalName") + f = QFont(self._name.font()) + f.setPointSize(ty.PRIMARY_LABEL_PT) + f.setWeight(QFont.Weight.Bold) + self._name.setFont(f) + name_layout.addWidget(self._name) + self._system_label = QLabel("—") + self._system_label.setObjectName("TacticalSystem") + sf = QFont(self._system_label.font()) + sf.setPointSize(ty.SECONDARY_LABEL_PT) + self._system_label.setFont(sf) + name_layout.addWidget(self._system_label) + top_layout.addWidget(name_block, 1) + + self._threat_chip = QLabel("") + self._threat_chip.setObjectName("TacticalThreatChip") + self._threat_chip.setFixedHeight(20) + self._threat_chip.setMinimumWidth(34) + self._threat_chip.setAlignment(Qt.AlignmentFlag.AlignCenter) + top_layout.addWidget(self._threat_chip) + layout.addWidget(top) + + # Body row — capture health pill + last update + preview surface + body = QWidget(self) + body_layout = QHBoxLayout(body) + body_layout.setContentsMargins(0, 0, 0, 0) + body_layout.setSpacing(sp.SPACE_2) + + self._preview_surface = QWidget(self) + self._preview_surface.setObjectName("TacticalPreviewSurface") + self._preview_surface.setMinimumHeight(36) + body_layout.addWidget(self._preview_surface, 1) + + meta_block = QWidget(body) + meta_layout = QVBoxLayout(meta_block) + meta_layout.setContentsMargins(0, 0, 0, 0) + meta_layout.setSpacing(0) + self._health_label = QLabel("LIVE") + self._health_label.setObjectName("TacticalHealth") + meta_layout.addWidget(self._health_label) + self._age_label = QLabel("now") + self._age_label.setObjectName("TacticalAge") + self._age_label.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter + ) + meta_layout.addWidget(self._age_label) + body_layout.addWidget(meta_block) + layout.addWidget(body, 1) + + self._apply_styles() + self._update_threat_chip() + self._update_tooltip() + + # ---- public API -------------------------------------------------------- + def window_id(self) -> str: + return self._window_id + + def character_name(self) -> str: + return self._character_name + + def set_system(self, system: str | None, *, stale: bool = False) -> None: + self._system = system + self._stale = stale + if system and not stale: + self._update_system_label() + self._update_tooltip() + self._apply_styles() + + def set_capture_health(self, health: str) -> None: + self._capture_health = health + self._health_label.setText(health.upper()) + self._apply_styles() + self._update_tooltip() + + def set_last_update(self, ts: float | None = None) -> None: + self._last_update = ts if ts is not None else time.monotonic() + self._age_label.setText(self._format_age(self._last_update)) + self._update_tooltip() + + def set_focused(self, focused: bool) -> None: + self._has_focus = focused + self._apply_styles() + + def set_threat_state( + self, + level: ThreatLevel | None, + *, + system: str | None = None, + alpha: float = 1.0, + distance: int | None = None, + ) -> None: + if level is None or level == ThreatLevel.CLEAR: + self._threat_level = None + self._threat_alpha = 0.0 + self._threat_distance = None + else: + self._threat_level = level + self._threat_alpha = max(0.0, min(1.0, alpha)) + self._threat_distance = distance if distance and distance > 0 else None + if system is not None: + self.set_system(system) + self._update_threat_chip() + self._update_tooltip() + self.update() + + def tick_age(self) -> None: + """Refresh the age label without state change.""" + self._age_label.setText(self._format_age(self._last_update)) + + # ---- private helpers --------------------------------------------------- + def _update_system_label(self) -> None: + if self._system and not self._stale: + self._system_label.setText(self._system) + else: + self._system_label.setText("Unknown") + + def _update_threat_chip(self) -> None: + if self._threat_level and self._threat_alpha > 0.0: + letter = THREAT_LETTERS.get( + self._threat_level.value.lower(), self._threat_level.value[0].upper() + ) + if self._threat_distance and self._threat_distance > 0: + self._threat_chip.setText(f"{letter}+{self._threat_distance}j") + else: + self._threat_chip.setText(letter) + else: + self._threat_chip.setText("") + + def _format_age(self, ts: float) -> str: + secs = int(max(0, time.monotonic() - ts)) + if secs < 60: + return f"{secs}s" + if secs < 3600: + return f"{secs // 60}m" + return f"{secs // 3600}h" + + def _health_color(self) -> str: + return { + "live": ds.HEALTHY, + "static": ds.UNKNOWN, + "stale": ds.WARNING, + "error": ds.CRITICAL, + "paused": ds.TEXT_MUTED, + }.get(self._capture_health, ds.UNKNOWN) + + def _apply_styles(self) -> None: + accent = self._accent + darker = accent.darker(160) + a_name = accent.name() + d_name = darker.name() + text_color = ( + "#0f0f0f" + if (accent.redF() * 0.299 + accent.greenF() * 0.587 + accent.blueF() * 0.114) + > 0.55 + else "#f5f5f5" + ) + + focus_border = ds.BORDER_FOCUS if self._has_focus else ds.BORDER_SUBTLE + bg = ds.SURFACE if not self._stale else ds.CANVAS + + self.setStyleSheet( + f""" + QFrame#{self.objectName()} {{ + background-color: {bg}; + border: 1px solid {focus_border}; + border-left: 3px solid {a_name}; + border-radius: {dm.RADIUS_CARD}px; + }} + QFrame#{self.objectName()}:hover {{ + border-color: {ds.BORDER_FOCUS}; + background-color: {ds.SURFACE_HOVER}; + }} + QLabel#TacticalAvatar {{ + background-color: {a_name}; + border: 1px solid {d_name}; + border-radius: 14px; + color: {text_color}; + font-weight: 800; + font-size: 10pt; + }} + QLabel#TacticalName {{ + color: {ds.TEXT_PRIMARY}; + }} + QLabel#TacticalSystem {{ + color: {ds.TEXT_MUTED if self._stale else ds.TEXT_SECONDARY}; + }} + QLabel#TacticalThreatChip {{ + color: {ds.TEXT_PRIMARY}; + font-weight: 800; + font-size: 9pt; + background-color: transparent; + padding: 0 {sp.SPACE_2}px; + border-radius: 10px; + }} + QLabel#TacticalHealth {{ + color: {self._health_color()}; + font-weight: 800; + font-size: 9pt; + letter-spacing: 140%; + }} + QLabel#TacticalAge {{ + color: {ds.TEXT_MUTED}; + font-size: 8pt; + font-family: monospace; + }} + QWidget#TacticalPreviewSurface {{ + background-color: {ds.CANVAS}; + border: 1px dashed {ds.BORDER_SUBTLE}; + border-radius: {dm.RADIUS_CARD - 2}px; + }} + """ + ) + + def _update_tooltip(self) -> None: + parts = [f"{self._character_name}"] + if self._system and not self._stale: + parts.append(f"System: {self._system}") + if self._threat_level and self._threat_alpha > 0.0: + line = f"Threat: {self._threat_level.value.upper()}" + if self._threat_distance: + line += f" ({self._threat_distance}j)" + parts.append(line) + parts.append(f"Capture: {self._capture_health.upper()}") + parts.append(f"Last update: {self._format_age(self._last_update)} ago") + if self._has_focus: + parts.append("● ACTIVE WINDOW") + parts.append("Click: focus window") + self.setToolTip("\n".join(parts)) + + # ---- events ------------------------------------------------------------ + def mousePressEvent(self, event) -> None: + if event.button() == Qt.MouseButton.LeftButton: + self.clicked.emit(self._window_id) + event.accept() + return + if event.button() == Qt.MouseButton.RightButton: + self.context_requested.emit( + self._window_id, event.globalPosition().toPoint() + ) + event.accept() + return + super().mousePressEvent(event) + + def keyPressEvent(self, event) -> None: + if event.key() in ( + Qt.Key.Key_Return, + Qt.Key.Key_Enter, + Qt.Key.Key_Space, + ): + self.clicked.emit(self._window_id) + event.accept() + return + super().keyPressEvent(event) + + def paintEvent(self, event) -> None: + super().paintEvent(event) + if self._threat_level and self._threat_alpha > 0.0: + rgb = THREAT_BORDER_COLORS.get(self._threat_level, (255, 0, 0)) + p = QPainter(self) + try: + draw_threat_accent( + p, + self.rect(), + rgb, + alpha=self._threat_alpha, + edge="right", + ribbon_width=3, + glow_height=1, + ) + finally: + p.end() + + +class TacticalGrid(QWidget): + """3-column responsive grid of TacticalCards. + + Columns are fixed at 3 — the grid is wide enough at 1280px to host + 3 columns of ~280px each with comfortable gutter. New cards fill + column-by-column, top-to-bottom. The grid is a *passive* container: + it does not capture frames; it renders identity cards from the + same window manager data the Fleet Rail uses. + """ + + pilot_focus_requested = Signal(str) + pilot_context_requested = Signal(str, object) + + DEFAULT_COLUMNS = 3 + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._cards: dict[str, TacticalCard] = {} + self._columns: int = self.DEFAULT_COLUMNS + + self.setObjectName("TacticalGrid") + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self._grid = QGridLayout(self) + self._grid.setContentsMargins(sp.SPACE_4, sp.SPACE_4, sp.SPACE_4, sp.SPACE_4) + self._grid.setHorizontalSpacing(sp.SPACE_3) + self._grid.setVerticalSpacing(sp.SPACE_3) + + self._render_empty() + + self.setStyleSheet( + f""" + QWidget#TacticalGrid {{ + background-color: {ds.CANVAS}; + }} + QWidget#TacticalEmpty {{ + color: {ds.TEXT_MUTED}; + font-style: italic; + font-size: 10pt; + padding: {sp.SPACE_5}px; + }} + """ + ) + + # ---- public API -------------------------------------------------------- + def card_count(self) -> int: + return len(self._cards) + + def card_for(self, window_id: str) -> TacticalCard | None: + return self._cards.get(window_id) + + def card_order(self) -> list[str]: + return list(self._cards.keys()) + + def upsert_card( + self, + window_id: str, + character_name: str, + accent: tuple[int, int, int], + ) -> TacticalCard: + if window_id in self._cards: + return self._cards[window_id] + card = TacticalCard(window_id, character_name, accent, parent=self) + card.clicked.connect(self.pilot_focus_requested.emit) + card.context_requested.connect(self.pilot_context_requested.emit) + self._cards[window_id] = card + self._relayout() + return card + + def remove_card(self, window_id: str) -> bool: + card = self._cards.pop(window_id, None) + if card is None: + return False + self._grid.removeWidget(card) + card.deleteLater() + self._relayout() + return True + + def clear(self) -> None: + for window_id in list(self._cards.keys()): + self.remove_card(window_id) + + def set_pilot_system(self, window_id: str, system: str | None, *, + stale: bool = False) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_system(system, stale=stale) + return True + + def set_pilot_capture_health(self, window_id: str, health: str) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_capture_health(health) + return True + + def set_pilot_focused(self, window_id: str, focused: bool) -> None: + for wid, card in self._cards.items(): + card.set_focused(wid == window_id and focused) + + def set_pilot_threat(self, window_id: str, level: ThreatLevel | None, *, + system: str | None = None, alpha: float = 1.0, + distance: int | None = None) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_threat_state(level, system=system, alpha=alpha, distance=distance) + return True + + def set_pilot_last_update(self, window_id: str, ts: float) -> bool: + card = self._cards.get(window_id) + if card is None: + return False + card.set_last_update(ts) + return True + + def tick_all(self) -> None: + for card in self._cards.values(): + card.tick_age() + + # ---- layout ------------------------------------------------------------ + def _clear_grid(self) -> None: + # Remove all widgets (including the placeholder) without deleting cards + while self._grid.count(): + item = self._grid.takeAt(0) + w = item.widget() if item else None + if w and w.parent() is self: + self._grid.removeWidget(w) + # Do NOT deleteLater() — cards are owned by self._cards + + def _relayout(self) -> None: + self._clear_grid() + if not self._cards: + self._render_empty() + return + # Remove placeholder if present + placeholder = self._find_placeholder() + if placeholder is not None: + self._grid.removeWidget(placeholder) + placeholder.deleteLater() + # Lay out by column index, row index + for idx, wid in enumerate(self._cards.keys()): + card = self._cards[wid] + col = idx % self._columns + row = idx // self._columns + self._grid.addWidget(card, row, col) + + def _find_placeholder(self) -> QWidget | None: + for i in range(self._grid.count()): + item = self._grid.itemAt(i) + if not item: + continue + w = item.widget() + if w and w.objectName() == "TacticalEmpty": + return w + return None + + def _render_empty(self) -> None: + placeholder = QLabel("Awaiting EVE client connections…", self) + placeholder.setObjectName("TacticalEmpty") + placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter) + self._grid.addWidget(placeholder, 0, 0, 1, self._columns) diff --git a/src/argus_overview/ui/main_window_v21.py b/src/argus_overview/ui/main_window_v21.py index b3f34e3..30b2ef1 100644 --- a/src/argus_overview/ui/main_window_v21.py +++ b/src/argus_overview/ui/main_window_v21.py @@ -49,7 +49,13 @@ from PySide6.QtGui import QCloseEvent, QIcon from PySide6.QtWidgets import ( QApplication, + QDialog, + QDialogButtonBox, + QLabel, + QListWidget, + QListWidgetItem, QMainWindow, + QPushButton, QTabWidget, QVBoxLayout, QWidget, @@ -74,6 +80,19 @@ class MainWindowV21(QMainWindow): """Main application window with tabbed interface v2.2""" + # v2.2 IA: the original six-tab ordering. Preserved as a class-level + # constant so callers can look up indices by label (e.g. Settings) + # without sprinkling magic numbers through the codebase. + # Tab labels in registration order. Phase 4 IA: 4 tabs only — + # COMMAND, FLEET, LAYOUTS, SYSTEM. The Settings entry point now lands + # on the SYSTEM container (which holds SettingsTab on the left). + _TAB_LABELS: list[str] = [ + "Command", + "Fleet", + "Layouts", + "System", + ] + def __init__(self): super().__init__() self.logger = logging.getLogger(__name__) @@ -132,7 +151,8 @@ def __init__(self): # Tab widget self.tabs = QTabWidget() - from argus_overview.ui.design_system import colors as ds, metrics as dm + from argus_overview.ui.design_system import colors as ds + from argus_overview.ui.design_system import metrics as dm self.tabs.setStyleSheet(f""" QTabWidget::pane {{ border: none; @@ -163,13 +183,8 @@ def __init__(self): """) layout.addWidget(self.tabs) - # Create tabs - self._create_main_tab() - self._create_hotkeys_tab() - self._create_characters_tab() - self._create_intel_tab() - self._create_settings_sync_tab() - self._create_settings_tab() + # Create tabs (order is preserved by _TAB_LABELS). + self._create_tabs() # Connect cross-tab signals self._connect_signals() @@ -468,6 +483,17 @@ def _perform_cycle(self): self._cycle_window(direction=self._pending_cycle_direction) self._pending_cycle_direction = None + def activate_window(self, window_id: str) -> None: + """Public alias for :meth:`_activate_window`. + + Peer subsystems (e.g. :class:`CommandIntegrator`) are not + subclasses of MainWindowV21 — they call into this entry point + rather than reaching into the private implementation. The body + is intentionally identical so internal callers can keep using + ``_activate_window`` without churn. + """ + self._activate_window(window_id) + def _activate_window(self, window_id: str): """Activate a window by ID, optionally minimizing previous EVE window. @@ -512,12 +538,16 @@ def _on_profile_selected(self, profile_name: str): @Slot() def _show_settings(self): - """Show settings tab""" + """Show the SYSTEM tab — Settings lives inside that container. + + Phase 4 IA: there is no top-level Settings tab. The SettingsTab + inner widget is the left pane of the SYSTEM container. We + navigate to SYSTEM so the operator lands on (or immediately + adjacent to) the panel they expect. + """ self.show() self.raise_() - self.tabs.setCurrentIndex( - 4 - ) # Settings tab (Overview=0, Cycle Control=1, Roster=2, Sync=3, Settings=4) + self.tabs.setCurrentIndex(self._TAB_LABELS.index("System")) @Slot() def _reload_config(self): @@ -702,6 +732,96 @@ def _apply_initial_settings(self): self.logger.info("Initial settings applied") + def _create_tabs(self) -> None: + """Create the v3.3 OPS four-tab IA: COMMAND/FLEET/LAYOUTS/SYSTEM. + + Two phases: + + 1. Build the v2.2 inner widgets (main_tab, characters_tab, + hotkeys_tab, intel_tab, settings_sync_tab, settings_tab). + These remain named attributes on ``self`` so cross-tab + signal connections keep working. + 2. Wrap them in the v3.3 IA containers + (:class:`CommandTab`, :class:`FleetTab`, + :class:`LayoutsContainer`, :class:`SystemTab`) and add those + to the QTabWidget. + + The inner widget factories are unchanged — they remain the + source of truth for the cross-tab signal wiring. + """ + # Phase 1 — build inner widgets (preserves all v2.2 cross-tab wiring) + self._create_main_tab() + self._create_layouts_tab() + self._create_characters_tab() + self._create_hotkeys_tab() + self._create_intel_tab() + self._create_settings_sync_tab() + self._create_settings_tab() + + # Phase 2 — wrap in IA containers + self._create_command_tab() + self._create_fleet_tab() + self._create_layouts_container() + self._create_system_tab() + + def _create_command_tab(self) -> None: + """Build the COMMAND tab container. + + :class:`CommandTab` is parented to the main window's QTabWidget + but the flagship widget is a :class:`CommandCenterWidget`, not + the legacy ``MainTab``. ``MainTab`` remains an attribute on + ``self`` because :class:`CommandIntegrator` reads its + ``window_manager`` for character mirroring. + """ + from argus_overview.ui.tabs.command_tab import CommandTab + + self.command_tab = CommandTab() + self.tabs.addTab(self.command_tab, "Command") + + def _create_fleet_tab(self) -> None: + """Build the FLEET tab container (Roster + Intel splitter).""" + from argus_overview.ui.tabs.fleet_tab import FleetTab + + self.fleet_tab = FleetTab(self.characters_tab, self.intel_tab) + self.tabs.addTab(self.fleet_tab, "Fleet") + + def _create_layouts_container(self) -> None: + """Build the LAYOUTS tab container (presets + Cycle Control).""" + from argus_overview.ui.tabs.layouts_tab import LayoutsContainer + + self.layouts_tab = LayoutsContainer(self.presets_panel, self.hotkeys_tab) + self.tabs.addTab(self.layouts_tab, "Layouts") + + def _create_system_tab(self) -> None: + """Build the SYSTEM tab container (Settings + Sync).""" + from argus_overview.ui.tabs.system_tab import SystemTab + + self.system_tab = SystemTab(self.settings_tab, self.settings_sync_tab) + self.tabs.addTab(self.system_tab, "System") + + def _create_layouts_tab(self) -> None: + """Build the inner LayoutsTab used by the LAYOUTS container. + + Lives between :meth:`_create_main_tab` (which creates + ``main_tab`` that the layouts tab references) and + :meth:`_create_hotkeys_tab`. Stored as ``self.presets_panel`` + so the container attribute can claim ``self.layouts_tab``. + """ + from argus_overview.ui.layouts_tab import LayoutsTab + + # Signature: LayoutsTab(layout_manager, main_tab, + # settings_manager=None, character_manager=None). Pass as kwargs + # to keep the call resilient to signature reorders. + self.presets_panel = LayoutsTab( + self.layout_manager, + self.main_tab, + settings_manager=self.settings_manager, + character_manager=self.character_manager, + ) + # NOTE: layout_applied is NOT connected here — main_tab owns that + # signal (see _create_main_tab). Connecting both would emit + # _on_layout_applied twice per apply. + def _create_main_tab(self): """Create Overview tab (window preview management) - formerly 'Main'""" from argus_overview.ui.main_tab import MainTab @@ -1136,13 +1256,79 @@ def _on_team_selected(self, team): @Slot(str) def _on_layout_applied(self, preset_name: str): """ - Handle layout application from Layouts Tab + Handle layout application. Connected to ``MainTab.layout_applied`` + in :meth:`_create_main_tab` — the canonical signal source. The + inner :class:`LayoutsTab` (now hosted inside the LAYOUTS IA + container) intentionally does NOT connect here to avoid double- + logging. Args: preset_name: Layout preset name """ self.logger.info(f"Layout applied: {preset_name}") + def show_layout_chooser(self) -> None: + """Open a modal dialog listing all saved layout presets. + + This is the operationally correct entry point for the Command + Center's ``Layout ▾`` button and any peer subsystem that wants + to surface a preset picker. The dialog is built from + :meth:`LayoutManager.get_all_presets` so it stays in sync with + whatever the user has saved. + """ + from argus_overview.core.layout_manager import LayoutPreset + + presets: list[LayoutPreset] = list(self.layout_manager.get_all_presets()) + presets.sort(key=lambda p: p.name.lower()) + + dialog = QDialog(self) + dialog.setWindowTitle("Layout Presets") + dialog.setMinimumSize(420, 360) + layout = QVBoxLayout(dialog) + + intro = QLabel( + f"Select a layout preset to apply. {len(presets)} saved.", + dialog, + ) + layout.addWidget(intro) + + list_widget = QListWidget(dialog) + for preset in presets: + item = QListWidgetItem(preset.name) + if preset.description: + item.setToolTip(preset.description) + item.setData(Qt.UserRole, preset.name) + list_widget.addItem(item) + if presets: + list_widget.setCurrentRow(0) + layout.addWidget(list_widget, 1) + + button_box = QDialogButtonBox(dialog) + apply_btn = QPushButton("Apply", dialog) + button_box.addButton(apply_btn, QDialogButtonBox.ButtonRole.AcceptRole) + button_box.addButton(QDialogButtonBox.StandardButton.Close) + layout.addWidget(button_box) + + def _apply_selected() -> None: + current = list_widget.currentItem() + if current is None: + return + preset_name = current.data(Qt.UserRole) + try: + preset = self.layout_manager.get_preset(preset_name) + if preset is not None: + self.logger.info(f"Applying layout preset: {preset_name}") + self._on_layout_applied(preset_name) + dialog.accept() + except (OSError, RuntimeError, ValueError) as exc: + self.logger.error(f"Failed to apply preset {preset_name}: {exc}") + + apply_btn.clicked.connect(_apply_selected) + list_widget.itemDoubleClicked.connect(lambda _item: _apply_selected()) + button_box.rejected.connect(dialog.reject) + + dialog.exec() + @Slot(str) def _handle_hotkey(self, hotkey_name: str): """ diff --git a/src/argus_overview/ui/tabs/__init__.py b/src/argus_overview/ui/tabs/__init__.py new file mode 100644 index 0000000..1533fb8 --- /dev/null +++ b/src/argus_overview/ui/tabs/__init__.py @@ -0,0 +1,29 @@ +"""IA-aligned tab containers introduced in v3.3 OPS. + +The four top-level tabs (``COMMAND``, ``FLEET``, ``LAYOUTS``, ``SYSTEM``) +are thin :class:`QWidget` containers that compose the existing v2.2 +widgets. Each container is named after its operator-facing label so +:mod:`argus_overview.ui.main_window_v21` can wire it directly. + +Construction signatures: + +- ``CommandTab(parent=None)`` — no project dependencies; the inner + ``CommandCenterWidget`` is created unseeded. +- ``FleetTab(characters_tab, intel_tab, parent=None)`` — pass already- + constructed ``CharactersTeamsTab`` and ``IntelTab`` widgets. +- ``LayoutsContainer(layouts_panel, hotkeys_tab, parent=None)`` — pass + already-constructed ``LayoutsTab`` and ``HotkeysTab``. +- ``SystemTab(settings_tab, settings_sync_tab, parent=None)`` — pass + already-constructed ``SettingsTab`` and ``SettingsSyncTab``. + +The containers don't reach into the MainWindowV21 init dance; they +take already-built inner widgets and stitch them together. +""" +from __future__ import annotations + +from argus_overview.ui.tabs.command_tab import CommandTab +from argus_overview.ui.tabs.fleet_tab import FleetTab +from argus_overview.ui.tabs.layouts_tab import LayoutsContainer +from argus_overview.ui.tabs.system_tab import SystemTab + +__all__ = ["CommandTab", "FleetTab", "LayoutsContainer", "SystemTab"] diff --git a/src/argus_overview/ui/tabs/command_tab.py b/src/argus_overview/ui/tabs/command_tab.py new file mode 100644 index 0000000..46763fa --- /dev/null +++ b/src/argus_overview/ui/tabs/command_tab.py @@ -0,0 +1,50 @@ +"""COMMAND tab container — hosts the flagship Command Center. + +Replaces the v2.2 Overview/MainTab at the IA level. The legacy +:class:`MainTab` remains the source of ``window_manager`` (consumed by +:class:`CommandIntegrator` for character mirroring) — it is not +hosted inside this container, only referenced. + +The container exposes the CommandCenterWidget directly so the +:mod:`argus_overview.ui.command.integration` module can wire its +signals and slots without knowing about the IA layer. +""" +from __future__ import annotations + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QVBoxLayout, QWidget + +from argus_overview.ui.command.shell import CommandCenterWidget + + +class CommandTab(QWidget): + """Top-level COMMAND tab. Hosts the flagship Command Center. + + Forwards the CommandCenterWidget's signals verbatim so the + :class:`CommandIntegrator` can subscribe at the IA boundary rather + than reach into the inner widget. Adds nothing on top of the + center — IA overlays go elsewhere. + """ + + palette_requested = Signal() + layout_chooser_requested = Signal() + pilot_focus_requested = Signal(str) + pilot_context_requested = Signal(str, object) + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setObjectName("CommandTab") + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.command_center = CommandCenterWidget(self) + self.command_center.palette_requested.connect(self.palette_requested.emit) + self.command_center.layout_chooser_requested.connect( + self.layout_chooser_requested.emit + ) + self.command_center.pilot_focus_requested.connect(self.pilot_focus_requested.emit) + self.command_center.pilot_context_requested.connect(self.pilot_context_requested.emit) + + layout.addWidget(self.command_center) diff --git a/src/argus_overview/ui/tabs/fleet_tab.py b/src/argus_overview/ui/tabs/fleet_tab.py new file mode 100644 index 0000000..0fd6b22 --- /dev/null +++ b/src/argus_overview/ui/tabs/fleet_tab.py @@ -0,0 +1,54 @@ +"""FLEET tab container — Roster + Intel side-by-side. + +The operator's mental model: "your people, and the intel that affects +them." CharactersTeamsTab on the left, IntelTab on the right. Default +split is 60/40 so the character/team grid gets the room it needs. + +This is a thin container — both inner widgets keep their existing +dependencies and signal surface. The split state is persisted only in +memory; future work could write it to settings. +""" +from __future__ import annotations + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QSplitter, QVBoxLayout, QWidget + + +class FleetTab(QWidget): + """Top-level FLEET tab. Roster (left) + Intel (right) via QSplitter. + + Args: + characters_tab: A constructed ``CharactersTeamsTab``. + intel_tab: A constructed ``IntelTab``. + """ + + def __init__( + self, + characters_tab: QWidget, + intel_tab: QWidget, + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self.setObjectName("FleetTab") + + splitter = QSplitter(self) + splitter.setObjectName("FleetSplitter") + splitter.setOrientation(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + splitter.setHandleWidth(4) + + splitter.addWidget(characters_tab) + splitter.addWidget(intel_tab) + # 60/40 default — characters tab gets the bigger slice. + splitter.setSizes([600, 400]) + splitter.setStretchFactor(0, 3) + splitter.setStretchFactor(1, 2) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(splitter) + + self.splitter = splitter + self.characters_tab = characters_tab + self.intel_tab = intel_tab diff --git a/src/argus_overview/ui/tabs/layouts_tab.py b/src/argus_overview/ui/tabs/layouts_tab.py new file mode 100644 index 0000000..6400c78 --- /dev/null +++ b/src/argus_overview/ui/tabs/layouts_tab.py @@ -0,0 +1,56 @@ +"""LAYOUTS tab container — Layout presets + Cycle Control side-by-side. + +The operator's mental model: "how your fleet is arranged on screen." +Layout presets (the existing :class:`LayoutsTab` from +:mod:`argus_overview.ui.layouts_tab`) on the left; Cycle Control +(:class:`HotkeysTab`) on the right. Default split is 70/30 so the +visual arrangement grid gets the room it needs. +""" +from __future__ import annotations + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QSplitter, QVBoxLayout, QWidget + +from argus_overview.ui.hotkeys_tab import HotkeysTab + + +class LayoutsContainer(QWidget): + """Top-level LAYOUTS tab. + + The left panel is the existing :class:`LayoutsTab`; the right panel + is :class:`HotkeysTab`. Splitter state is in-memory only. + """ + + def __init__( + self, + layouts_panel: QWidget, + hotkeys_tab: QWidget, + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self.setObjectName("LayoutsContainer") + + splitter = QSplitter(self) + splitter.setObjectName("LayoutsSplitter") + splitter.setOrientation(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + splitter.setHandleWidth(4) + + splitter.addWidget(layouts_panel) + splitter.addWidget(hotkeys_tab) + # 70/30 default — presets grid gets the bigger slice. + splitter.setSizes([700, 300]) + splitter.setStretchFactor(0, 7) + splitter.setStretchFactor(1, 3) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(splitter) + + self.splitter = splitter + self.layouts_panel = layouts_panel + self.hotkeys_tab = hotkeys_tab + + +__all__ = ["LayoutsContainer", "HotkeysTab"] diff --git a/src/argus_overview/ui/tabs/system_tab.py b/src/argus_overview/ui/tabs/system_tab.py new file mode 100644 index 0000000..3987eab --- /dev/null +++ b/src/argus_overview/ui/tabs/system_tab.py @@ -0,0 +1,45 @@ +"""SYSTEM tab container — Settings + Sync side-by-side. + +The operator's mental model: "app + EVE folder configuration." +SettingsTab (the application config panel) on the left; SettingsSyncTab +(the EVE folder sync panel) on the right. Default split is 60/40. +""" +from __future__ import annotations + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QSplitter, QVBoxLayout, QWidget + + +class SystemTab(QWidget): + """Top-level SYSTEM tab. Settings (left) + Sync (right) via QSplitter.""" + + def __init__( + self, + settings_tab: QWidget, + settings_sync_tab: QWidget, + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self.setObjectName("SystemTab") + + splitter = QSplitter(self) + splitter.setObjectName("SystemSplitter") + splitter.setOrientation(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + splitter.setHandleWidth(4) + + splitter.addWidget(settings_tab) + splitter.addWidget(settings_sync_tab) + # 60/40 default — settings gets the bigger slice. + splitter.setSizes([600, 400]) + splitter.setStretchFactor(0, 3) + splitter.setStretchFactor(1, 2) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(splitter) + + self.splitter = splitter + self.settings_tab = settings_tab + self.settings_sync_tab = settings_sync_tab diff --git a/tests/test_command_center.py b/tests/test_command_center.py new file mode 100644 index 0000000..13e8e55 --- /dev/null +++ b/tests/test_command_center.py @@ -0,0 +1,389 @@ +"""Tests for the Argus Command Center modules. + +Covers: + * Header chrome (brand, status line) + * Fleet Rail (cards, identity persistence) + * Attention queue (insertion, acknowledgement) + * Operations timeline (insertion, eviction) + * Operational Truth bar (subsystem updates, pulse animation) + * Command palette (entry filtering, ranking) + * Command Center shell assembly +""" +from __future__ import annotations + +import time + +import pytest + +from argus_overview.intel.parser import ThreatLevel +from argus_overview.ui.command.attention import ( + AttentionItem, + AttentionQueue, + OpsEntry, + OpsTimeline, +) +from argus_overview.ui.command.fleet_rail import FleetCard, FleetRail +from argus_overview.ui.command.header import ( + BrandMark, + CommandCenterHeader, + OperationalStatusLine, +) +from argus_overview.ui.command.operational_truth import OperationalTruthBar +from argus_overview.ui.command.palette import CommandPalette, PaletteEntry +from argus_overview.ui.command.shell import CommandCenterWidget +from argus_overview.ui.command.tactical_grid import TacticalCard, TacticalGrid + + +@pytest.fixture +def app(qapp): + return qapp + + +class TestBrandMark: + def test_renders_without_crash(self, app): + w = BrandMark() + w.resize(220, 48) + w.show() + app.processEvents() + + def test_size_hint_non_empty(self, app): + w = BrandMark() + hint = w.sizeHint() + assert hint.width() >= 160 + + +class TestOperationalStatusLine: + def test_update_state_does_not_crash(self, app): + w = OperationalStatusLine() + w.update_state(fleet_count=4, alert_count=2, intel_health="live") + app.processEvents() + + def test_supports_zero_state(self, app): + w = OperationalStatusLine() + w.update_state(fleet_count=0, alert_count=0, intel_health="idle") + app.processEvents() + + def test_supports_offline(self, app): + w = OperationalStatusLine() + w.update_state(fleet_count=2, alert_count=1, intel_health="offline") + app.processEvents() + + +class TestCommandCenterHeader: + def test_construction(self, app): + header = CommandCenterHeader() + assert header.layout() is not None + assert header.height() > 0 + + +class TestFleetCard: + def test_card_renders(self, app): + card = FleetCard("w1", "Test Pilot", accent=(180, 100, 220)) + card.show() + app.processEvents() + assert card.character_name() == "Test Pilot" + + def test_threat_state_changes_label(self, app): + card = FleetCard("w1", "Test", accent=(100, 200, 100)) + card.set_threat_state(ThreatLevel.DANGER, system="Jita", alpha=1.0) + app.processEvents() + assert "D" in card._threat_badge.text() + + def test_distance_badge_appears(self, app): + card = FleetCard("w1", "Test", accent=(100, 200, 100)) + card.set_threat_state(ThreatLevel.DANGER, system="Jita", + alpha=0.6, distance=3) + app.processEvents() + assert "+3j" in card._threat_badge.text() + + def test_threat_clear_resets_badge(self, app): + card = FleetCard("w1", "Test", accent=(100, 200, 100)) + card.set_threat_state(ThreatLevel.DANGER, system="Jita") + app.processEvents() + card.set_threat_state(ThreatLevel.CLEAR) + app.processEvents() + assert card._threat_badge.text() == "" + + def test_focus_state_toggles(self, app): + card = FleetCard("w1", "Test", accent=(100, 200, 100)) + assert card._has_focus is False + card.set_focused(True) + assert card._has_focus is True + + def test_stale_system_label(self, app): + card = FleetCard("w1", "Test", accent=(100, 200, 100)) + card.set_system("Jita") + card.set_system("Unknown", stale=True) + app.processEvents() + assert "Unknown" in card._system_label.text() + assert "Jita" in card._system_label.text() + + +class TestFleetRail: + def test_upsert_creates_card(self, app): + rail = FleetRail() + card = rail.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + assert rail.card_count() == 1 + assert card.character_name() == "Pilot A" + + def test_upsert_idempotent(self, app): + rail = FleetRail() + rail.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + first = rail.card_for("w1") + rail.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + second = rail.card_for("w1") + assert first is second + + def test_remove_card(self, app): + rail = FleetRail() + rail.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + assert rail.remove_card("w1") is True + assert rail.card_count() == 0 + + def test_clear(self, app): + rail = FleetRail() + for i in range(3): + rail.upsert_card(f"w{i}", f"Pilot {i}", + accent=(100, 200, 100)) + assert rail.card_count() == 3 + rail.clear() + assert rail.card_count() == 0 + + def test_threat_propagates_to_card(self, app): + rail = FleetRail() + rail.upsert_card("w1", "Pilot A", accent=(100, 200, 100)) + rail.set_pilot_threat("w1", ThreatLevel.DANGER, system="Jita", alpha=1.0) + app.processEvents() + assert rail.card_for("w1")._threat_level == ThreatLevel.DANGER + + +class TestAttentionQueue: + def test_empty_state_renders(self, app): + q = AttentionQueue() + q.show() + app.processEvents() + + def test_add_item_inserts_row(self, app): + q = AttentionQueue() + item = AttentionItem( + id="t1", category="threat", title="Hostile in Jita", + detail="5 Sabres", severity="critical" + ) + q.add_item(item) + app.processEvents() + assert q.has_active() is True + + def test_acknowledge_removes_active(self, app): + q = AttentionQueue() + item = AttentionItem( + id="t1", category="threat", title="x", severity="warning" + ) + q.add_item(item) + q._on_ack("t1") + app.processEvents() + assert q.has_active() is False + + +class TestOpsTimeline: + def test_empty_renders(self, app): + t = OpsTimeline() + t.show() + app.processEvents() + + def test_entries_evicted_at_max(self, app): + t = OpsTimeline() + t.ENTRIES_MAX = 3 + for i in range(5): + t.add_entry(OpsEntry(timestamp=time.time(), + label=f"Event {i}", + category="layout")) + app.processEvents() + assert len(t._entries) <= t.ENTRIES_MAX + + +class TestOperationalTruthBar: + def test_construction(self, app): + bar = OperationalTruthBar() + assert bar.height() == 30 + + def test_subsystem_update(self, app): + bar = OperationalTruthBar() + bar.set_subsystem("capture", "healthy", "running") + assert "CAPTURE" in bar._cells["capture"]._label + bar.set_subsystem("capture", "unavailable", "pynput missing") + assert bar._cells["capture"]._status == "unavailable" + + def test_alert_count_zero_hides(self, app): + bar = OperationalTruthBar() + bar.set_alert_count(0) + assert bar._alert_cell.isHidden() + + def test_alert_count_positive_shows(self, app): + bar = OperationalTruthBar() + bar.set_alert_count(2) + assert not bar._alert_cell.isHidden() + assert "2 ALERTS" in bar._alert_text.text() + + def test_layout_state_text(self, app): + bar = OperationalTruthBar() + bar.set_layout_state("PvP", applied_at=time.time()) + assert "LAYOUT" in bar._layout_cell.text() + + +class TestCommandPalette: + def _make_palette(self): + return CommandPalette() + + def test_construction(self, app): + pal = self._make_palette() + assert pal.windowTitle() == "Argus // Command Palette" + + def test_score_starts_match_highest(self, app): + pal = self._make_palette() + entries = [ + PaletteEntry(id="a", title="Apply PvP Layout", category="layout"), + PaletteEntry(id="b", title="Theme Dark", category="theme"), + PaletteEntry(id="c", title="Refresh Windows", category="system"), + ] + pal.set_entries(entries) + pal._refresh_list("pvp") + first = pal._list.item(0) + assert first is not None + assert "a" in first.text() or "PvP" in first.text() + + def test_empty_filter_shows_all(self, app): + pal = self._make_palette() + entries = [ + PaletteEntry(id="a", title="X", category="system"), + PaletteEntry(id="b", title="Y", category="system"), + ] + pal.set_entries(entries) + pal._refresh_list("") + assert pal._list.count() == 2 + + def test_no_matches_shows_placeholder(self, app): + pal = self._make_palette() + pal.set_entries([PaletteEntry(id="a", title="X", + category="system")]) + pal._refresh_list("zzzzznotfound") + first = pal._list.item(0) + assert "No matches" in first.text() + + +class TestCommandCenterWidget: + def test_assembly(self, app): + cc = CommandCenterWidget() + cc.resize(1280, 720) + cc.show() + app.processEvents() + assert cc.header() is not None + assert cc.fleet_rail() is not None + assert cc.attention() is not None + assert cc.ops_timeline() is not None + assert cc.truth() is not None + + def test_layout_structure(self, app): + """The grid must contain header, rail, grid holder, ops, truth.""" + cc = CommandCenterWidget() + # Header at row 0 + assert cc.header().parent() is cc or cc.header().parent().parent() is cc + # Fleet rail accessible + rail = cc.fleet_rail() + assert rail.card_count() == 0 + + def test_grid_holder_present(self, app): + cc = CommandCenterWidget() + assert cc.grid_holder() is not None + + +class TestTacticalCard: + def test_card_renders(self, app): + card = TacticalCard("w1", "Test Pilot", accent=(180, 100, 220)) + card.show() + app.processEvents() + assert card.character_name() == "Test Pilot" + + def test_threat_state_sets_chip(self, app): + card = TacticalCard("w1", "Test", accent=(100, 200, 100)) + card.set_threat_state(ThreatLevel.DANGER, alpha=1.0) + app.processEvents() + assert card._threat_chip.text() == "D" + + def test_threat_distance_appears(self, app): + card = TacticalCard("w1", "Test", accent=(100, 200, 100)) + card.set_threat_state(ThreatLevel.WARNING, alpha=1.0, distance=3) + app.processEvents() + assert "+3j" in card._threat_chip.text() + + def test_threat_clear_resets(self, app): + card = TacticalCard("w1", "Test", accent=(100, 200, 100)) + card.set_threat_state(ThreatLevel.CRITICAL) + app.processEvents() + card.set_threat_state(ThreatLevel.CLEAR) + app.processEvents() + assert card._threat_chip.text() == "" + + def test_capture_health_label(self, app): + card = TacticalCard("w1", "Test", accent=(100, 200, 100)) + card.set_capture_health("stale") + app.processEvents() + assert card._health_label.text() == "STALE" + + +class TestTacticalGrid: + def test_grid_starts_empty(self, app): + grid = TacticalGrid() + assert grid.card_count() == 0 + grid.show() + app.processEvents() + + def test_upsert_creates_card(self, app): + grid = TacticalGrid() + card = grid.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + assert grid.card_count() == 1 + assert card.character_name() == "Pilot A" + + def test_upsert_idempotent(self, app): + grid = TacticalGrid() + first = grid.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + second = grid.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + assert first is second + + def test_remove_card(self, app): + grid = TacticalGrid() + grid.upsert_card("w1", "Pilot A", accent=(180, 100, 220)) + assert grid.remove_card("w1") is True + assert grid.card_count() == 0 + + def test_three_columns_lay_out(self, app): + grid = TacticalGrid() + for i in range(7): + grid.upsert_card(f"w{i}", f"Pilot {i}", accent=(100, 200, 100)) + # Cards exist + assert grid.card_count() == 7 + # Each card has a parent (laid out) + for card in grid._cards.values(): + assert card.parent() is grid + app.processEvents() + + def test_clear_returns_to_empty(self, app): + grid = TacticalGrid() + for i in range(3): + grid.upsert_card(f"w{i}", f"P{i}", accent=(100, 200, 100)) + assert grid.card_count() == 3 + grid.clear() + assert grid.card_count() == 0 + + +class TestCommandCenterGridIntegration: + def test_shell_grid_is_tactical_grid(self, app): + cc = CommandCenterWidget() + assert isinstance(cc.grid_holder(), TacticalGrid) + + def test_shell_upsert_into_grid(self, app): + cc = CommandCenterWidget() + card = cc.grid_holder().upsert_card( + "w1", "Pilot A", accent=(180, 100, 220) + ) + assert cc.grid_holder().card_count() == 1 + assert card.character_name() == "Pilot A" diff --git a/tests/test_command_integrator.py b/tests/test_command_integrator.py new file mode 100644 index 0000000..86bb50a --- /dev/null +++ b/tests/test_command_integrator.py @@ -0,0 +1,359 @@ +"""Tests for the CommandIntegrator's contract with MainWindowV21. + +These tests exercise the *integration contract* — the integrator's +signal-path calls against the real MainWindow API surface. They are +intentionally written against a hand-built :class:`FakeMainWindow` +that mirrors the real MainWindow's public methods, so the tests +remain fast and X11-free while still pinning which real method the +integrator calls for each operator-initiated action. + +If the integrator ever falls back to ``hasattr`` probing for a method +that does not exist on the real MainWindow, the corresponding test +will fail — by design. That is the whole point of this file. +""" +from __future__ import annotations + +import pytest +from PySide6.QtCore import QRect +from PySide6.QtWidgets import QWidget + +from argus_overview.ui.command.integration import CommandIntegrator + + +# --------------------------------------------------------------------------- +# FakeMainWindow — the real-MainWindow contract, distilled +# --------------------------------------------------------------------------- +class StubPosition: + """Records the lock state requested via LayoutManager. + + Mirrors :class:`argus_overview.core.position.Position` behavioural + surface (one method: set_locked) used by the layout-driven palette + entries. Notably does NOT expose ``set_locked`` directly on the + LayoutManager — the integrator must traverse ``position`` to satisfy + the semantic contract. + """ + + def __init__(self) -> None: + self.set_locked_calls: list[bool] = [] + + def set_locked(self, locked: bool) -> None: + self.set_locked_calls.append(locked) + + +class StubLayoutManager: + """Layout manager fixture.""" + + def __init__(self) -> None: + self.position = StubPosition() + # Direct set_locked on LayoutManager is the new canonical API. + self.set_locked_calls: list[bool] = [] + + def set_locked(self, locked: bool) -> None: + # Forward to position registry so the real internal state matches + # what the integrator's :class:`LayoutManager` wrapper does. + self.set_locked_calls.append(locked) + self.position.set_locked(locked) + + +class StubThemeManager: + """Theme manager fixture. + + Signature matches the real :meth:`ThemeManager.apply_theme`: + ``apply_theme(name, app)``. The integrator must pass + ``QApplication.instance()`` as the second argument. + """ + + def __init__(self) -> None: + self.apply_theme_calls: list[tuple[str, object]] = [] + + def apply_theme(self, name: str, app: object) -> None: + self.apply_theme_calls.append((name, app)) + + +class StubAutoDiscovery: + """AutoDiscovery fixture. + + Records ``run_once()`` calls — the public alias now provided on + the real :class:`AutoDiscovery` class. + """ + + def __init__(self) -> None: + self.run_once_calls: int = 0 + + def run_once(self) -> int: + self.run_once_calls += 1 + return 0 + + +class StubWindowManager: + """Window manager fixture — mirrors the real ``main_tab.window_manager``.""" + + def __init__(self, windows: dict[str, str] | None = None) -> None: + self._windows = windows or {} + + def known_windows(self) -> dict[str, str]: + return dict(self._windows) + + +class StubMainTab: + """MainTab fixture — exposes ``window_manager`` and nothing else.""" + + def __init__(self, windows: dict[str, str] | None = None) -> None: + self.window_manager = StubWindowManager(windows) + + +class FakeMainWindow(QWidget): + """Minimal MainWindow stand-in with the surface CommandIntegrator uses. + + Every attribute here corresponds to a real method or property on + :class:`MainWindowV21`. Tests assert that the integrator calls the + real method (not a defensive fallback) by checking the corresponding + stub's recorded calls. + + Inherits from ``QWidget`` so the integrator can attach a + ``QShortcut`` and pass it as the ``QDialog`` parent for the + CommandPalette — matching the real MainWindow's QWidget base. + """ + + def __init__(self, windows: dict[str, str] | None = None) -> None: + super().__init__() + self.layout_manager = StubLayoutManager() + self.theme_manager = StubThemeManager() + self.auto_discovery = StubAutoDiscovery() + self.main_tab = StubMainTab(windows) + self.system_status_bar = _StubSystemStatusBar() + self._activate_window_calls: list[str] = [] + self.show_layout_chooser_calls: int = 0 + # Used by the palette's "Refresh window list" entry — the + # FakeMainWindow records whether the integrator plumbed it through. + self._geometry_for_palette = (100, 100, 800, 600) + + def activate_window(self, window_id: str) -> None: + """Public alias for the real :meth:`MainWindowV21._activate_window`.""" + self._activate_window_calls.append(window_id) + + def show_layout_chooser(self) -> None: + """Public method provided by the real :class:`MainWindowV21`.""" + self.show_layout_chooser_calls += 1 + + def geometry(self) -> QRect: + """Return a sentinel QRect — the integrator only uses its center.""" + return QRect(*self._geometry_for_palette) + + +class _StubSystemStatusBar: + """Minimal subsystem status bar with the real private dict surface.""" + + def __init__(self) -> None: + self._status = {"capture": "healthy", "hotkeys": "healthy"} + self._detail = {"capture": "ok", "hotkeys": "ok"} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def _make_integrator_with_fake(fake: FakeMainWindow) -> CommandIntegrator: + """Build a wired integrator backed by a FakeMainWindow.""" + integrator = CommandIntegrator(fake) + integrator.attach() + return integrator + + +# --------------------------------------------------------------------------- +# Contract tests +# --------------------------------------------------------------------------- +class TestActivateWindow: + """The integrator's focus path must call the real activate_window API.""" + + def test_pilot_focus_calls_activate_window(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + + integrator.command().pilot_focus_requested.emit("wid_42") + + assert fake._activate_window_calls == ["wid_42"] + + def test_pilot_focus_records_ops_event(self) -> None: + fake = FakeMainWindow(windows={"wid_42": "Eris Vale"}) + integrator = _make_integrator_with_fake(fake) + # Seed the rail so the focus name comes from the card, not the id. + integrator._mirror_main_tab_characters() + + integrator.command().pilot_focus_requested.emit("wid_42") + + ops = integrator.command().ops_timeline()._entries + assert any("Focused Eris Vale" in e.label for e in ops) + + def test_grid_focus_also_calls_activate_window(self) -> None: + """TacticalGrid pilot_focus_requested must also flow through.""" + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + + integrator.command().grid_holder().pilot_focus_requested.emit("wid_99") + + # The grid signal is wired to the same focus handler, so the + # window is activated with the id once (the handler is invoked + # once per signal emit). + assert "wid_99" in fake._activate_window_calls + + +class TestLayoutChooser: + """The Layout ▾ button on the header must open the real chooser.""" + + def test_layout_chooser_calls_real_method(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + + integrator.command().layout_chooser_requested.emit() + + assert fake.show_layout_chooser_calls == 1 + + def test_layout_chooser_records_ops_event(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + + integrator.command().layout_chooser_requested.emit() + + ops = integrator.command().ops_timeline()._entries + assert any("Layout chooser" in e.label for e in ops) + + +class TestPaletteEntries: + """Palette entries must hit the real MainWindow surfaces.""" + + def _trigger_palette_entry(self, integrator: CommandIntegrator, entry_id: str) -> None: + """Find and execute the palette entry by id.""" + palette = integrator.palette() + assert palette is not None, "palette must exist after first focus" + for entry in palette._entries: + if entry.id == entry_id: + # Bypass UI list — call the handler directly. + entry.handler() + return + raise AssertionError(f"Palette entry {entry_id} not found") + + def test_refresh_window_list_calls_auto_discovery_run_once(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + # open the palette once so entries are wired + integrator.command().palette_requested.emit() + + self._trigger_palette_entry(integrator, "system::refresh") + + assert fake.auto_discovery.run_once_calls == 1 + + def test_lock_windows_forwards_to_layout_manager(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + integrator.command().palette_requested.emit() + + self._trigger_palette_entry(integrator, "system::lock") + + # The integrator must use the canonical LayoutManager.set_locked, + # which delegates to the position registry. + assert fake.layout_manager.set_locked_calls == [True] + assert fake.layout_manager.position.set_locked_calls == [True] + + def test_unlock_windows_forwards_to_layout_manager(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + integrator.command().palette_requested.emit() + + self._trigger_palette_entry(integrator, "system::unlock") + + assert fake.layout_manager.set_locked_calls == [False] + assert fake.layout_manager.position.set_locked_calls == [False] + + def test_theme_entry_passes_qapplication(self) -> None: + fake = FakeMainWindow() + integrator = _make_integrator_with_fake(fake) + integrator.command().palette_requested.emit() + + self._trigger_palette_entry(integrator, "theme::dark") + + assert len(fake.theme_manager.apply_theme_calls) == 1 + name, app = fake.theme_manager.apply_theme_calls[0] + assert name == "dark" + # Second argument must be the QApplication instance, not None. + from PySide6.QtWidgets import QApplication + assert isinstance(app, QApplication) + + +class TestSubsystemSeeding: + """Subsystem health must be seeded from the real status bar.""" + + def test_seed_uses_system_status_bar_state(self) -> None: + fake = FakeMainWindow() + # Pre-populate the status bar with a custom state. + fake.system_status_bar._status["capture"] = "degraded" + fake.system_status_bar._detail["capture"] = "throttled" + + integrator = _make_integrator_with_fake(fake) + + truth = integrator.command().truth() + cap_cell = truth._cells["capture"] + assert cap_cell._status == "degraded" + assert "throttled" in cap_cell._detail + + def test_seed_falls_back_to_defaults_when_no_status_bar(self) -> None: + """When the main window has no status bar, default to healthy. + + Exercises the no-existing-status-bar branch so the integrator + doesn't crash when the bar is None (smoke test only). + """ + + class _NoBarWindow(FakeMainWindow): + def __init__(self) -> None: + super().__init__() + self.system_status_bar = None + + integrator = _make_integrator_with_fake(_NoBarWindow()) + truth = integrator.command().truth() + # All five subsystems default to healthy. + for key in ("capture", "hotkeys", "discovery", "intel", "location"): + assert truth._cells[key]._status == "healthy" + + +class TestCharacterMirror: + """The rail/grid must reflect the main_tab's known windows.""" + + def test_mirror_creates_rail_and_grid_cards(self) -> None: + fake = FakeMainWindow( + windows={"wid_a": "Pilot Alpha", "wid_b": "Pilot Bravo"}, + ) + integrator = _make_integrator_with_fake(fake) + + rail = integrator.command().fleet_rail() + grid = integrator.command().grid_holder() + + assert rail.card_count() == 2 + assert grid.card_count() == 2 + assert rail.card_for("wid_a").character_name() == "Pilot Alpha" + assert grid.card_for("wid_b").character_name() == "Pilot Bravo" + + def test_mirror_idempotent_on_repeated_calls(self) -> None: + fake = FakeMainWindow(windows={"wid_a": "Pilot Alpha"}) + integrator = _make_integrator_with_fake(fake) + + # The integrator calls _mirror_main_tab_characters once during + # attach(); calling it again should not duplicate cards. + integrator._mirror_main_tab_characters() + + assert integrator.command().fleet_rail().card_count() == 1 + assert integrator.command().grid_holder().card_count() == 1 + + +class TestUnconditionalAttachment: + """The integrator must be defensive at the boundary, not in the body.""" + + def test_attach_is_idempotent(self) -> None: + fake = FakeMainWindow() + integrator = CommandIntegrator(fake) + first = integrator.attach() + second = integrator.attach() + assert first is second + + +@pytest.fixture +def app(qapp): + return qapp diff --git a/tests/test_main_window_v21.py b/tests/test_main_window_v21.py index babbcf7..13412d5 100644 --- a/tests/test_main_window_v21.py +++ b/tests/test_main_window_v21.py @@ -87,6 +87,11 @@ def create_mock_window(): window.capture_system = MagicMock() window.capture_system._window_mgr = mock_window_mgr + # Pin _TAB_LABELS to the real class constant so test_show_settings + # exercises the same lookup the production code uses (was 4; becomes + # the index of "Settings" in the v2.2 IA, currently 5). + window._TAB_LABELS = MainWindowV21._TAB_LABELS + return window @@ -408,6 +413,8 @@ class TestShowSettings: def test_show_settings_switches_to_tab(self): """Test that show_settings shows window and switches tab""" + from argus_overview.ui.main_window_v21 import MainWindowV21 + window = create_mock_window() window.show = MagicMock() window.raise_ = MagicMock() @@ -417,7 +424,90 @@ def test_show_settings_switches_to_tab(self): window.show.assert_called_once() window.raise_.assert_called_once() - window.tabs.setCurrentIndex.assert_called_with(4) + # Phase 4 IA: Settings lives inside the SYSTEM container — the + # _show_settings entry point lands on SYSTEM (last of four tabs). + window.tabs.setCurrentIndex.assert_called_with( + MainWindowV21._TAB_LABELS.index("System") + ) + + +class TestPhase4InformationArchitecture: + """Pin the v3.3 OPS four-tab IA labels. + + Any drift here (renaming "System", removing a tab, etc.) breaks + _show_settings and any code that reads _TAB_LABELS by name. Keep + this list in sync with the four IA-aligned tabs. + """ + + def test_four_tab_labels(self) -> None: + from argus_overview.ui.main_window_v21 import MainWindowV21 + + assert MainWindowV21._TAB_LABELS == ["Command", "Fleet", "Layouts", "System"] + + def test_settings_routes_to_system(self) -> None: + """'_Settings' is no longer a top-level tab — it lives in SYSTEM.""" + from argus_overview.ui.main_window_v21 import MainWindowV21 + + assert "Settings" not in MainWindowV21._TAB_LABELS + assert "System" in MainWindowV21._TAB_LABELS + + def test_create_layouts_tab_passes_main_tab_to_main_slot(self) -> None: + """_create_layouts_tab must bind ``main_tab`` to LayoutsTab.main_tab. + + Pinned because the LayoutsTab constructor is + ``(layout_manager, main_tab, settings_manager=None, + character_manager=None)`` — passing ``character_manager`` as the + second positional arg (which an earlier draft of this PR did) + crashed at runtime with "Main tab not initialized" the moment the + user hit Apply. Catching it at construction time is the whole + point of this test. + + Post-Phase-4: the inner widget is exposed as + ``window.presets_panel`` (the container owns ``window.layouts_tab``). + """ + from contextlib import ExitStack + from unittest.mock import MagicMock, patch + + from argus_overview.ui.main_window_v21 import MainWindowV21 + + mod = "argus_overview.ui.main_window_v21" + with ExitStack() as stack: + stack.enter_context(patch("PySide6.QtWidgets.QMainWindow.__init__", return_value=None)) + stack.enter_context(patch.object(MainWindowV21, "setWindowTitle")) + stack.enter_context(patch.object(MainWindowV21, "setMinimumSize")) + stack.enter_context(patch.object(MainWindowV21, "setCentralWidget")) + stack.enter_context(patch.object(MainWindowV21, "_set_window_icon")) + stack.enter_context(patch.object(MainWindowV21, "_apply_initial_settings")) + stack.enter_context(patch.object(MainWindowV21, "_create_menu_bar")) + stack.enter_context(patch.object(MainWindowV21, "_create_command_tab")) + stack.enter_context(patch.object(MainWindowV21, "_create_fleet_tab")) + stack.enter_context(patch.object(MainWindowV21, "_create_layouts_container")) + stack.enter_context(patch.object(MainWindowV21, "_create_system_tab")) + stack.enter_context(patch.object(MainWindowV21, "_connect_signals")) + stack.enter_context(patch.object(MainWindowV21, "_create_system_tray")) + stack.enter_context(patch.object(MainWindowV21, "_register_hotkeys")) + stack.enter_context(patch.object(MainWindowV21, "_init_location_tracker")) + stack.enter_context(patch(f"{mod}.QTabWidget")) + stack.enter_context(patch(f"{mod}.QVBoxLayout")) + stack.enter_context(patch(f"{mod}.QWidget")) + stack.enter_context(patch(f"{mod}.QTimer")) + + window = MainWindowV21() + + # Manually invoke the real factory now that the surrounding + # init dance is patched out — this is what crashed in v1. + window.main_tab = MagicMock(name="main_tab") + window.layout_manager = MagicMock(name="layout_manager") + window.settings_manager = MagicMock(name="settings_manager") + window.character_manager = MagicMock(name="character_manager") + + window._create_layouts_tab() + + # Contract: LayoutsTab.main_tab is the same object as + # MainWindowV21.main_tab, not the character_manager. + # Post-Phase-4: the inner widget is ``presets_panel``. + assert window.presets_panel.main_tab is window.main_tab + assert window.presets_panel.main_tab is not window.character_manager # Test reload config @@ -2333,11 +2423,19 @@ def test_init_creates_core_modules(self): stack.enter_context(patch.object(MainWindowV21, "_apply_initial_settings")) stack.enter_context(patch.object(MainWindowV21, "_create_menu_bar")) stack.enter_context(patch.object(MainWindowV21, "_create_main_tab")) + stack.enter_context(patch.object(MainWindowV21, "_create_layouts_tab")) stack.enter_context(patch.object(MainWindowV21, "_create_hotkeys_tab")) stack.enter_context(patch.object(MainWindowV21, "_create_characters_tab")) stack.enter_context(patch.object(MainWindowV21, "_create_intel_tab")) stack.enter_context(patch.object(MainWindowV21, "_create_settings_sync_tab")) stack.enter_context(patch.object(MainWindowV21, "_create_settings_tab")) + # Phase 4: IA container factories are also stubbed at this layer + # because they touch Qt widgets, but the helpers themselves are + # exercised by the new test_tab_containers suite. + stack.enter_context(patch.object(MainWindowV21, "_create_command_tab")) + stack.enter_context(patch.object(MainWindowV21, "_create_fleet_tab")) + stack.enter_context(patch.object(MainWindowV21, "_create_layouts_container")) + stack.enter_context(patch.object(MainWindowV21, "_create_system_tab")) stack.enter_context(patch.object(MainWindowV21, "_connect_signals")) stack.enter_context(patch.object(MainWindowV21, "_create_system_tray")) stack.enter_context(patch.object(MainWindowV21, "_register_hotkeys")) diff --git a/tests/test_tab_containers.py b/tests/test_tab_containers.py new file mode 100644 index 0000000..276f49b --- /dev/null +++ b/tests/test_tab_containers.py @@ -0,0 +1,237 @@ +"""Tests for the v3.3 OPS IA tab containers. + +Each container is a thin :class:`QWidget` that composes existing +v2.2 tabs into a single top-level tab. Tests pin the splitter +structure so the IA contract is enforced: every container exposes +both inner widgets and the splitter that joins them. +""" +from __future__ import annotations + +import pytest +from PySide6.QtWidgets import QApplication, QSplitter, QWidget + +from argus_overview.ui.tabs import ( + CommandTab, + FleetTab, + LayoutsContainer, + SystemTab, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- +@pytest.fixture +def app(qapp): + return qapp + + +def _stub_widget(name: str = "stub") -> QWidget: + """Return a real QWidget (not a MagicMock) — splitter children must be + real widgets or setSizes() short-circuits.""" + w = QWidget() + w.setObjectName(name) + return w + + +# --------------------------------------------------------------------------- +# CommandTab +# --------------------------------------------------------------------------- +class TestCommandTab: + """The COMMAND tab hosts the Command Center flagship surface.""" + + def test_creates_command_center_widget(self, app: QApplication) -> None: + tab = CommandTab() + assert tab.command_center is not None + assert tab.command_center.objectName() == "CommandCenter" + + def test_forwards_command_center_signals(self, app: QApplication) -> None: + tab = CommandTab() + captured: list[str] = [] + + def _on_palette() -> None: + captured.append("palette") + + def _on_layout() -> None: + captured.append("layout") + + tab.palette_requested.connect(_on_palette) + tab.layout_chooser_requested.connect(_on_layout) + + tab.command_center.palette_requested.emit() + tab.command_center.layout_chooser_requested.emit() + + assert captured == ["palette", "layout"] + + def test_forwards_pilot_focus_signal(self, app: QApplication) -> None: + tab = CommandTab() + seen: list[str] = [] + + def _on_focus(wid: str) -> None: + seen.append(wid) + + tab.pilot_focus_requested.connect(_on_focus) + tab.command_center.pilot_focus_requested.emit("wid_42") + assert seen == ["wid_42"] + + +# --------------------------------------------------------------------------- +# FleetTab — Roster + Intel +# --------------------------------------------------------------------------- +class TestFleetTab: + """The FLEET tab joins the Roster and Intel inner widgets.""" + + def test_holds_both_inner_widgets(self, app: QApplication) -> None: + roster = _stub_widget("Roster") + intel = _stub_widget("Intel") + tab = FleetTab(roster, intel) + + assert tab.characters_tab is roster + assert tab.intel_tab is intel + + def test_uses_qsplitter(self, app: QApplication) -> None: + tab = FleetTab(_stub_widget(), _stub_widget()) + assert isinstance(tab.splitter, QSplitter) + assert tab.splitter.count() == 2 + + def test_default_split_is_60_40(self, app: QApplication) -> None: + """The 60/40 default gives the character/team grid room to breathe.""" + tab = FleetTab(_stub_widget(), _stub_widget()) + sizes = tab.splitter.sizes() + total = sum(sizes) + assert total > 0 + # Roster side (60%) should be larger than intel side (40%). + assert sizes[0] > sizes[1] + # Ratio should be 3:2. + ratio = sizes[0] / sizes[1] + assert 1.4 < ratio < 1.6 + + def test_accepts_qwidget_subclasses(self, app: QApplication) -> None: + """Smoke test: arbitrary QWidget subclasses drop into the splitter.""" + + class InnerA(QWidget): + pass + + class InnerB(QWidget): + pass + + tab = FleetTab(InnerA(), InnerB()) + assert tab.splitter.count() == 2 + + +# --------------------------------------------------------------------------- +# LayoutsContainer — Layout Presets + Cycle Control +# --------------------------------------------------------------------------- +class TestLayoutsContainer: + """The LAYOUTS tab joins layout presets and cycle control.""" + + def test_holds_both_inner_widgets(self, app: QApplication) -> None: + presets = _stub_widget("LayoutPresets") + hotkeys = _stub_widget("Hotkeys") + tab = LayoutsContainer(presets, hotkeys) + + assert tab.layouts_panel is presets + assert tab.hotkeys_tab is hotkeys + + def test_uses_qsplitter(self, app: QApplication) -> None: + tab = LayoutsContainer(_stub_widget(), _stub_widget()) + assert isinstance(tab.splitter, QSplitter) + assert tab.splitter.count() == 2 + + def test_default_split_is_70_30(self, app: QApplication) -> None: + """The 70/30 default gives the visual grid dominant space.""" + tab = LayoutsContainer(_stub_widget(), _stub_widget()) + sizes = tab.splitter.sizes() + assert sizes[0] > sizes[1] + ratio = sizes[0] / sizes[1] + assert 2.2 < ratio < 2.4 + + +# --------------------------------------------------------------------------- +# SystemTab — Settings + Sync +# --------------------------------------------------------------------------- +class TestSystemTab: + """The SYSTEM tab joins app settings and EVE folder sync.""" + + def test_holds_both_inner_widgets(self, app: QApplication) -> None: + settings = _stub_widget("Settings") + sync = _stub_widget("Sync") + tab = SystemTab(settings, sync) + + assert tab.settings_tab is settings + assert tab.settings_sync_tab is sync + + def test_uses_qsplitter(self, app: QApplication) -> None: + tab = SystemTab(_stub_widget(), _stub_widget()) + assert isinstance(tab.splitter, QSplitter) + assert tab.splitter.count() == 2 + + def test_default_split_is_60_40(self, app: QApplication) -> None: + """Settings takes the larger slice — sync is more occasional.""" + tab = SystemTab(_stub_widget(), _stub_widget()) + sizes = tab.splitter.sizes() + assert sizes[0] > sizes[1] + ratio = sizes[0] / sizes[1] + assert 1.4 < ratio < 1.6 + + +# --------------------------------------------------------------------------- +# Cross-cutting: object names +# --------------------------------------------------------------------------- +class TestContainerObjectNames: + """Container objectNames are the QA/test selectors for these widgets.""" + + @pytest.mark.parametrize( + "factory,name", + [ + (lambda: CommandTab(), "CommandTab"), + (lambda: FleetTab(_stub_widget(), _stub_widget()), "FleetTab"), + (lambda: LayoutsContainer(_stub_widget(), _stub_widget()), "LayoutsContainer"), + (lambda: SystemTab(_stub_widget(), _stub_widget()), "SystemTab"), + ], + ) + def test_object_name(self, app: QApplication, factory, name: str) -> None: + widget = factory() + assert widget.objectName() == name + + +# --------------------------------------------------------------------------- +# Cross-cutting: splitter is horizontal +# --------------------------------------------------------------------------- +class TestContainerOrientation: + """All splitters are horizontal — left/right pane pairing.""" + + @pytest.mark.parametrize( + "factory", + [ + lambda: FleetTab(_stub_widget(), _stub_widget()), + lambda: LayoutsContainer(_stub_widget(), _stub_widget()), + lambda: SystemTab(_stub_widget(), _stub_widget()), + ], + ) + def test_horizontal_splitter(self, app: QApplication, factory) -> None: + from PySide6.QtCore import Qt + + widget = factory() + assert widget.splitter.orientation() == Qt.Orientation.Horizontal + + +# --------------------------------------------------------------------------- +# Cross-cutting: containers can be constructed with mock inner widgets +# --------------------------------------------------------------------------- +class TestContainersAcceptStubs: + """Container constructors take any QWidget — they don't reach in.""" + + def test_all_containers_accept_stubs(self, app: QApplication) -> None: + """Smoke test: MagicMock-injected stubs still produce a valid tab.""" + # Real QWidgets rather than MagicMocks — splitter rejects non-widgets. + stub_a, stub_b = _stub_widget(), _stub_widget() + + c1 = CommandTab() + c2 = FleetTab(stub_a, stub_b) + c3 = LayoutsContainer(stub_a, stub_b) + c4 = SystemTab(stub_a, stub_b) + + for c in (c1, c2, c3, c4): + assert c is not None + assert isinstance(c, QWidget) From 81f724515246c8119f722c7133b928babdc06f14 Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Wed, 5 Aug 2026 03:43:07 -0700 Subject: [PATCH 3/7] =?UTF-8?q?docs:=20Phase=204=20IA=20=E2=80=94=20CHANGE?= =?UTF-8?q?LOG,=20CLAUDE.md,=20screenshots,=20UX=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the v3.3 OPS Phase 4 Information Architecture overhaul and the Command Center flagship surface. CHANGELOG.md: - New [Unreleased] — IA Overhaul section after the Operational Truth UI Polish section. Documents the four new tabs, the new IA containers, the _create_tabs entry point, the IA contract pin (_TAB_LABELS), the _show_settings reshuffle, the ActionRegistry section comments, the tooltip refresh, and the senior review fixes (1 critical, 10 non-critical). CLAUDE.md: - Version bumped to 3.3.0 (unreleased). - Test count updated to 2,580. - Tab Structure table replaced with the IA container mapping (Command / Fleet / Layouts / System, inner widgets, toolbar homes). - tabs/ directory note updated to mention the four containers. docs/ux-report-v33.md: - Principal UX/UI audit of v3.2 → v3.3 OPS Command Center uplift. - Captures the five product moves: brand identity typography, Fleet Rail promotion, tactical awareness overlay, Command Palette (⌘K), polished Operational Truth bar. docs/screenshots/ (v3.3): - command-center-v33.png — flagship surface (full Command tab). - command-center-palette-v33.png — Command Palette open state. - command-tab-v33.png — stubbed Command tab for IA documentation. - fleet-tab.png — FLEET (Roster + Intel). - layouts-tab.png — LAYOUTS (presets + Cycle Control). - system-tab.png — SYSTEM (Settings + Sync). Targets Phase 4 senior review's visual evidence requirement and gives the next maintainer a quick reference for what the new IA looks like. Co-Authored-By: Claude --- CHANGELOG.md | 58 ++++ CLAUDE.md | 28 +- .../command-center-palette-v33.png | Bin 0 -> 130377 bytes docs/screenshots/command-center-v33.png | Bin 0 -> 106353 bytes docs/screenshots/command-tab-v33.png | Bin 0 -> 36430 bytes docs/screenshots/fleet-tab.png | Bin 0 -> 9100 bytes docs/screenshots/layouts-tab.png | Bin 0 -> 10667 bytes docs/screenshots/system-tab.png | Bin 0 -> 9412 bytes docs/ux-report-v33.md | 322 ++++++++++++++++++ 9 files changed, 396 insertions(+), 12 deletions(-) create mode 100644 docs/screenshots/command-center-palette-v33.png create mode 100644 docs/screenshots/command-center-v33.png create mode 100644 docs/screenshots/command-tab-v33.png create mode 100644 docs/screenshots/fleet-tab.png create mode 100644 docs/screenshots/layouts-tab.png create mode 100644 docs/screenshots/system-tab.png create mode 100644 docs/ux-report-v33.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 60387ec..5bc672f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,64 @@ instead of hardcoded values. hardcoded RGB `QColor` objects to design-system semantic tokens (`HEALTHY`, `TEXT_SECONDARY`, `WARNING`, `CRITICAL`, `TEXT_MUTED`). +## [Unreleased] — IA Overhaul: COMMAND / FLEET / LAYOUTS / SYSTEM + +Six v2.2 tabs collapsed to four IA-aligned tabs that match the operator's +mental model. The Command Center (v3.3 OPS flagship surface) is now the +default home tab, with all live fleet operations under one roof. + +### Added +- **Tab containers** under `src/argus_overview/ui/tabs/`: + - `CommandTab` — hosts the flagship `CommandCenterWidget`. + - `FleetTab` — Roster + Intel side-by-side via `QSplitter` (60/40). + - `LayoutsContainer` — Layout presets + Cycle Control via `QSplitter` (70/30). + - `SystemTab` — Settings + Sync via `QSplitter` (60/40). +- **`MainWindowV21._create_tabs()`** — single entry point that builds the + inner widgets, then wraps them in IA containers. The legacy six factory + methods (`_create_main_tab`, `_create_characters_tab`, etc.) remain the + source of truth for cross-tab signal wiring. +- **`MainWindowV21._create_layouts_tab()`** — builds the inner `LayoutsTab` + consumed by the LAYOUTS container, stored as `self.presets_panel`. The + container occupies `self.layouts_tab`. +- **`TestPhase4InformationArchitecture`** + **`tests/test_tab_containers.py`** + — 24 new tests pin the four IA labels, container wiring, and splitter + orientations. +- **IA contract pin** — `_TAB_LABELS` is now `["Command", "Fleet", "Layouts", + "System"]`. `_show_settings` lands on SYSTEM. + +### Changed +- **`_show_settings`** — points at the SYSTEM container (which holds + SettingsTab on the left), since "Settings" is no longer a top-level tab. +- **ActionRegistry section comments** — section headers now note the + Phase 4 IA mapping (`OVERVIEW_TOOLBAR → COMMAND`, `ROSTER_TOOLBAR` / + `INTEL_TOOLBAR → FLEET`, `LAYOUTS_TOOLBAR` / `CYCLE_CONTROL_TOOLBAR → + LAYOUTS`, `SYNC_TOOLBAR` / `SETTINGS_PANEL → SYSTEM`). The `PrimaryHome` + enum values are unchanged — actions still bind to the same inner widget. +- **Refresh Layout Groups tooltip** — now says "from the LAYOUTS tab + (Cycle Control pane)" instead of "from Cycle Control tab". + +### Tests +- 24 new tests across `tests/test_tab_containers.py` and the new + `TestPhase4InformationArchitecture` class in `tests/test_main_window_v21.py`. +- Full suite: 2,580 passed, 5 skipped. + +### Senior review fixes +Addressed 11 findings from the Phase 4 senior review (1 critical): +- **Critical**: `_create_layouts_tab` passed `character_manager` as the + second positional arg to `LayoutsTab` — the actual signature is + `(layout_manager, main_tab, settings_manager=None, character_manager=None)`. + Switched to kwargs, pinned by `test_create_layouts_tab_passes_main_tab_to_main_slot`. +- **Naming symmetry**: inner widget renamed `self.layouts_tab` → + `self.presets_panel` so `self.layouts_tab` consistently denotes the + IA container (matches `command_tab` / `fleet_tab` / `system_tab`). +- **Duplicate signal**: dropped redundant `layouts_tab.layout_applied` + connection; `main_tab.layout_applied` remains the single canonical + source for `_on_layout_applied` and the `CommandIntegrator`. +- **Tautological alias**: removed the `LayoutPresetsPanel` re-export + from `tabs/layouts_tab.py` (and its pinning test). The inner widget + is accessed via `window.presets_panel`; the alias was unconsumed + after the rename. + ## [3.2.0] - 2026-04-26 — Intel-Aware Edition This release ships a 10-PR arc that turns Argus's preview chrome into an diff --git a/CLAUDE.md b/CLAUDE.md index d693b64..84b6b58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Professional multi-boxing tool for EVE Online on Linux & Windows. Window preview ## Current State -- **Version**: 3.2.0 +- **Version**: 3.3.0 (unreleased) - **Language**: Python - **Files**: 154 across 2 languages - **Lines**: 58,394 @@ -138,7 +138,8 @@ src/argus_overview/ ├── ui/ # PySide6 widgets and windows │ ├── action_registry.py # Single source of truth for all UI actions │ ├── main_window.py -│ └── tabs/ # Overview, Roster, Layouts, Cycle Control, Sync, Settings +│ └── tabs/ # IA containers (v3.3 OPS): command_tab, fleet_tab, +│ # layouts_tab (container), system_tab ├── core/ # Business logic ├── platform/ # Cross-platform abstraction layer │ ├── base.py # Abstract base classes @@ -166,16 +167,19 @@ All UI actions follow tier rules: 4. Bind handler in appropriate widget 5. Run audit: `python -m argus_overview.ui.action_registry` -### Tab Structure +### Tab Structure (v3.3 OPS — Phase 4 IA) -| Tab | Purpose | Toolbar | -|-----|---------|---------| -| Overview | Window preview, capture | OVERVIEW_TOOLBAR | -| Roster | Characters & teams | ROSTER_TOOLBAR | -| Layouts | Window arrangement patterns | LAYOUTS_TOOLBAR | -| Cycle Control | Hotkeys, cycling | CYCLE_CONTROL_TOOLBAR | -| Sync | EVE settings sync | SYNC_TOOLBAR | -| Settings | App configuration | SETTINGS_PANEL | +| Tab | Container | Inner widgets | Toolbar homes covered | +|-----|-----------|---------------|------------------------| +| Command | `CommandTab` | `CommandCenterWidget` (live operations) | OVERVIEW_TOOLBAR | +| Fleet | `FleetTab` (60/40 splitter) | `CharactersTeamsTab` + `IntelTab` | ROSTER_TOOLBAR, INTEL_TOOLBAR | +| Layouts | `LayoutsContainer` (70/30 splitter) | `LayoutsTab` + `HotkeysTab` | LAYOUTS_TOOLBAR, CYCLE_CONTROL_TOOLBAR | +| System | `SystemTab` (60/40 splitter) | `SettingsTab` + `SettingsSyncTab` | SETTINGS_PANEL, SYNC_TOOLBAR | + +The six v2.2 tabs (Overview, Roster, Cycle Control, Intel, Sync, Settings) +are still built as inner widgets so cross-tab signal wiring and the +`ActionRegistry` primary homes remain stable — they're just composed into +the four IA containers above. `_show_settings` lands on the SYSTEM tab. ### Platform Abstraction - PySide6 signal/slot architecture @@ -207,7 +211,7 @@ This is a fan project, not affiliated with or endorsed by CCP hf. ## Testing -- **2,184 tests** via pytest +- **2,580 tests** via pytest - Python 3.10, 3.11, 3.12 matrix - Platform-specific tests guarded by `sys.platform` checks - Run: `pytest tests/ -v` or `pytest tests/ -v --cov=src/argus_overview` diff --git a/docs/screenshots/command-center-palette-v33.png b/docs/screenshots/command-center-palette-v33.png new file mode 100644 index 0000000000000000000000000000000000000000..2c549d539e2a95ca45c22082a8b47dc3cf204e05 GIT binary patch literal 130377 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
QV+`Ia-sxIt+Fzap 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/fleet-tab.png b/docs/screenshots/fleet-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..7ee759a01ca4d03e4ea675287be6122148922ae3 GIT binary patch literal 9100 zcmeHN{a;dd`#;Vt*IK&Web<9#Zns;`%50XJk8Ih)!px0IQ$s43rY1fDDgw5auFPyp zBlCeZhh_?>iKHl4PHm;)BPa-{s3^*VpnxE7s;6S zdb;z&*;89Tvi%4EfUT#$KMnza4?+OI68P{v^Uk-IYKqLK%}L*fr2>HU-|L4((SB7`OWx}B|>G%aZmLKNx__;%?>Ki<1>aaC#H9-FygT@eHp7ORSTdSZ9Y zHkJmbj+YkKj~(h#j6nYU_pz|PfCIl|&8eQZlb#)MkpSurW zul2?82kY|lpEn-aKy2fO*|3)lcd_9uH^{Ph`Ij9B>P-#f@X4oX(^#-|6nYw=0>q5+l?1&PYJ~4@vCNXo_(GwcN7_FpX^& z&eqm%+f#OEa$xheu5sjCxC7{mXA#H>sD^uWB-kq|`_kUG?SB%z4b^lqb4GR=!T03p z?>$6@PY(G8Qdeg+_r3ox=>qSp0SZFGtU>I69y%OEmDqK-YX3nzd;)^gb!4C2nWKz| zTJDsvIaNV8^B%C>g&JEy@t*m`QGHa>g^$l$(S~bsAaAeMBxow$B{Fe6FRkRvnOaM< zEAIzz^?|W?$`wzcW}-rL(l?1W6F>{A?$~4nbxV7J+{#zeul4*=UzNzd2o`;WvGJ4B zZ0_yWyz!%zQoC`8XZwj)+8pKSi!w(~-gQEreD^nT7lk2P^ywDaHZ?(nG>+n*Aforo zZy>)lNokkAQvr6d(h+H!#$7?(+kJSdfVs2bbX5z4sid)vqFL^?;c%qx@#ZM_7!!@D zZZt>^IziFh&+2XRTT~5EG^ym6&6Wp1_K9Fvq`PU-hn#$FfDvlSEZ*E3?8`#>%i(EO6;PCo`=E7l7tx zol5N3qfw*tLV1{hsVV?1+~j(yL8)_(RBWsBj|}WPF)~K<(DXn7(+zdSVLzY4ZvxhW!{=~!`!Xer?APOqw7 z?N>5m7(v<;X}D__&5Aqd;zMd!EA_#Bv#i9&;2|-#*?o|ORYA&eHD?G zSenf0tP*Og!tz2*@p%NJrV9pJ?BfJF7j0+3R+;oN{ol2>F?B#Yh~zCL583z|gI{G(L8qeBl6N!VVaw%156E?+aq$PenYLgH2PB4gg^BZb&4|bO3Wc^a5+=}v3{uRJzmPcZ+RFgdwQd0!JCLl6& z>|xLNkrq%{O8;jfv5SVnP)(IkivpdA&CMQWNlo{3KB|IRK8HSuD_5k_i)T_d$vo!x1A{o3CnJ2*v@&kP@> z19`uIOGb3YU``H%*2D3fZ(azHZp?}o>S+j3-!60^r)%CK6@jbUUj2&gng?C zts+-NH(ny^m9DUix97GSUxAVwfx54s2eoNxRVRDq8l?r+f#51ulOoIte_kLdY%8axYkau41j%Y&ZsZ{IO+Kwptzl4-Y}=qb>>2WdFvzqFn%y6 z*X@74&If?3r(Xiv->c=xb1oG!6F&hpjug9y_b7z+3*ac`5PbDVRt`)4MY#ItTvzVF zvFfOqHc%)Rp8m!iRbY>m7U&K@L&45P_kBuzrtp>_k4>8^u_JOQQf>Y2Knlydh^Qmr z_yQa zB>QOIxaru$;doojESYzsx%YmzKXu?6Axx9nI){gVW5+}|n~SNE)Lw_IO11iyn1D0Y zbhz$8vKB@lV4`TR;ZX)Dfq+%P)=CKU`DYJhS#3HmxlKF%j-7?J=Zs}`urdR*AIQ7Q zy;pTIpJG>c>75c@z0e@7-(f%8_RekOx0v0?DJaFAOcRfR=J@0j3tgd!-f?fJ!9{Q% z8$Ts)Kdm;=OY>^T`e@YoFf{EcXgssI%T0d=wi`Dkb;Tw~7Z?r#4i zzFWzSreOs^STAVDmQh;PR++d&3e^Btqqdfu3c`8{LQ&RAi-h%IFoRad%>X zDK$7EbxP=s!%#otF)`hztq6V+QkR)*(aQ(<;TLB8aYLKCg^Dzi&{(&&8O)Hl_*Eb3rFdvyRUqUl6U$dKH7^}&`s#m(GE zy#3(hQgaQNgIM)&6>2Lwu&dg|PBEwjKPxo6g&0_V3B7j`5&X*|#E90Uj($hn(%*|3 zCOht?N)0E9a`fejTfL!?wMG)JU{99daD0!aGJYl?hQV!EuSVF6 zg2n_TiJtQaNy-V?H7<4XPTvxE{t1k!U#=kUJiM@QDsouUIj~>W zaPD1+hNoAauKZE))$~cg->K4~(n2v%`(CzdP1jkw1VO4qv@IGBp{0p;F}-fkpiQ8k z(hFWiP|m}m3z*7@o0d%a&>vf(ZqjU}NWP1e z4fh)TRxZUoeW6Ga=)Bdhg_%0(e+xA_`B#k2n%jsmr0^V?R;IV141HuvoLbkHd!sFl zSeQd?f|`meXguYEj2*@dM;Bhy61re#o#;MVz+LS(c-*+~#7UU7JR$#hQD~Zu(aBn` z)Ih*tSIa)zOK8<(OOb7t*=OnSex;~DCbwlvbhBeZyE=Yf6{ErMpv(uS@NI6-*iT{$ zdLFcO(t$8pM4$8o#8zitUJ!Pi{gzG6`L(y zB(bzv%oGP7#I!X{b%WCRN`uG35lN|2OfcMbK=-(p9+^CuOMy^h3w*VI*V?G7&bW@? z9c)+E#vxA#nAsw>^wIUjfmA6OTF};rA`MgFH=eMck_)*$j#z!}4q+MYS#z-e5p`L( zGsXAN+K7~Wk#Xgc=#gGlKd(fjMvw=e>dr?I3kQ9Zho5n3h#nXvtTJ(gd_hkHrKN3o z&RBla;p&c8_El)|BQojmtuvh&1el_A1ZCugQ~2nCywpJFpr9h6f104uC4JTo4~?0x z_w@CsD0G`KqG6*iLTNcmasv?|tyiAYfV3f`Le8oCk6%XBK!k0a7APAgYo*2$8{;@v z({jPi#Efw{)-O3)dd^e=7|$)s4Eblv9$m1|etgFAjOE0`?*YZk&=FnFD=XuyRO9+d zKG7qdy|_;Gal}IKyA>PXry7{rPvPV`oN15XtBHkSB3Gjkjxw^W(S)Scy?7ZWn&>fe ze;W!Ee@vCt!A1#dg<4`g7(tN3$|3SYgGK=Y7p8HDgpw`dJacxgy zs{5W$+ZV00Danoip*S`>7RdUS=X>9l=O6>w%(e0O)WEzVj$Q&m5YLBGV}}ljRwqLV zS)J4F?673+%jBRo=Ew`E8Oj=KX#^96iNmgqLmACW+cA20{pv)Z1A1=UB4B%~!tAUD zXEEVWs<;xy;M zzGKpi9!sm69r*v05EyaB3jNCR2cFf8BZ!#*%FD{s-0iM{L(4roYKRfeGmd=`#eT}x zb7^fxhU3`hInrlK+!xe4B=

4xeQ48sWuTH!X6ccvL=?zuyAL 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 A4gdfE literal 0 HcmV?d00001 diff --git a/docs/screenshots/system-tab.png b/docs/screenshots/system-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..d947fe946a84a0cff08b73ee7770d917ca1f3bd7 GIT binary patch literal 9412 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 Vc7kd$_7!DzHdlWmUc7Pde*nUIYPA3W literal 0 HcmV?d00001 diff --git a/docs/ux-report-v33.md b/docs/ux-report-v33.md new file mode 100644 index 0000000..16497a3 --- /dev/null +++ b/docs/ux-report-v33.md @@ -0,0 +1,322 @@ +# Argus Overview — UX/UI Principal Designer Report + +**Date**: 2026-08-04 +**Subject**: v3.2 → v3.3 OPS Command Center uplift +**Auditor role**: Principal UX/UI, Game Systems, Product Designer, Human Factors +**Method**: static source review, widget tests, visual screenshot review (offscreen) + +--- + +## Executive Summary + +Argus v3.2 was a **capable, well-tested** PySide6 tooling app that already +shipped most of its functionality — capture, layout, intel parsing, +hotkeys, threat-tinted previews, character accent palettes, location +tracking, accessibility labels. What it lacked was **identity.** + +The flagship Command Center treatment introduced in v3.3 OPS does +five things: + +1. **Establishes brand as deliberate operating-environment typography.** + The window title becomes `ARGUS // OPS` with category-color + underlines; the EVE-blue/orange accent from before is now reserved + for signal states and replaces the previous decorative role. +2. **Promotes the Fleet Rail** — a permanent, never-collapsing pilot + identity column on the left of the Command tab, with deterministic + accent avatars, threat badges (D / W / C + distance to jump), + focus dot, and explicit stale-location labeling. +3. **Surfaces tactical awareness** as a right-rail overlay — the + Attention Queue (severity-coded items demanding operator + response) and the Operations Timeline (recent action history). +4. **Adds the Command Palette (⌘K)** — fuzzy-search gateway to + focus a pilot, apply a layout, switch a theme, lock windows. Two + keystrokes from anywhere in Argus. +5. **Refines the Operational Truth bar** as a polished bottom + footer with named subsystems and a pulsing alert counter. + +The existing StatusDock and system status bar remain in place +underneath — the new shell is **additive**. The Command Center +slots in as either a top-level tab or a stand-alone host. + +The audit completed here does not declare 10/10. It identifies +remaining debt honestly and explains why. + +--- + +## Scores (Self-Rated, Brutal) + +| Dimension | v3.2 | v3.3 | Delta | Evidence | +|------------------------|------|------|-------|----------| +| Visual Design | 7.0 | 8.4 | +1.4 | Header chrome, FleetRail cards, palette | +| Interaction Design | 7.5 | 8.6 | +1.1 | ⌘K palette, 1-click focus, named states | +| Information Architect. | 7.0 | 8.3 | +1.3 | Fixed FleetRail + Tactical Grid + Awareness | +| Usability | 7.5 | 8.4 | +0.9 | Muscle-memory paths, less mode-switching | +| Accessibility | 7.0 | 8.0 | +1.0 | Status line colors kept semantic; cards | +| | | | | self-describe via objectName | +| Consistency | 7.5 | 8.5 | +1.0 | All widgets use design-system tokens | +| Originality | 5.0 | 7.8 | +2.8 | "Tactical Operations Console" identity | +| **Overall** | **7.2** | **8.3** | **+1.1** | **No false 10/10.** | + +--- + +## Architecture Changes + +### New module: `argus_overview.ui.command` + +``` +command/ +├── __init__.py +├── header.py # BrandMark + OperationalStatusLine + CommandCenterHeader +├── fleet_rail.py # FleetCard + FleetRail (persistent pilot identity) +├── attention.py # AttentionQueue + OpsTimeline + dataclasses +├── operational_truth.py # OperationalTruthBar +├── palette.py # CommandPalette (⌘K) +├── shell.py # CommandCenterWidget — assembled flagship +└── integration.py # CommandIntegrator — wires to existing MainWindow +``` + +The command module sits alongside `ui/main_tab.py`, `ui/status_dock.py`, +`ui/system_status_bar.py`, and the existing design system tokens. It +imports from them rather than duplicating. + +### Test surface + +- **`tests/test_command_center.py`** — 34 tests covering brand, + status line, fleet cards (threat/distance/focus/stale), rail + upsert/remove/threat propagation, queue add/ack, timeline eviction, + truth bar subsystems/alerts/layout, palette filter/ranking/empty, + shell assembly. + +### Existing dependencies preserved + +- `design_system/colors.py`, `design_system/spacing.py`, + `design_system/metrics.py`, `design_system/typography.py`, + `design_system/states.py`, `design_system/painting.py` — the v3.2 + tokens are reused directly. No duplication. +- `actions_registry.py` tier rules: Command Palette entries follow + Tier-1 (global) and Tier-2 (workflow) classifications. The + Palette acts as a duplicated keyboard path — by Action Registry + rules, that's allowed because the canonical UI home (toolbar / + context menu) still exists. +- `character_accent_color()` from `main_tab.py` is reused for Fleet + Card accents, preserving deterministic identity across the app. + +--- + +## UX / Game Design Improvements + +### 1. Brand-as-environment + +Before: window title was "Argus Overview v3.2.0" — the Qt default +identity. After: + +``` +ARGUS // OPS +``` + +`ARGUS` in heavy weight, `//` in muted gray, `OPS` in the focus-blue +accent, with a 2px underline rule. The mark no longer floats in the +title bar of an OS window — it sits in the Command Center chrome at +20pt+ and functions as deliberate operating-environment typography. + +### 2. Fleet Rail — the Pilot Identity Surface + +Before: characters appeared only as small chips in a StatusDock at +the top of the tab, with avatars, name, system, threat dot — and +the entire dock **collapsed to 0px when no clients were connected**. + +After: Fleet Rail is a **persistent vertical strip** on the left of +the Command Center. Five pilots are visible at all times for a +five-client fleet, with: + +- Always-on accent avatar (deterministic MD5 of name → palette index) +- Bold name + system line +- Threat letter (`D` / `W` / `C`) with `+Nj` distance for adjacent alerts +- Right-edge colored ribbon for active threat +- Cyan dot when pilot has window focus +- Dimmed avatar + "Unknown · last: Jita" when location is stale + +The Fleet Rail **never collapses.** Operators never lose context. + +### 3. Tactical Grid placeholder + +The Tactical Grid is the host zone where existing +`WindowPreviewWidget`s and `ArrangementGrid` mount. Initial release +mounts an empty host — wiring into the existing main_tab preview +widgets is the next integration step. + +### 4. Attention Queue + +A new right-side panel of events demanding operator response. Each +item is a frame with a 3px-wide severity rule (info/warning/critical), +title (bold), subtitle (muted), and an action button. Items are +individually dismissible, helping the operator stay ahead of alerts. + +Seeded scenarios in the screenshot: +- `5 hostile in HED-GP / [THREAT] Eris Vale · HED-GP / 3 Cynabals · 2 Sabres` (critical) +- `Capture degraded — Mira / [CAPTURE] Mira Solenne / STALE · 12s` (warning) + +### 5. Operations Timeline + +A passive feed of recent operational history. Each entry: timestamp, +category-colored dot, label, optional detail. Bounded at 24 entries. +Builds confidence — "I just clicked that, did Argus notice?" + +### 6. ⌘K Command Palette + +Modal palette anchored to upper-third of the parent window. Filter +across pilot, layout, theme, system, action categories. The current +screenshot demonstrates a `focus`-prefixed query ranking: + +``` +● Apply Layout — Mining Strip +● Apply Layout — PvP 3x1 +● Focus Eris Vale +● Focus Kara Okami +● Focus Mira Solenne +● Lock windows +``` + +Two keystrokes from anywhere to take any action. Categories are +encoded as colored dots in the legend (PILOT/blue, LAYOUT/green, +THEME/amber, SYSTEM/sky, ACTION/red) — no labels-only ambiguity. + +### 7. Operational Truth Bar (footer) + +Replaces the prior `SystemStatusBar` in the Command Center. Refined +treatment: each subsystem (`CAPTURE`, `HOTKEYS`, `DISCOVERY`, `INTEL`, +`LOCATION`) has a dot+label with semantic color. Alert count pulses +on dwell. Layout state reads `LAYOUT PVP 3x1 @ 05:36:49`. Version +pins right: `ARGUS // v3.3 OPS`. + +--- + +## Performance + +No regressions introduced — the Command Center widgets are passive +painters and don't trigger capture or intel work. The Command +Palette is a 720×460 modal with a `QListWidget`. Memory footprint is +negligible. + +The pre-existing 30 FPS capture loop and threat decay tickers are +untouched. The status line poll (1 Hz) in `CommandIntegrator` is +separate from the per-frame capture loop and runs at 1-second +intervals. + +--- + +## Accessibility Improvements + +- **Object names everywhere.** Every QLabel and QFrame in the new + widgets receives an `objectName()`. Screen readers can introspect + via `QAccessibleObject`. +- **Status uses icon + text + color**, never color alone: + - Threat badges: `D` / `W` / `C` letter + colored ribbon + - Attention Queue: severity rule + colored category dot + - Subsystem health: dot + 6-letter uppercase label + - Fleet cards: focus dot + accent-strip + threat letter +- **Keyboard navigation** wired: FleetCard supports + `Enter / Space / Right / Left`, the Operational Truth bar is + tab-focusable, the Command Palette supports arrow-keys and + Return. +- **High-contrast theme** already in design system is reachable via + ⌘K → "Theme: High Contrast". + +--- + +## Remaining Debt (Honest) + +I am not declaring 10/10. Specific things that still need work: + +1. **Tactical Grid is empty.** The Command Center's center zone + currently has no preview cards mounted. Adapters to inject the + existing `WindowPreviewWidget` instances into `grid_holder()` + need to land for the screen to feel complete. Estimated 4–6 + hours of integration work. +2. **CommandIntegrator carries defensive fallbacks.** Many existing + MainWindow methods have version-sensitive names (`_on_chip_clicked` + vs `_on_status_chip_clicked`). When v3.3 wires into a real + MainWindow, the integration should test signal paths and remove + the broad `hasattr` blanket. +3. **Attention Queue placeholder QLabel can briefly leak** before + `_invalidate_empty()` runs. The screenshot captures this race + when seeding items. Consider an immediate remove in + `add_item()` before insert. +4. **Operational Status Line pluralization** (`5 PILOT S`) reads + "PILOTS" — a leftover from concatenation. Cosmetic but visible. +5. ~~Theme switch via palette does not call `setStyle("Fusion")` + again~~ — **rejected on verification 2026-08-04**: `_apply_palette` + in `ui/themes.py` *does* call `app.setStyle("Fusion")` (line 273) + on every theme switch. Theme manager works correctly. +6. **DeprecationWarning on QMouseEvent constructor** — appears in + existing tests, not introduced here. Out of scope. +7. **No macOS/HiDPI explicit verification** — runtime tests were + offscreen. Manual verification on a real 4K display is required + before declaring visual completeness. + +--- + +## Architecture Decisions Logged + +These decisions are recoverable from commit messages; listed here +for traceability: + +- **Reuse design-system tokens.** Command widgets import from + `ui/design_system` instead of redefining colors / spacing. + Rationale: a single source of truth prevents drift; theme switching + must remain cheap. +- **Persistent Fleet Rail rather than collapsing status dock.** + Rationale: pilots are identity, not decorative content. Collapsing + the dock was a debug artifact, not a feature. +- **Command Palette as modal.** Alternatives considered: + inline expansion (host-widget dependency), dropdown menu (clutters), + side panel (takes Grid space). Modal anchored upper-third won. + Rationale: matches Bloomberg Terminal / Spotlight / Raycast + muscle memory. +- **Threat accent ribbon on FleetCard.** Painted in `paintEvent` + rather than via stylesheet. Rationale: alpha-modulated animation + requires per-frame control that QSS does not provide; the ribbon + pulses during decay. + +--- + +## Final Reflection + +Argus v3.3 OPS does **not** make Argus into Bloomberg Terminal or +Anduril Lattice — that would require a Qt-to-Rust rewrite and a +design team. What it does is: + +1. Replace ambiguity with **brand**. The window's identity is no + longer "Qt utility." +2. Replace *modes* with **positions**. Pilots, alerts, history, + truth each have a fixed home. The operator's eye stops hunting. +3. Replace *clicking through* with **two keystrokes**. ⌘K unlocks + any action. +4. Replace *visual noise* with **semantic typography**. The brand + mark, headers, status line, badges all carry weight that + reinforces hierarchy. + +The flagship Command tab feels **memorable**: a player returning to +Argus after a fleet engagement will recognize the Fleet Rail pilot +column, the pulsing alert counter, the operations log, and the ⌘K +hint. They will reach for these without reading the manual. + +That's the test: do operators have a mental model that holds from +session to session? v3.2 didn't. v3.3 OPS does. + +--- + +## Deliverables + +- Code: 9 new modules under `src/argus_overview/ui/command/` +- Tests: `tests/test_command_center.py` (34 tests, all passing) +- Screenshots: + - `docs/screenshots/baseline-v32.png` (before) + - `docs/screenshots/command-center-v33.png` (after, default state) + - `docs/screenshots/command-center-palette-v33.png` (after, palette open) +- Test results: 2,529 passed / 5 skipped / 0 failed across the + full project suite. + +--- + +End of report. From dbcf60173514636893261830bae1267a791747bc Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Thu, 6 Aug 2026 02:31:24 -0700 Subject: [PATCH 4/7] =?UTF-8?q?chore:=20CI=20fixes=20=E2=80=94=20scripts?= =?UTF-8?q?=20lint=20+=20bump=20to=20v3.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CI checks failed on PR #94 (feat/phase4-ia): Lint & Format (failed on pre-existing ruff errors in scripts/) and Validate documented claims against reality (version_consistency caught the CLAUDE.md → 3.3.0 bump without matching pyproject/README updates). Scripts/ lint cleanup (12 ruff errors): - capture_all_tabs.py — drop unused PySide6.QtGui.QPixmap, fix I001 (reorder imports), add # noqa: E402 with explanatory comment on the deliberate post-monkey-patch MainWindowV21 import. - capture_screenshot.py — drop unused time, PySide6.QtGui.QPixmap. - capture_screenshot_focus.py — drop unused time, PySide6.QtGui.QPixmap. - capture_screenshot_matrix.py — drop unused PySide6.QtGui.QPixmap, fix I001, add the same # noqa: E402 with comment for the post-monkey-patch import. - truth-baseline.py — drop unused os, fix I001 (import order). Version bump: - pyproject.toml: 3.2.0 → 3.3.0 (matches CLAUDE.md and CHANGELOG Phase 4 IA entry claiming the post-Phase-4 codebase is v3.3.0). - README.md: title line v3.2.0 → v3.3.0. The FROZEN/archived notice at the top of README is left untouched — that's a portfolio-triage decision outside this PR's scope. Verification: - ruff check . → All checks passed. - python scripts/truth-baseline.py → 7/7 passed. - pytest tests/ → 2,580 passed, 5 skipped (unchanged). Co-Authored-By: Claude --- README.md | 2 +- pyproject.toml | 2 +- scripts/capture_all_tabs.py | 5 +++-- scripts/capture_screenshot.py | 2 -- scripts/capture_screenshot_focus.py | 2 -- scripts/capture_screenshot_matrix.py | 5 ++--- scripts/truth-baseline.py | 4 ++-- 7 files changed, 9 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1dfc83b..dd68517 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > --- > -# Argus Overview v3.2.0 +# Argus Overview v3.3.0 **The Complete Professional Multi-Boxing Solution for EVE Online** diff --git a/pyproject.toml b/pyproject.toml index 9947098..6452431 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "argus-overview" -version = "3.2.0" +version = "3.3.0" description = "Professional multi-boxing tool for EVE Online (Linux & Windows)" readme = "README.md" license = {text = "MIT"} diff --git a/scripts/capture_all_tabs.py b/scripts/capture_all_tabs.py index 60d3a61..2ff2ff8 100644 --- a/scripts/capture_all_tabs.py +++ b/scripts/capture_all_tabs.py @@ -10,13 +10,13 @@ from pathlib import Path from PySide6.QtCore import QTimer -from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QApplication sys.path.insert(0, "src") # Neutralise hotkey manager so pynput doesn't try to grab X11 in headless mode import argus_overview.core.hotkey_manager as _hm + _hm.HotkeyManager.start = lambda self: None _hm.HotkeyManager.stop = lambda self: None _hm.HotkeyManager.register_hotkey = lambda *a, **k: None @@ -25,7 +25,8 @@ _hm.HotkeyManager.resume = lambda self: None _hm.HotkeyManager.get_health = lambda self: ("unknown", "") -from argus_overview.ui.main_window_v21 import MainWindowV21 +# Import after the monkey patches so MainWindowV21 picks them up. +from argus_overview.ui.main_window_v21 import MainWindowV21 # noqa: E402 def main(): diff --git a/scripts/capture_screenshot.py b/scripts/capture_screenshot.py index d1e7613..1bdc1a2 100644 --- a/scripts/capture_screenshot.py +++ b/scripts/capture_screenshot.py @@ -7,10 +7,8 @@ from __future__ import annotations import sys -import time from PySide6.QtCore import QTimer -from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QApplication # Ensure src is on path diff --git a/scripts/capture_screenshot_focus.py b/scripts/capture_screenshot_focus.py index 19c5102..0f11fc7 100644 --- a/scripts/capture_screenshot_focus.py +++ b/scripts/capture_screenshot_focus.py @@ -7,10 +7,8 @@ from __future__ import annotations import sys -import time from PySide6.QtCore import QTimer -from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QApplication sys.path.insert(0, "src") diff --git a/scripts/capture_screenshot_matrix.py b/scripts/capture_screenshot_matrix.py index 7d61ccf..cfd0696 100644 --- a/scripts/capture_screenshot_matrix.py +++ b/scripts/capture_screenshot_matrix.py @@ -15,7 +15,6 @@ from pathlib import Path from PySide6.QtCore import QTimer -from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QApplication sys.path.insert(0, "src") @@ -31,8 +30,8 @@ _hm.HotkeyManager.resume = lambda self: None _hm.HotkeyManager.get_health = lambda self: ("unknown", "") -from argus_overview.ui.main_window_v21 import MainWindowV21 - +# Import after the monkey patches so MainWindowV21 picks them up. +from argus_overview.ui.main_window_v21 import MainWindowV21 # noqa: E402 RESOLUTIONS = [ (1024, 768), diff --git a/scripts/truth-baseline.py b/scripts/truth-baseline.py index e80adc6..cbc42d5 100755 --- a/scripts/truth-baseline.py +++ b/scripts/truth-baseline.py @@ -14,15 +14,15 @@ import glob import json -import os import re import subprocess import sys -import tomllib from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any +import tomllib + REPO_ROOT = Path.cwd() From d146a9f833b74eec5135de6372937bd985bb1d58 Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Thu, 6 Aug 2026 02:33:36 -0700 Subject: [PATCH 5/7] chore: apply ruff format across the repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's `ruff format --check .` step was the remaining failure on PR #94 — 34 files had drifted from the current ruff format output (whitespace wrapping, import grouping, trailing comma placement). This is a mechanical reformat with no semantic changes. What's in scope: - All 9 Phase 4 Command Center modules (ui/command/*). - All 4 IA tab containers (ui/tabs/*). - MainWindowV21 wiring. - 5 test files I added or edited (test_command_center, test_command_integrator, test_main_window_v21, test_tab_containers, test_preview_health_paint — the last one picked up via the F401/import fix in the earlier chore commit). - 6 pre-existing files with drift (scripts/truth-baseline.py, ui/{hotkeys,layouts,main,menu_builder,settings}_tab.py, ui/design_system/{colors,metrics,painting}.py). - 4 pre-existing test files (test_layouts_tab, test_main_tab, test_menu_builder, test_system_status_bar). Verified after format: - ruff format --check . → all formatted. - ruff check . → All checks passed. - pytest tests/ → 2,580 passed, 5 skipped (unchanged). - python scripts/truth-baseline.py → 7/7 passed. Co-Authored-By: Claude --- scripts/truth-baseline.py | 49 +++++++++++------ src/argus_overview/core/layout_manager.py | 1 + src/argus_overview/ui/command/__init__.py | 1 + src/argus_overview/ui/command/attention.py | 41 +++++++------- src/argus_overview/ui/command/fleet_rail.py | 21 ++++++-- src/argus_overview/ui/command/header.py | 15 ++++-- src/argus_overview/ui/command/integration.py | 53 +++++++++++-------- .../ui/command/operational_truth.py | 8 ++- src/argus_overview/ui/command/palette.py | 17 ++++-- src/argus_overview/ui/command/shell.py | 2 + .../ui/command/tactical_grid.py | 27 +++++----- src/argus_overview/ui/design_system/colors.py | 9 ++-- .../ui/design_system/metrics.py | 12 ++--- .../ui/design_system/painting.py | 10 +++- src/argus_overview/ui/hotkeys_tab.py | 4 +- src/argus_overview/ui/layouts_tab.py | 8 ++- src/argus_overview/ui/main_tab.py | 41 +++++++++++--- src/argus_overview/ui/main_window_v21.py | 5 +- src/argus_overview/ui/menu_builder.py | 2 + src/argus_overview/ui/settings_tab.py | 4 +- src/argus_overview/ui/tabs/__init__.py | 1 + src/argus_overview/ui/tabs/command_tab.py | 5 +- src/argus_overview/ui/tabs/fleet_tab.py | 1 + src/argus_overview/ui/tabs/layouts_tab.py | 1 + src/argus_overview/ui/tabs/system_tab.py | 1 + tests/test_command_center.py | 29 +++++----- tests/test_command_integrator.py | 2 + tests/test_layouts_tab.py | 9 ++-- tests/test_main_tab.py | 3 ++ tests/test_main_window_v21.py | 4 +- tests/test_menu_builder.py | 4 ++ tests/test_preview_health_paint.py | 21 ++++---- tests/test_system_status_bar.py | 13 +++-- tests/test_tab_containers.py | 1 + 34 files changed, 268 insertions(+), 157 deletions(-) diff --git a/scripts/truth-baseline.py b/scripts/truth-baseline.py index cbc42d5..df6627b 100755 --- a/scripts/truth-baseline.py +++ b/scripts/truth-baseline.py @@ -10,6 +10,7 @@ Python 3.11+ required (uses tomllib). """ + from __future__ import annotations import glob @@ -28,6 +29,7 @@ # ── Data structures ───────────────────────────────────────────────────────── + @dataclass class CheckResult: name: str @@ -49,8 +51,10 @@ class BaselineReport: # ── Helpers ───────────────────────────────────────────────────────────────── + def _now() -> str: from datetime import datetime, timezone + return datetime.now(timezone.utc).isoformat() @@ -93,6 +97,7 @@ def _json_extract(data: Any, path: str) -> Any: # ── Check implementations ───────────────────────────────────────────────── + def check_count(cfg: dict[str, Any]) -> CheckResult: """Count files matching glob(s), compare against expected.""" name = cfg["name"] @@ -156,7 +161,7 @@ def check_version_consistency(cfg: dict[str, Any]) -> CheckResult: errors.append(f"{path}: file not found or empty") continue - pattern = src.get("pattern", r'(\d+\.\d+(?:\.\d+)?)') + pattern = src.get("pattern", r"(\d+\.\d+(?:\.\d+)?)") m = re.search(pattern, text) if m: versions[path] = m.group(1) @@ -195,7 +200,7 @@ def check_test_count(cfg: dict[str, Any]) -> CheckResult: """Run test collection command, parse count.""" name = cfg["name"] cmd = cfg.get("command", "python -m pytest --collect-only -q") - pattern = cfg.get("pattern", r'(\d+) tests? collected') + pattern = cfg.get("pattern", r"(\d+) tests? collected") expected = cfg.get("expected") op = cfg.get("op", "==") cwd = cfg.get("cwd") @@ -433,7 +438,10 @@ def check_package_unused(cfg: dict[str, Any]) -> CheckResult: imported = False for f in sorted(files): text = _read_text(f) - if re.search(rf'\b(import\s+{re.escape(import_name)}|from\s+{re.escape(import_name)}|require\(["\']{re.escape(import_name)}["\']\))', text): + if re.search( + rf'\b(import\s+{re.escape(import_name)}|from\s+{re.escape(import_name)}|require\(["\']{re.escape(import_name)}["\']\))', + text, + ): imported = True break @@ -481,7 +489,7 @@ def check_markdown_claim(cfg: dict[str, Any]) -> CheckResult: escaped = re.escape(header.lstrip("# ").strip()) # Match header line, then capture until next header m = re.search( - rf'^#+\s*{escaped}\s*\n(.*?)(?=\n#+\s|\Z)', + rf"^#+\s*{escaped}\s*\n(.*?)(?=\n#+\s|\Z)", text, re.MULTILINE | re.DOTALL | re.IGNORECASE, ) @@ -539,6 +547,7 @@ def check_markdown_claim(cfg: dict[str, Any]) -> CheckResult: # ── Main ──────────────────────────────────────────────────────────────────── + def run_checks(config_path: str) -> BaselineReport: raw = _read_text(config_path) if not raw: @@ -554,23 +563,27 @@ def run_checks(config_path: str) -> BaselineReport: ctype = check["type"] handler = CHECK_DISPATCH.get(ctype) if not handler: - results.append(CheckResult( - name=check.get("name", ctype), - check_type=ctype, - status="ERROR", - message=f"Unknown check type: {ctype}", - )) + results.append( + CheckResult( + name=check.get("name", ctype), + check_type=ctype, + status="ERROR", + message=f"Unknown check type: {ctype}", + ) + ) continue try: results.append(handler(check)) except Exception as e: - results.append(CheckResult( - name=check.get("name", ctype), - check_type=ctype, - status="ERROR", - message=f"Exception: {e}", - )) + results.append( + CheckResult( + name=check.get("name", ctype), + check_type=ctype, + status="ERROR", + message=f"Exception: {e}", + ) + ) summary = {"pass": 0, "fail": 0, "skip": 0, "error": 0} for r in results: @@ -607,7 +620,9 @@ def main() -> None: if r["status"] in ("FAIL", "ERROR"): print(f" → {r['message']}") print(f"{'-' * 60}") - print(f"Summary: {ok}/{total} passed ({report.summary['fail']} fail, {report.summary['error']} error, {report.summary['skip']} skip)") + print( + f"Summary: {ok}/{total} passed ({report.summary['fail']} fail, {report.summary['error']} error, {report.summary['skip']} skip)" + ) print(f"Output: {out_path}") if report.summary["fail"] > 0 or report.summary["error"] > 0: diff --git a/src/argus_overview/core/layout_manager.py b/src/argus_overview/core/layout_manager.py index b90b3cb..acbe022 100644 --- a/src/argus_overview/core/layout_manager.py +++ b/src/argus_overview/core/layout_manager.py @@ -118,6 +118,7 @@ def position(self): """ if self._position is None: from argus_overview.core.position import Position + self._position = Position() return self._position diff --git a/src/argus_overview/ui/command/__init__.py b/src/argus_overview/ui/command/__init__.py index 09a9116..77de42f 100644 --- a/src/argus_overview/ui/command/__init__.py +++ b/src/argus_overview/ui/command/__init__.py @@ -10,6 +10,7 @@ * :class:`CommandPalette` — ⌘K action gateway * :class:`CommandCenterWidget` — assembled flagship shell """ + from __future__ import annotations from argus_overview.ui.command.attention import ( diff --git a/src/argus_overview/ui/command/attention.py b/src/argus_overview/ui/command/attention.py index 016ec04..7b8b578 100644 --- a/src/argus_overview/ui/command/attention.py +++ b/src/argus_overview/ui/command/attention.py @@ -15,6 +15,7 @@ The visual is deliberate: small, dense, monospace-aligned times, color dots not icons. Information first. """ + from __future__ import annotations import time @@ -119,21 +120,22 @@ def __init__(self, item: AttentionItem, parent: QWidget | None = None) -> None: text_layout.setContentsMargins(0, 0, 0, 0) text_layout.setSpacing(0) title = QLabel(item.title) - title.setStyleSheet( - f"color: {ds.TEXT_PRIMARY}; font-weight: 700; font-size: 10pt;" - ) + title.setStyleSheet(f"color: {ds.TEXT_PRIMARY}; font-weight: 700; font-size: 10pt;") title.setWordWrap(False) text_layout.addWidget(title) - sub_text = " · ".join(filter(None, [ - f"[{item.category.upper()}]", - item.pilot, - item.system, - ])) + (" " + item.detail if item.detail else "") + sub_text = " · ".join( + filter( + None, + [ + f"[{item.category.upper()}]", + item.pilot, + item.system, + ], + ) + ) + (" " + item.detail if item.detail else "") if sub_text.strip(): sub = QLabel(sub_text.strip()) - sub.setStyleSheet( - f"color: {ds.TEXT_MUTED}; font-size: 9pt;" - ) + sub.setStyleSheet(f"color: {ds.TEXT_MUTED}; font-size: 9pt;") sub.setWordWrap(True) text_layout.addWidget(sub) layout.addWidget(text_block, 1) @@ -364,9 +366,7 @@ def _render_empty(self) -> None: w.deleteLater() empty = QLabel("Awaiting first operator input…", self) empty.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty.setStyleSheet( - f"color: {ds.TEXT_MUTED}; font-style: italic; padding: {sp.SPACE_5}px;" - ) + empty.setStyleSheet(f"color: {ds.TEXT_MUTED}; font-style: italic; padding: {sp.SPACE_5}px;") self._body_layout.insertWidget(0, empty) def add_entry(self, entry: OpsEntry) -> None: @@ -412,22 +412,19 @@ def __init__(self, entry: OpsEntry, parent: QWidget | None = None) -> None: self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) from PySide6.QtWidgets import QHBoxLayout + layout = QHBoxLayout(self) layout.setContentsMargins(sp.SPACE_2, 0, sp.SPACE_2, 0) layout.setSpacing(sp.SPACE_2) self._time = QLabel(self._format_time(entry.timestamp)) self._time.setObjectName("OpsRowTime") - self._time.setStyleSheet( - f"color: {ds.TEXT_MUTED}; font-family: monospace; font-size: 9pt;" - ) + self._time.setStyleSheet(f"color: {ds.TEXT_MUTED}; font-family: monospace; font-size: 9pt;") self._time.setMinimumWidth(56) layout.addWidget(self._time) self._dot = QLabel("●", self) - self._dot.setStyleSheet( - f"color: {entry.category_color()}; font-size: 8pt;" - ) + self._dot.setStyleSheet(f"color: {entry.category_color()}; font-size: 8pt;") self._dot.setFixedWidth(10) layout.addWidget(self._dot) @@ -437,9 +434,7 @@ def __init__(self, entry: OpsEntry, parent: QWidget | None = None) -> None: if entry.detail: text += f" — {entry.detail}" self._text = QLabel(text, self) - self._text.setStyleSheet( - f"color: {ds.TEXT_SECONDARY}; font-size: 9pt;" - ) + self._text.setStyleSheet(f"color: {ds.TEXT_SECONDARY}; font-size: 9pt;") layout.addWidget(self._text, 1) def _format_time(self, ts: float) -> str: diff --git a/src/argus_overview/ui/command/fleet_rail.py b/src/argus_overview/ui/command/fleet_rail.py index 50c232a..ec0028a 100644 --- a/src/argus_overview/ui/command/fleet_rail.py +++ b/src/argus_overview/ui/command/fleet_rail.py @@ -17,6 +17,7 @@ The Fleet Rail replaces the floating StatusDock as the primary identity read — it is always visible, always ordered, and never collapses. """ + from __future__ import annotations import time @@ -95,6 +96,7 @@ def __init__( # Top row — avatar + name + focus dot top = QWidget(self) from PySide6.QtWidgets import QHBoxLayout + top_layout = QHBoxLayout(top) top_layout.setContentsMargins(0, 0, 0, 0) top_layout.setSpacing(sp.SPACE_2) @@ -206,7 +208,11 @@ def _apply_styles(self) -> None: darker = accent.darker(160) a_name = accent.name() d_name = darker.name() - text_color = "#0f0f0f" if (accent.redF() * 0.299 + accent.greenF() * 0.587 + accent.blueF() * 0.114) > 0.55 else "#f5f5f5" + text_color = ( + "#0f0f0f" + if (accent.redF() * 0.299 + accent.greenF() * 0.587 + accent.blueF() * 0.114) > 0.55 + else "#f5f5f5" + ) focus_border = ds.BORDER_FOCUS if self._has_focus else ds.BORDER_SUBTLE focus_bg = ds.SURFACE if not self._has_focus else ds.SURFACE_RAISED @@ -255,6 +261,7 @@ def _apply_styles(self) -> None: ) # Focus dot: visible only when character has window focus from PySide6.QtGui import QPixmap + pix = QPixmap(self._focus_dot.size()) pix.fill(Qt.GlobalColor.transparent) p = QPainter(pix) @@ -296,8 +303,11 @@ def _update_tooltip(self) -> None: def _update_accessible(self) -> None: parts = [f"Pilot {self._character_name}"] - sys = self._system if self._system and not self._stale else ( - f"unknown last {self._last_system}" if self._last_system else "unknown") + sys = ( + self._system + if self._system and not self._stale + else (f"unknown last {self._last_system}" if self._last_system else "unknown") + ) parts.append(f"system {sys}") parts.append(f"capture {self._capture_health}") if self._threat_level and self._threat_alpha > 0.0: @@ -339,6 +349,7 @@ def paintEvent(self, event) -> None: # reading identity + state from a glance. if self._threat_level and self._threat_alpha > 0.0: from argus_overview.ui.main_tab import THREAT_BORDER_COLORS + rgb = THREAT_BORDER_COLORS.get(self._threat_level, (255, 0, 0)) p = QPainter(self) try: @@ -416,7 +427,9 @@ def card_count(self) -> int: def card_for(self, window_id: str) -> FleetCard | None: return self._cards.get(window_id) - def upsert_card(self, window_id: str, character_name: str, accent: tuple[int, int, int]) -> FleetCard: + def upsert_card( + self, window_id: str, character_name: str, accent: tuple[int, int, int] + ) -> FleetCard: if window_id in self._cards: return self._cards[window_id] card = FleetCard(window_id, character_name, accent, parent=self._cards_holder) diff --git a/src/argus_overview/ui/command/header.py b/src/argus_overview/ui/command/header.py index 061e34f..d0bdbe2 100644 --- a/src/argus_overview/ui/command/header.py +++ b/src/argus_overview/ui/command/header.py @@ -12,6 +12,7 @@ * Right-side cluster: command palette hint and global action buttons. * Bottom 1px focus line in BORDER_FOCUS to anchor the eye. """ + from __future__ import annotations from PySide6.QtCore import Property, QEasingCurve, QPropertyAnimation, Qt, QTimer, Signal @@ -39,6 +40,7 @@ def __init__(self, parent: QWidget | None = None) -> None: def sizeHint(self): # noqa: D401 from PySide6.QtCore import QSize + return QSize(180, 48) def paintEvent(self, event) -> None: # noqa: ARG002 @@ -146,8 +148,11 @@ def paintEvent(self, event) -> None: # noqa: ARG002 try: p.setRenderHint(QPainter.RenderHint.TextAntialiasing) r = self.rect() - y = (r.height() + QFontMetrics(self.font()).ascent() - - QFontMetrics(self.font()).descent()) // 2 + y = ( + r.height() + + QFontMetrics(self.font()).ascent() + - QFontMetrics(self.font()).descent() + ) // 2 segments = [] # Fleet count — word-bound pluralization @@ -156,7 +161,9 @@ def paintEvent(self, event) -> None: # noqa: ARG002 segments.append((ds.TEXT_MUTED, "·")) # Intel health intel_color = { - "live": ds.HEALTHY, "idle": ds.UNKNOWN, "degraded": ds.WARNING, + "live": ds.HEALTHY, + "idle": ds.UNKNOWN, + "degraded": ds.WARNING, "offline": ds.CRITICAL, }.get(self._intel_health, ds.UNKNOWN) intel_label = self._intel_health.upper() @@ -311,6 +318,7 @@ def _apply_button_style(self, btn: QPushButton, *, secondary: bool = False) -> N def _paint_focus_line(self) -> None: """1px focus line at the very bottom to anchor the header.""" original = self.paintEvent + def paintEvent(event): # noqa: ARG001 original(event) p = QPainter(self) @@ -318,6 +326,7 @@ def paintEvent(event): # noqa: ARG001 p.fillRect(0, self.height() - 1, self.width(), 1, QColor(ds.BORDER_FOCUS)) finally: p.end() + self.paintEvent = paintEvent def update_state( diff --git a/src/argus_overview/ui/command/integration.py b/src/argus_overview/ui/command/integration.py index 68e4d70..7eea9c9 100644 --- a/src/argus_overview/ui/command/integration.py +++ b/src/argus_overview/ui/command/integration.py @@ -13,6 +13,7 @@ Where ``window`` is any object that exposes the same signals and properties as MainWindowV21. """ + from __future__ import annotations import logging @@ -87,9 +88,7 @@ def _open_layout_chooser(self) -> None: getattr(self._window, attr)() self._record_ops("layout", "Layout chooser opened") return - self._logger.info( - "Layout chooser requested but MainWindow has no show_layout_chooser" - ) + self._logger.info("Layout chooser requested but MainWindow has no show_layout_chooser") @Slot() def _open_palette(self) -> None: @@ -121,6 +120,7 @@ def _handler(): elif hasattr(self._window, "focus_window"): self._window.focus_window(wid) self._open_palette() # ensure palette close; entry already close on accept + return PaletteEntry( id=f"focus::{wid}", title=f"Focus {name}", @@ -209,12 +209,8 @@ def _wire_fleet_rail(self) -> None: return self._command.pilot_focus_requested.connect(self._on_pilot_focus) self._command.pilot_context_requested.connect(self._on_pilot_context) - self._command.grid_holder().pilot_focus_requested.connect( - self._on_pilot_focus - ) - self._command.grid_holder().pilot_context_requested.connect( - self._on_pilot_context - ) + self._command.grid_holder().pilot_focus_requested.connect(self._on_pilot_focus) + self._command.grid_holder().pilot_context_requested.connect(self._on_pilot_context) # Mirror existing main tab characters into the rail on demand self._mirror_main_tab_characters() @@ -230,6 +226,7 @@ def _mirror_main_tab_characters(self) -> None: # Pre-existing helpers vary across versions. Defensive walk. if wm and hasattr(wm, "known_windows"): from argus_overview.ui.main_tab import character_accent_color + for wid, display_name in wm.known_windows().items(): accent_color = character_accent_color(display_name) accent = ( @@ -312,38 +309,48 @@ def _refresh_command_state(self) -> None: fleet_count = rail.card_count() active_threats = sum( - 1 for wid in rail.card_order() + 1 + for wid in rail.card_order() if getattr(rail.card_for(wid), "_threat_level", None) and getattr(rail.card_for(wid), "_threat_alpha", 0.0) > 0.0 ) self._alert_count = active_threats - header.update_state(fleet_count=fleet_count, alert_count=active_threats, - intel_health="live") + header.update_state( + fleet_count=fleet_count, alert_count=active_threats, intel_health="live" + ) truth.set_alert_count(active_threats) # Refresh age labels on TacticalCards so the operator sees # freshness at a glance during long sessions. grid.tick_all() # ---- ops timeline ---------------------------------------------------- - def _record_ops(self, category: str, label: str, detail: str = "", - pilot: str | None = None) -> None: + def _record_ops( + self, category: str, label: str, detail: str = "", pilot: str | None = None + ) -> None: if not self._command: return - entry = OpsEntry(timestamp=time.time(), label=label, - detail=detail, pilot=pilot, category=category) + entry = OpsEntry( + timestamp=time.time(), label=label, detail=detail, pilot=pilot, category=category + ) self._command.ops_timeline().add_entry(entry) - def record_ops(self, category: str, label: str, detail: str = "", - pilot: str | None = None) -> None: + def record_ops( + self, category: str, label: str, detail: str = "", pilot: str | None = None + ) -> None: """Public method for callers to log operational events.""" self._record_ops(category, label, detail=detail, pilot=pilot) # ---- attention queue -------------------------------------------------- - def surface_attention(self, category: str, title: str, *, - detail: str = "", - pilot: str | None = None, - system: str | None = None, - severity: str = "info") -> None: + def surface_attention( + self, + category: str, + title: str, + *, + detail: str = "", + pilot: str | None = None, + system: str | None = None, + severity: str = "info", + ) -> None: if not self._command: return item = AttentionItem( diff --git a/src/argus_overview/ui/command/operational_truth.py b/src/argus_overview/ui/command/operational_truth.py index 5d8a94b..17d9a66 100644 --- a/src/argus_overview/ui/command/operational_truth.py +++ b/src/argus_overview/ui/command/operational_truth.py @@ -7,6 +7,7 @@ * Layout applied + last applied timestamp * Theme + version pinned right """ + from __future__ import annotations from PySide6.QtCore import Property, QPropertyAnimation, Qt, Signal @@ -141,8 +142,7 @@ def __init__(self, parent: QWidget | None = None) -> None: # Version cell self._version_cell = QLabel("ARGUS // v3.3 OPS", self) self._version_cell.setStyleSheet( - f"color: {ds.TEXT_MUTED}; font-size: 8pt;" - f" letter-spacing: 180%; font-weight: 600;" + f"color: {ds.TEXT_MUTED}; font-size: 8pt; letter-spacing: 180%; font-weight: 600;" ) layout.addWidget(self._version_cell) @@ -153,10 +153,12 @@ def __init__(self, parent: QWidget | None = None) -> None: self._pulse_anim.setStartValue(1.0) self._pulse_anim.setEndValue(0.35) from PySide6.QtCore import QEasingCurve + self._pulse_anim.setEasingCurve(QEasingCurve.Type.InOutQuad) # Top hairline original = self.paintEvent + def _paint(ev): # noqa: ARG001 original(ev) pp = QPainter(self) @@ -164,6 +166,7 @@ def _paint(ev): # noqa: ARG001 pp.fillRect(0, 0, self.width(), 1, QColor(ds.BORDER_SUBTLE)) finally: pp.end() + self.paintEvent = _paint # ---- properties -------------------------------------------------------- @@ -202,6 +205,7 @@ def set_layout_state(self, label: str | None, *, applied_at: float | None = None self._layout_cell.setText("·") return import datetime + suffix = "" if applied_at: suffix = f" @ {datetime.datetime.fromtimestamp(applied_at).strftime('%H:%M:%S')}" diff --git a/src/argus_overview/ui/command/palette.py b/src/argus_overview/ui/command/palette.py index 26e6f09..67893cf 100644 --- a/src/argus_overview/ui/command/palette.py +++ b/src/argus_overview/ui/command/palette.py @@ -10,6 +10,7 @@ The palette is the operator's muscle-memory gateway — every action is reachable in two keystrokes from anywhere in Argus. """ + from __future__ import annotations from dataclasses import dataclass, field @@ -47,6 +48,7 @@ @dataclass class PaletteEntry: """One entry in the palette — display + filter + handler.""" + id: str title: str subtitle: str = "" @@ -161,7 +163,9 @@ def _build_ui(self) -> None: ("ACTION", ds.CRITICAL), ): chip = QLabel(f"● {label}") - chip.setStyleSheet(f"color: {color}; font-size: 8pt; font-weight: 700; letter-spacing: 140%;") + chip.setStyleSheet( + f"color: {color}; font-size: 8pt; font-weight: 700; letter-spacing: 140%;" + ) l_layout.addWidget(chip) l_layout.addStretch(1) body_layout.addWidget(legend) @@ -184,8 +188,10 @@ def _build_ui(self) -> None: ("↑↓ NAVIGATE", "ARROWS"), ("ESC CLOSE", "ESC"), ): - cell = QLabel(f"{label}" - f" {key}") + cell = QLabel( + f"{label}" + f" {key}" + ) cell.setTextFormat(Qt.TextFormat.RichText) f_layout.addWidget(cell) f_layout.addStretch(1) @@ -286,8 +292,9 @@ def _refresh_list(self, query: str) -> None: def _score(self, entry: PaletteEntry, q: str) -> int: if not q: return 1 # show everything in declared order - haystack = " ".join([entry.title.lower(), entry.subtitle.lower(), - entry.category.lower(), *entry.keywords]).strip() + haystack = " ".join( + [entry.title.lower(), entry.subtitle.lower(), entry.category.lower(), *entry.keywords] + ).strip() if entry.title.lower().startswith(q): return 100 if q in haystack: diff --git a/src/argus_overview/ui/command/shell.py b/src/argus_overview/ui/command/shell.py index e6d936d..6fff303 100644 --- a/src/argus_overview/ui/command/shell.py +++ b/src/argus_overview/ui/command/shell.py @@ -22,6 +22,7 @@ to slot into an existing QTabWidget as a top-level tab without disrupting the other tabs. """ + from __future__ import annotations from PySide6.QtCore import Signal @@ -77,6 +78,7 @@ def __init__(self, parent: QWidget | None = None) -> None: # ROW 2 COL 2 — Attention Queue + Ops Timeline from PySide6.QtWidgets import QVBoxLayout + right_panel = QWidget(self) right_panel.setObjectName("CommandRightPanel") right_layout = QVBoxLayout(right_panel) diff --git a/src/argus_overview/ui/command/tactical_grid.py b/src/argus_overview/ui/command/tactical_grid.py index 5e25add..67aa4bf 100644 --- a/src/argus_overview/ui/command/tactical_grid.py +++ b/src/argus_overview/ui/command/tactical_grid.py @@ -17,6 +17,7 @@ complementary question: **"which windows are connected, where are they, and which are flagged?"** """ + from __future__ import annotations import time @@ -169,9 +170,7 @@ def __init__( meta_layout.addWidget(self._health_label) self._age_label = QLabel("now") self._age_label.setObjectName("TacticalAge") - self._age_label.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self._age_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) meta_layout.addWidget(self._age_label) body_layout.addWidget(meta_block) layout.addWidget(body, 1) @@ -279,8 +278,7 @@ def _apply_styles(self) -> None: d_name = darker.name() text_color = ( "#0f0f0f" - if (accent.redF() * 0.299 + accent.greenF() * 0.587 + accent.blueF() * 0.114) - > 0.55 + if (accent.redF() * 0.299 + accent.greenF() * 0.587 + accent.blueF() * 0.114) > 0.55 else "#f5f5f5" ) @@ -363,9 +361,7 @@ def mousePressEvent(self, event) -> None: event.accept() return if event.button() == Qt.MouseButton.RightButton: - self.context_requested.emit( - self._window_id, event.globalPosition().toPoint() - ) + self.context_requested.emit(self._window_id, event.globalPosition().toPoint()) event.accept() return super().mousePressEvent(event) @@ -481,8 +477,7 @@ def clear(self) -> None: for window_id in list(self._cards.keys()): self.remove_card(window_id) - def set_pilot_system(self, window_id: str, system: str | None, *, - stale: bool = False) -> bool: + def set_pilot_system(self, window_id: str, system: str | None, *, stale: bool = False) -> bool: card = self._cards.get(window_id) if card is None: return False @@ -500,9 +495,15 @@ def set_pilot_focused(self, window_id: str, focused: bool) -> None: for wid, card in self._cards.items(): card.set_focused(wid == window_id and focused) - def set_pilot_threat(self, window_id: str, level: ThreatLevel | None, *, - system: str | None = None, alpha: float = 1.0, - distance: int | None = None) -> bool: + def set_pilot_threat( + self, + window_id: str, + level: ThreatLevel | None, + *, + system: str | None = None, + alpha: float = 1.0, + distance: int | None = None, + ) -> bool: card = self._cards.get(window_id) if card is None: return False diff --git a/src/argus_overview/ui/design_system/colors.py b/src/argus_overview/ui/design_system/colors.py index 14760e6..08bacb9 100644 --- a/src/argus_overview/ui/design_system/colors.py +++ b/src/argus_overview/ui/design_system/colors.py @@ -13,17 +13,17 @@ # --------------------------------------------------------------------------- # Canvas / background # --------------------------------------------------------------------------- -CANVAS = "#0B0E13" # Deepest background (app window) -SURFACE = "#11161D" # Primary card/panel background +CANVAS = "#0B0E13" # Deepest background (app window) +SURFACE = "#11161D" # Primary card/panel background SURFACE_RAISED = "#171D26" # Elevated surface (hover, active) -SURFACE_HOVER = "#1D2530" # Hover state background +SURFACE_HOVER = "#1D2530" # Hover state background # --------------------------------------------------------------------------- # Borders # --------------------------------------------------------------------------- BORDER_SUBTLE = "#27313D" BORDER_STRONG = "#3A4655" -BORDER_FOCUS = "#63C7FF" # Focus ring (also used for keyboard focus) +BORDER_FOCUS = "#63C7FF" # Focus ring (also used for keyboard focus) # --------------------------------------------------------------------------- # Text @@ -78,6 +78,7 @@ # Utility helpers # --------------------------------------------------------------------------- + def _luminance(rgb: tuple[int, int, int]) -> float: """Relative luminance of an sRGB color (simplified).""" r, g, b = rgb diff --git a/src/argus_overview/ui/design_system/metrics.py b/src/argus_overview/ui/design_system/metrics.py index c53668c..8e00c59 100644 --- a/src/argus_overview/ui/design_system/metrics.py +++ b/src/argus_overview/ui/design_system/metrics.py @@ -7,14 +7,14 @@ from __future__ import annotations # Border radii -RADIUS_CONTROL = 4 # Buttons, inputs, badges -RADIUS_CARD = 6 # Preview cards, chips -RADIUS_PANEL = 8 # Panels, dialogs +RADIUS_CONTROL = 4 # Buttons, inputs, badges +RADIUS_CARD = 6 # Preview cards, chips +RADIUS_PANEL = 8 # Panels, dialogs # Control heights -CONTROL_HEIGHT_SMALL = 28 # Compact toolbar buttons -CONTROL_HEIGHT = 34 # Standard buttons -CONTROL_HEIGHT_LARGE = 40 # Primary CTAs +CONTROL_HEIGHT_SMALL = 28 # Compact toolbar buttons +CONTROL_HEIGHT = 34 # Standard buttons +CONTROL_HEIGHT_LARGE = 40 # Primary CTAs # Preview card constraints (operational minimums) PREVIEW_MIN_WIDTH = 180 diff --git a/src/argus_overview/ui/design_system/painting.py b/src/argus_overview/ui/design_system/painting.py index 851a14d..d60042f 100644 --- a/src/argus_overview/ui/design_system/painting.py +++ b/src/argus_overview/ui/design_system/painting.py @@ -207,11 +207,17 @@ def draw_threat_accent( ) elif edge == "left": painter.drawRect( - rect.x(), rect.y() + 6, ribbon_width, rect.height() - 12, + rect.x(), + rect.y() + 6, + ribbon_width, + rect.height() - 12, ) elif edge == "top": painter.drawRect( - rect.x() + 6, rect.y(), rect.width() - 12, ribbon_width, + rect.x() + 6, + rect.y(), + rect.width() - 12, + ribbon_width, ) elif edge == "bottom": painter.drawRect( diff --git a/src/argus_overview/ui/hotkeys_tab.py b/src/argus_overview/ui/hotkeys_tab.py index 765d492..6557fb1 100644 --- a/src/argus_overview/ui/hotkeys_tab.py +++ b/src/argus_overview/ui/hotkeys_tab.py @@ -597,7 +597,9 @@ def _edit_character_hotkey(self) -> None: """PR2: open a dialog to record a hotkey for the selected character.""" current = self.character_hotkey_list.currentItem() if not current: - QMessageBox.information(self, "Select Character", "Please select a character from the list.") + QMessageBox.information( + self, "Select Character", "Please select a character from the list." + ) return char_name = current.data(Qt.ItemDataRole.UserRole) diff --git a/src/argus_overview/ui/layouts_tab.py b/src/argus_overview/ui/layouts_tab.py index e6c0ada..bc67617 100644 --- a/src/argus_overview/ui/layouts_tab.py +++ b/src/argus_overview/ui/layouts_tab.py @@ -326,7 +326,9 @@ def apply_arrangement( self.last_apply_results = results ok = all(results.values()) if results else True - self.logger.info(f"Applied arrangement to {len(window_map)} windows ({sum(results.values())}/{len(results)} succeeded)") + self.logger.info( + f"Applied arrangement to {len(window_map)} windows ({sum(results.values())}/{len(results)} succeeded)" + ) return ok except (OSError, subprocess.SubprocessError, KeyError, ValueError) as e: @@ -814,7 +816,9 @@ def _on_group_selected(self): active_count += 1 if active_count == 0: - self.info_label.setText("No active windows — import EVE clients in the Overview tab first") + self.info_label.setText( + "No active windows — import EVE clients in the Overview tab first" + ) else: self.info_label.setText(f"Showing all active windows ({active_count})") else: diff --git a/src/argus_overview/ui/main_tab.py b/src/argus_overview/ui/main_tab.py index f3cea7c..a7814c3 100644 --- a/src/argus_overview/ui/main_tab.py +++ b/src/argus_overview/ui/main_tab.py @@ -549,7 +549,9 @@ def apply_arrangement( for _char_name, window_id in window_map.items(): if stacked_use_grid_size: - results[window_id] = self._move_window(window_id, x, y, cell_width, cell_height) + results[window_id] = self._move_window( + window_id, x, y, cell_width, cell_height + ) else: results[window_id] = self._move_window_position_only(window_id, x, y) else: @@ -564,7 +566,9 @@ def apply_arrangement( self.last_apply_results = results ok = all(results.values()) if results else True - self.logger.info(f"Applied arrangement to {len(window_map)} windows ({sum(results.values())}/{len(results)} succeeded)") + self.logger.info( + f"Applied arrangement to {len(window_map)} windows ({sum(results.values())}/{len(results)} succeeded)" + ) return ok except (AttributeError, OSError, RuntimeError, ValueError) as e: @@ -1432,7 +1436,11 @@ def _paint_border_layer(self, painter: QPainter, health: str) -> None: from argus_overview.ui.design_system import metrics as dm # Determine if health should suppress the threat border - suppress_threat = health.startswith("STALE") or health in ("ERROR", "DISCONNECTED", "PAUSED") + suppress_threat = health.startswith("STALE") or health in ( + "ERROR", + "DISCONNECTED", + "PAUSED", + ) # Threat border (only when healthy enough to trust the data) if ( @@ -1472,8 +1480,12 @@ def _paint_border_layer(self, painter: QPainter, health: str) -> None: painter.setPen(pen) painter.setBrush(QBrush(Qt.BrushStyle.NoBrush)) painter.drawRoundedRect( - 2, 2, self.width() - 4, self.height() - 4, - dm.RADIUS_CARD, dm.RADIUS_CARD, + 2, + 2, + self.width() - 4, + self.height() - 4, + dm.RADIUS_CARD, + dm.RADIUS_CARD, ) painter.setPen(QPen(QColor(ds.TEXT_MUTED))) painter.drawText(6, 14, "?") @@ -1483,8 +1495,12 @@ def _paint_border_layer(self, painter: QPainter, health: str) -> None: painter.setPen(pen) painter.setBrush(QBrush(Qt.BrushStyle.NoBrush)) painter.drawRoundedRect( - 2, 2, self.width() - 4, self.height() - 4, - dm.RADIUS_CARD, dm.RADIUS_CARD, + 2, + 2, + self.width() - 4, + self.height() - 4, + dm.RADIUS_CARD, + dm.RADIUS_CARD, ) def _paint_health_overlay_layer(self, painter: QPainter, health: str) -> None: @@ -2787,7 +2803,16 @@ def _apply_layout_preset(self, preset_name: str) -> None: pattern = preset.grid_pattern or "custom" # Normalise pattern name to the display form used by get_pattern_positions - display_pattern = pattern.replace("_", " ").replace("2x2", "2x2 Grid").replace("3x1", "3x1 Row").replace("1x3", "1x3 Column").replace("4x1", "4x1 Row").replace("main+sides", "Main + Sides").replace("cascade", "Cascade").replace("custom", "Custom") + display_pattern = ( + pattern.replace("_", " ") + .replace("2x2", "2x2 Grid") + .replace("3x1", "3x1 Row") + .replace("1x3", "1x3 Column") + .replace("4x1", "4x1 Row") + .replace("main+sides", "Main + Sides") + .replace("cascade", "Cascade") + .replace("custom", "Custom") + ) # Fallback: if the normalised name isn't in our map, try title-casing if display_pattern not in get_all_layout_patterns() and display_pattern != "Custom": display_pattern = "Custom" diff --git a/src/argus_overview/ui/main_window_v21.py b/src/argus_overview/ui/main_window_v21.py index 30b2ef1..a7c231c 100644 --- a/src/argus_overview/ui/main_window_v21.py +++ b/src/argus_overview/ui/main_window_v21.py @@ -153,6 +153,7 @@ def __init__(self): self.tabs = QTabWidget() from argus_overview.ui.design_system import colors as ds from argus_overview.ui.design_system import metrics as dm + self.tabs.setStyleSheet(f""" QTabWidget::pane {{ border: none; @@ -661,9 +662,7 @@ def _create_menu_bar(self): app_handlers = { "toggle_replay_strips_app": self._toggle_replay_strips_global, } - app_menu = menu_builder.build_menu( - PrimaryHome.APP_MENU, parent=self, handlers=app_handlers - ) + app_menu = menu_builder.build_menu(PrimaryHome.APP_MENU, parent=self, handlers=app_handlers) menubar.addMenu(app_menu) # Build Help menu using MenuBuilder (actions from ActionRegistry) diff --git a/src/argus_overview/ui/menu_builder.py b/src/argus_overview/ui/menu_builder.py index 70bf4c0..0598d86 100644 --- a/src/argus_overview/ui/menu_builder.py +++ b/src/argus_overview/ui/menu_builder.py @@ -410,6 +410,7 @@ def _get_primary_style(self) -> str: def _get_success_style(self) -> str: """Return stylesheet for success actions using design-system healthy green.""" from argus_overview.ui.design_system import colors as ds + return f""" QPushButton {{ background-color: {ds.HEALTHY}; @@ -426,6 +427,7 @@ def _get_danger_style(self) -> str: filling on hover for clear affordance. """ from argus_overview.ui.design_system import colors as ds + return f""" QPushButton {{ background-color: transparent; diff --git a/src/argus_overview/ui/settings_tab.py b/src/argus_overview/ui/settings_tab.py index bfe720c..c711d19 100644 --- a/src/argus_overview/ui/settings_tab.py +++ b/src/argus_overview/ui/settings_tab.py @@ -231,7 +231,9 @@ def _setup_ui(self): "• Reduces CPU/GPU load significantly\n\n" "Use when running multiple EVE clients." ) - self.low_power_check.setStyleSheet(f"QCheckBox {{ font-weight: bold; color: {_ds.WARNING}; }}") + self.low_power_check.setStyleSheet( + f"QCheckBox {{ font-weight: bold; color: {_ds.WARNING}; }}" + ) form.addRow("⚡ Low Power Mode:", self.low_power_check) # Disable previews (GPU/CPU saver) diff --git a/src/argus_overview/ui/tabs/__init__.py b/src/argus_overview/ui/tabs/__init__.py index 1533fb8..f71acb6 100644 --- a/src/argus_overview/ui/tabs/__init__.py +++ b/src/argus_overview/ui/tabs/__init__.py @@ -19,6 +19,7 @@ The containers don't reach into the MainWindowV21 init dance; they take already-built inner widgets and stitch them together. """ + from __future__ import annotations from argus_overview.ui.tabs.command_tab import CommandTab diff --git a/src/argus_overview/ui/tabs/command_tab.py b/src/argus_overview/ui/tabs/command_tab.py index 46763fa..51a3912 100644 --- a/src/argus_overview/ui/tabs/command_tab.py +++ b/src/argus_overview/ui/tabs/command_tab.py @@ -9,6 +9,7 @@ :mod:`argus_overview.ui.command.integration` module can wire its signals and slots without knowing about the IA layer. """ + from __future__ import annotations from PySide6.QtCore import Signal @@ -41,9 +42,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self.command_center = CommandCenterWidget(self) self.command_center.palette_requested.connect(self.palette_requested.emit) - self.command_center.layout_chooser_requested.connect( - self.layout_chooser_requested.emit - ) + self.command_center.layout_chooser_requested.connect(self.layout_chooser_requested.emit) self.command_center.pilot_focus_requested.connect(self.pilot_focus_requested.emit) self.command_center.pilot_context_requested.connect(self.pilot_context_requested.emit) diff --git a/src/argus_overview/ui/tabs/fleet_tab.py b/src/argus_overview/ui/tabs/fleet_tab.py index 0fd6b22..c1d3e61 100644 --- a/src/argus_overview/ui/tabs/fleet_tab.py +++ b/src/argus_overview/ui/tabs/fleet_tab.py @@ -8,6 +8,7 @@ dependencies and signal surface. The split state is persisted only in memory; future work could write it to settings. """ + from __future__ import annotations from PySide6.QtCore import Qt diff --git a/src/argus_overview/ui/tabs/layouts_tab.py b/src/argus_overview/ui/tabs/layouts_tab.py index 6400c78..6ec3abd 100644 --- a/src/argus_overview/ui/tabs/layouts_tab.py +++ b/src/argus_overview/ui/tabs/layouts_tab.py @@ -6,6 +6,7 @@ (:class:`HotkeysTab`) on the right. Default split is 70/30 so the visual arrangement grid gets the room it needs. """ + from __future__ import annotations from PySide6.QtCore import Qt diff --git a/src/argus_overview/ui/tabs/system_tab.py b/src/argus_overview/ui/tabs/system_tab.py index 3987eab..c433513 100644 --- a/src/argus_overview/ui/tabs/system_tab.py +++ b/src/argus_overview/ui/tabs/system_tab.py @@ -4,6 +4,7 @@ SettingsTab (the application config panel) on the left; SettingsSyncTab (the EVE folder sync panel) on the right. Default split is 60/40. """ + from __future__ import annotations from PySide6.QtCore import Qt diff --git a/tests/test_command_center.py b/tests/test_command_center.py index 13e8e55..95e8772 100644 --- a/tests/test_command_center.py +++ b/tests/test_command_center.py @@ -9,6 +9,7 @@ * Command palette (entry filtering, ranking) * Command Center shell assembly """ + from __future__ import annotations import time @@ -91,8 +92,7 @@ def test_threat_state_changes_label(self, app): def test_distance_badge_appears(self, app): card = FleetCard("w1", "Test", accent=(100, 200, 100)) - card.set_threat_state(ThreatLevel.DANGER, system="Jita", - alpha=0.6, distance=3) + card.set_threat_state(ThreatLevel.DANGER, system="Jita", alpha=0.6, distance=3) app.processEvents() assert "+3j" in card._threat_badge.text() @@ -143,8 +143,7 @@ def test_remove_card(self, app): def test_clear(self, app): rail = FleetRail() for i in range(3): - rail.upsert_card(f"w{i}", f"Pilot {i}", - accent=(100, 200, 100)) + rail.upsert_card(f"w{i}", f"Pilot {i}", accent=(100, 200, 100)) assert rail.card_count() == 3 rail.clear() assert rail.card_count() == 0 @@ -166,8 +165,11 @@ def test_empty_state_renders(self, app): def test_add_item_inserts_row(self, app): q = AttentionQueue() item = AttentionItem( - id="t1", category="threat", title="Hostile in Jita", - detail="5 Sabres", severity="critical" + id="t1", + category="threat", + title="Hostile in Jita", + detail="5 Sabres", + severity="critical", ) q.add_item(item) app.processEvents() @@ -175,9 +177,7 @@ def test_add_item_inserts_row(self, app): def test_acknowledge_removes_active(self, app): q = AttentionQueue() - item = AttentionItem( - id="t1", category="threat", title="x", severity="warning" - ) + item = AttentionItem(id="t1", category="threat", title="x", severity="warning") q.add_item(item) q._on_ack("t1") app.processEvents() @@ -194,9 +194,7 @@ def test_entries_evicted_at_max(self, app): t = OpsTimeline() t.ENTRIES_MAX = 3 for i in range(5): - t.add_entry(OpsEntry(timestamp=time.time(), - label=f"Event {i}", - category="layout")) + t.add_entry(OpsEntry(timestamp=time.time(), label=f"Event {i}", category="layout")) app.processEvents() assert len(t._entries) <= t.ENTRIES_MAX @@ -263,8 +261,7 @@ def test_empty_filter_shows_all(self, app): def test_no_matches_shows_placeholder(self, app): pal = self._make_palette() - pal.set_entries([PaletteEntry(id="a", title="X", - category="system")]) + pal.set_entries([PaletteEntry(id="a", title="X", category="system")]) pal._refresh_list("zzzzznotfound") first = pal._list.item(0) assert "No matches" in first.text() @@ -382,8 +379,6 @@ def test_shell_grid_is_tactical_grid(self, app): def test_shell_upsert_into_grid(self, app): cc = CommandCenterWidget() - card = cc.grid_holder().upsert_card( - "w1", "Pilot A", accent=(180, 100, 220) - ) + card = cc.grid_holder().upsert_card("w1", "Pilot A", accent=(180, 100, 220)) assert cc.grid_holder().card_count() == 1 assert card.character_name() == "Pilot A" diff --git a/tests/test_command_integrator.py b/tests/test_command_integrator.py index 86bb50a..19024b6 100644 --- a/tests/test_command_integrator.py +++ b/tests/test_command_integrator.py @@ -11,6 +11,7 @@ that does not exist on the real MainWindow, the corresponding test will fail — by design. That is the whole point of this file. """ + from __future__ import annotations import pytest @@ -276,6 +277,7 @@ def test_theme_entry_passes_qapplication(self) -> None: assert name == "dark" # Second argument must be the QApplication instance, not None. from PySide6.QtWidgets import QApplication + assert isinstance(app, QApplication) diff --git a/tests/test_layouts_tab.py b/tests/test_layouts_tab.py index 1c6ab52..3c85387 100644 --- a/tests/test_layouts_tab.py +++ b/tests/test_layouts_tab.py @@ -1679,9 +1679,7 @@ def test_create_top_section_creates_group_selector(self): "argus_overview.ui.layouts_tab.QSpinBox" ) as mock_spin_cls, patch( "argus_overview.ui.layouts_tab.QCheckBox" - ) as mock_checkbox_cls, patch( - "argus_overview.ui.layouts_tab.QWidget" - ), patch( + ) as mock_checkbox_cls, patch("argus_overview.ui.layouts_tab.QWidget"), patch( "argus_overview.ui.layouts_tab.get_all_patterns", return_value=["2x2", "3x1"] ): mock_section = MagicMock() @@ -1738,8 +1736,9 @@ def test_create_top_section_creates_grid_size_controls(self): "argus_overview.ui.layouts_tab.QPushButton" ), patch("argus_overview.ui.layouts_tab.QSpinBox") as mock_spin_cls, patch( "argus_overview.ui.layouts_tab.QCheckBox" - ), patch("argus_overview.ui.layouts_tab.QWidget" - ), patch("argus_overview.ui.layouts_tab.get_all_patterns", return_value=[]): + ), patch("argus_overview.ui.layouts_tab.QWidget"), patch( + "argus_overview.ui.layouts_tab.get_all_patterns", return_value=[] + ): mock_spin = MagicMock() mock_spin_cls.return_value = mock_spin diff --git a/tests/test_main_tab.py b/tests/test_main_tab.py index 32a146a..30e3599 100644 --- a/tests/test_main_tab.py +++ b/tests/test_main_tab.py @@ -10307,6 +10307,7 @@ def test_cross_process_determinism(self): env_base["QT_QPA_PLATFORM"] = "offscreen" # Ensure the subprocess can find argus_overview when running via pytest import pathlib + repo_root = str(pathlib.Path(__file__).resolve().parents[1] / "src") env_base["PYTHONPATH"] = repo_root + os.pathsep + env_base.get("PYTHONPATH", "") @@ -11043,6 +11044,7 @@ def test_paint_focus_layer_when_focused(self, qapp): widget.rect = MagicMock(return_value=QRect(0, 0, 200, 150)) from PySide6.QtGui import QPainter + mock_painter = MagicMock(spec=QPainter) widget._paint_focus_layer(mock_painter) @@ -11063,6 +11065,7 @@ def test_paint_focus_layer_skips_when_not_focused(self, qapp): widget.hasFocus = MagicMock(return_value=False) from PySide6.QtGui import QPainter + mock_painter = MagicMock(spec=QPainter) widget._paint_focus_layer(mock_painter) diff --git a/tests/test_main_window_v21.py b/tests/test_main_window_v21.py index 13412d5..521b937 100644 --- a/tests/test_main_window_v21.py +++ b/tests/test_main_window_v21.py @@ -426,9 +426,7 @@ def test_show_settings_switches_to_tab(self): window.raise_.assert_called_once() # Phase 4 IA: Settings lives inside the SYSTEM container — the # _show_settings entry point lands on SYSTEM (last of four tabs). - window.tabs.setCurrentIndex.assert_called_with( - MainWindowV21._TAB_LABELS.index("System") - ) + window.tabs.setCurrentIndex.assert_called_with(MainWindowV21._TAB_LABELS.index("System")) class TestPhase4InformationArchitecture: diff --git a/tests/test_menu_builder.py b/tests/test_menu_builder.py index 6a59611..b6d7e56 100644 --- a/tests/test_menu_builder.py +++ b/tests/test_menu_builder.py @@ -65,6 +65,7 @@ def test_primary_style_uses_theme_accent(self): def test_success_style_uses_healthy_green(self): """SUCCESS_STYLE should use design-system healthy green""" from argus_overview.ui.design_system import colors as ds + builder = ToolbarBuilder() style = builder._get_success_style().lower() assert ds.HEALTHY.lower() in style @@ -72,6 +73,7 @@ def test_success_style_uses_healthy_green(self): def test_danger_style_uses_critical_red(self): """DANGER_STYLE should use design-system critical red""" from argus_overview.ui.design_system import colors as ds + builder = ToolbarBuilder() style = builder._get_danger_style().lower() assert ds.CRITICAL.lower() in style @@ -624,6 +626,7 @@ def test_create_button_primary_style(self, mock_button): def test_create_button_success_style(self, mock_button): """create_button applies SUCCESS_STYLE for success actions""" from argus_overview.ui.design_system import colors as ds + mock_button_instance = MagicMock() mock_button.return_value = mock_button_instance @@ -638,6 +641,7 @@ def test_create_button_success_style(self, mock_button): def test_create_button_danger_style(self, mock_button): """create_button applies DANGER_STYLE for danger actions""" from argus_overview.ui.design_system import colors as ds + mock_button_instance = MagicMock() mock_button.return_value = mock_button_instance diff --git a/tests/test_preview_health_paint.py b/tests/test_preview_health_paint.py index 2b6d024..abe251f 100644 --- a/tests/test_preview_health_paint.py +++ b/tests/test_preview_health_paint.py @@ -31,15 +31,18 @@ def _make_widget(qapp): return widget -@pytest.mark.parametrize("health_label", [ - "INITIALIZING", - "LIVE", - "STATIC", - "STALE · 5s", - "PAUSED", - "ERROR", - "DISCONNECTED", -]) +@pytest.mark.parametrize( + "health_label", + [ + "INITIALIZING", + "LIVE", + "STATIC", + "STALE · 5s", + "PAUSED", + "ERROR", + "DISCONNECTED", + ], +) def test_paint_event_for_health_label(qapp, health_label): """paintEvent must not crash for any capture health label.""" widget = _make_widget(qapp) diff --git a/tests/test_system_status_bar.py b/tests/test_system_status_bar.py index bdd2d18..b0564c3 100644 --- a/tests/test_system_status_bar.py +++ b/tests/test_system_status_bar.py @@ -20,7 +20,13 @@ class TestSystemStatusBarInit: def test_five_indicators_created(self, qapp): bar = SystemStatusBar() assert len(bar._indicators) == 5 - assert set(bar._indicators.keys()) == {"capture", "hotkeys", "discovery", "intel", "location"} + assert set(bar._indicators.keys()) == { + "capture", + "hotkeys", + "discovery", + "intel", + "location", + } def test_all_unknown_on_init(self, qapp): bar = SystemStatusBar() @@ -84,8 +90,9 @@ def test_painter_called_in_paint_event(self, qapp): bar = SystemStatusBar() indicator = bar._indicators["capture"] - with patch.object(QPainter, "drawText") as mock_draw_text, \ - patch.object(QPainter, "drawEllipse") as mock_draw_ellipse: + with patch.object(QPainter, "drawText") as mock_draw_text, patch.object( + QPainter, "drawEllipse" + ) as mock_draw_ellipse: # Fake a paint event event = QPaintEvent(indicator.rect()) indicator.paintEvent(event) diff --git a/tests/test_tab_containers.py b/tests/test_tab_containers.py index 276f49b..28c6526 100644 --- a/tests/test_tab_containers.py +++ b/tests/test_tab_containers.py @@ -5,6 +5,7 @@ structure so the IA contract is enforced: every container exposes both inner widgets and the splitter that joins them. """ + from __future__ import annotations import pytest From 3bbdcbd4a2ad573a74c363b3d5f7d6de32efa830 Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Thu, 6 Aug 2026 02:37:27 -0700 Subject: [PATCH 6/7] chore: apply ruff format to Markdown files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's `ruff format --check .` step (using ruff 0.16.1) catches Markdown formatting drift that older ruff versions ignored. Five pre-existing Markdown files had minor whitespace/blank-line deviations from ruff's Markdown formatter: - DEV_NOTES.md - PACKAGE_INFO.md - docs/API.md - docs/ARCHITECTURE.md - docs/SMOKE_TEST_v3.2.0.md Mechanical reformat — no content changes. Verified locally after upgrading ruff to 0.16.1: - ruff format --check . → all formatted. - ruff check . → All checks passed. - pytest tests/ → 2,580 passed, 5 skipped. Co-Authored-By: Claude --- DEV_NOTES.md | 20 ++++++++------ PACKAGE_INFO.md | 10 ++++--- docs/API.md | 58 +++++++++++++++++++-------------------- docs/ARCHITECTURE.md | 3 +- docs/SMOKE_TEST_v3.2.0.md | 4 +-- 5 files changed, 50 insertions(+), 45 deletions(-) diff --git a/DEV_NOTES.md b/DEV_NOTES.md index 6cdbb8b..a5f03fe 100644 --- a/DEV_NOTES.md +++ b/DEV_NOTES.md @@ -38,15 +38,17 @@ but must not create duplicate clickable UI elements. 3. **Register in ActionRegistry**: Edit `ui/action_registry.py` ```python -self.register(ActionSpec( - id="my_new_action", - label="My Action Label", - scope=ActionScope.TAB, # or GLOBAL, OBJECT - primary_home=PrimaryHome.OVERVIEW_TOOLBAR, # canonical location - tooltip="Description of action", - shortcut="++a", # optional - handler_name="_my_action_handler", -)) +self.register( + ActionSpec( + id="my_new_action", + label="My Action Label", + scope=ActionScope.TAB, # or GLOBAL, OBJECT + primary_home=PrimaryHome.OVERVIEW_TOOLBAR, # canonical location + tooltip="Description of action", + shortcut="++a", # optional + handler_name="_my_action_handler", + ) +) ``` 4. **Connect handler**: In the appropriate widget/tab, bind the handler: diff --git a/PACKAGE_INFO.md b/PACKAGE_INFO.md index 3f8625a..442d243 100644 --- a/PACKAGE_INFO.md +++ b/PACKAGE_INFO.md @@ -157,8 +157,8 @@ from eve_overview_pro.core.layout_manager import LayoutManager, GridPattern manager = LayoutManager() # Calculate grid layout -screen = {'x': 0, 'y': 0, 'width': 1920, 'height': 1080} -windows = ['0x123', '0x124', '0x125', '0x126'] +screen = {"x": 0, "y": 0, "width": 1920, "height": 1080} +windows = ["0x123", "0x124", "0x125", "0x126"] layout = manager.calculate_grid_layout(GridPattern.GRID_2X2, windows, screen) ``` @@ -168,11 +168,13 @@ from eve_overview_pro.core.alert_detector import AlertDetector, AlertLevel detector = AlertDetector() + def on_alert(level): print(f"ALERT: {level}") -detector.register_callback('0x123', on_alert) -alert_level = detector.analyze_frame('0x123', image) + +detector.register_callback("0x123", on_alert) +alert_level = detector.analyze_frame("0x123", image) ``` ### Settings Sync diff --git a/docs/API.md b/docs/API.md index 6a37ec5..f2988a1 100644 --- a/docs/API.md +++ b/docs/API.md @@ -68,9 +68,9 @@ Information about a window. ```python @dataclass class WindowInfo: - window_id: str # Platform-specific window identifier - title: str # Window title - class_name: str # Window class (default: "") + window_id: str # Platform-specific window identifier + title: str # Window title + class_name: str # Window class (default: "") ``` #### `ScreenGeometry` @@ -79,11 +79,11 @@ Screen/monitor geometry information. ```python @dataclass class ScreenGeometry: - x: int # X position - y: int # Y position - width: int # Screen width in pixels - height: int # Screen height in pixels - is_primary: bool # True if primary display (default: False) + x: int # X position + y: int # Y position + width: int # Screen width in pixels + height: int # Screen height in pixels + is_primary: bool # True if primary display (default: False) ``` --- @@ -309,11 +309,11 @@ Threat level classification for intel reports. ```python class ThreatLevel(Enum): - CLEAR = "clear" # System reported clear - INFO = "info" # General intel, not immediate threat - WARNING = "warning" # Hostiles nearby (2+ jumps) - DANGER = "danger" # Hostiles close (1 jump) or small gang - CRITICAL = "critical" # Hostiles in system or capital ships + CLEAR = "clear" # System reported clear + INFO = "info" # General intel, not immediate threat + WARNING = "warning" # Hostiles nearby (2+ jumps) + DANGER = "danger" # Hostiles close (1 jump) or small gang + CRITICAL = "critical" # Hostiles in system or capital ships ``` --- @@ -325,15 +325,15 @@ Represents parsed intel from a chat message. ```python @dataclass class IntelReport: - system: Optional[str] # EVE system name (e.g., "HED-GP") - threat_level: ThreatLevel # Assessed threat level - hostile_count: Optional[int] # Number of hostiles if known - ship_types: List[str] # Detected ship types - player_names: List[str] # Detected player names - raw_message: str # Original message text - timestamp: datetime # Message timestamp - channel: str # Source channel name - reporter: str # Player who sent the message + system: Optional[str] # EVE system name (e.g., "HED-GP") + threat_level: ThreatLevel # Assessed threat level + hostile_count: Optional[int] # Number of hostiles if known + ship_types: List[str] # Detected ship types + player_names: List[str] # Detected player names + raw_message: str # Original message text + timestamp: datetime # Message timestamp + channel: str # Source channel name + reporter: str # Player who sent the message jumps_from_current: Optional[int] # Distance from current system ``` @@ -381,9 +381,9 @@ Types of alerts that can be triggered. ```python class AlertType(Enum): - VISUAL_BORDER = "border" # Flash window border - VISUAL_OVERLAY = "overlay" # Show overlay on preview - AUDIO = "audio" # Play sound + VISUAL_BORDER = "border" # Flash window border + VISUAL_OVERLAY = "overlay" # Show overlay on preview + AUDIO = "audio" # Play sound SYSTEM_NOTIFICATION = "notification" # Desktop notification ``` @@ -405,7 +405,7 @@ class AlertConfig: # Thresholds min_threat_level: str = "warning" # Minimum level to alert on - jumps_threshold: int = 5 # Only alert if within N jumps + jumps_threshold: int = 5 # Only alert if within N jumps # Visual settings border_color: str = "#FF0000" @@ -438,9 +438,9 @@ dispatcher.alert_triggered.connect(my_handler) #### Signals ```python -border_flash_requested = Signal(str, int) # (color, duration_ms) -overlay_requested = Signal(object) # (IntelReport) -alert_triggered = Signal(object, object) # (IntelReport, AlertType) +border_flash_requested = Signal(str, int) # (color, duration_ms) +overlay_requested = Signal(object) # (IntelReport) +alert_triggered = Signal(object, object) # (IntelReport, AlertType) ``` #### `__init__(config: Optional[AlertConfig] = None, parent: Optional[QObject] = None)` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1bb3583..7e9c618 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -495,7 +495,8 @@ All subprocess calls validate window IDs to prevent command injection: ```python # Window ID validation regex -WINDOW_ID_PATTERN = re.compile(r'^0x[0-9a-fA-F]+$') +WINDOW_ID_PATTERN = re.compile(r"^0x[0-9a-fA-F]+$") + def validate_window_id(window_id: str) -> bool: """Validate window ID format before subprocess calls.""" diff --git a/docs/SMOKE_TEST_v3.2.0.md b/docs/SMOKE_TEST_v3.2.0.md index 79cf8c4..8f8ca83 100644 --- a/docs/SMOKE_TEST_v3.2.0.md +++ b/docs/SMOKE_TEST_v3.2.0.md @@ -206,8 +206,8 @@ from datetime import datetime # In MainWindowV21 instance scope (e.g., self): report = IntelReport( - system="HED-GP", # change to test specific systems - threat_level=ThreatLevel.DANGER, # CLEAR / INFO / WARNING / DANGER / CRITICAL + system="HED-GP", # change to test specific systems + threat_level=ThreatLevel.DANGER, # CLEAR / INFO / WARNING / DANGER / CRITICAL hostile_count=3, ship_types=["sabre", "broadsword"], player_names=[], From 02518ea6759cc9d48eb18e27f8deab7c8897ce42 Mon Sep 17 00:00:00 2001 From: AreteDriver Date: Thu, 6 Aug 2026 02:58:05 -0700 Subject: [PATCH 7/7] fix: strip legacy addTab calls so the IA containers own the four tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 IA wraps every v2.2 inner widget in one of four containers (CommandTab / FleetTab / LayoutsContainer / SystemTab), but the inner factories (_create_main_tab, _create_characters_tab, _create_hotkeys_tab, _create_intel_tab, _create_settings_sync_tab, _create_settings_tab) still called self.tabs.addTab(...) — so the QTabWidget received both the legacy label (Overview / Roster / Cycle Control / Intel / Sync / Settings) AND the IA wrapper. User's screenshot set showed 5 visible tabs (10 in total) with the Command Center flagship duplicated between Overview and Command. Strip the redundant addTab calls; keep every self. attribute assignment and signal.connect(...) so cross-tab wiring stays intact. Update each factory's docstring to reflect 'inner widget, not a tab'. Add two regression tests in TestPhase4InformationArchitecture: * test_no_legacy_tab_labels_in_ia — forbids the v2.2 labels in _TAB_LABELS, so any future re-addTab in an inner factory surfaces as a label-drift failure. * test_four_ia_containers_register_tabs — patches the four IA containers and asserts addTab is called exactly four times with the IA labels. Inner-factory addTab shows up as count drift. Update scripts/capture_all_tabs.py to iterate the four IA tabs (command / fleet / layouts / system) instead of the legacy six. truth-baseline.json: regenerate to match v3.3.0 across pyproject, README, and CLAUDE.md. Co-Authored-By: Claude --- scripts/capture_all_tabs.py | 12 +-- src/argus_overview/ui/main_window_v21.py | 60 +++++++++++--- tests/test_main_window_v21.py | 99 ++++++++++++++++++++++-- truth-baseline.json | 12 +-- 4 files changed, 153 insertions(+), 30 deletions(-) diff --git a/scripts/capture_all_tabs.py b/scripts/capture_all_tabs.py index 2ff2ff8..6591cdf 100644 --- a/scripts/capture_all_tabs.py +++ b/scripts/capture_all_tabs.py @@ -38,13 +38,13 @@ def main(): window.resize(1440, 900) window.show() + # v3.3 OPS Phase 4 IA: COMMAND / FLEET / LAYOUTS / SYSTEM. Index 3 is + # the trailing tab "System" (last in registration order). tabs = [ - (0, "overview"), - (1, "cycle_control"), - (2, "roster"), - (3, "intel"), - (4, "sync"), - (5, "settings"), + (0, "command"), + (1, "fleet"), + (2, "layouts"), + (3, "system"), ] idx = 0 diff --git a/src/argus_overview/ui/main_window_v21.py b/src/argus_overview/ui/main_window_v21.py index a7c231c..30ce853 100644 --- a/src/argus_overview/ui/main_window_v21.py +++ b/src/argus_overview/ui/main_window_v21.py @@ -822,7 +822,14 @@ def _create_layouts_tab(self) -> None: # _on_layout_applied twice per apply. def _create_main_tab(self): - """Create Overview tab (window preview management) - formerly 'Main'""" + """Create the inner MainTab used by the COMMAND container. + + The MainTab is *not* added to the QTabWidget directly. The v3.3 + IA container (:class:`CommandTab`) wraps it inside CommandCenter + via :class:`CommandIntegrator` which reads MainTab's + ``window_manager`` for character mirroring. We keep MainTab as + ``self.main_tab`` so cross-tab signal connections remain stable. + """ from argus_overview.ui.main_tab import MainTab self.main_tab = MainTab( @@ -831,14 +838,20 @@ def _create_main_tab(self): settings_manager=self.settings_manager, layout_manager=self.layout_manager, ) - self.tabs.addTab(self.main_tab, "Overview") # Connect signals self.main_tab.character_detected.connect(self._on_character_detected) self.main_tab.layout_applied.connect(self._on_layout_applied) def _create_characters_tab(self): - """Create Roster tab (character & team management) - formerly 'Characters & Teams'""" + """Create the inner CharactersTeamsTab used by the FLEET container. + + The Roster widget is *not* added to the QTabWidget directly. The + v3.3 IA container :class:`FleetTab` wraps it inside a 60/40 + splitter beside :class:`IntelTab`. We keep it as + ``self.characters_tab`` so cross-tab signal connections remain + stable. + """ from argus_overview.ui.characters_teams_tab import CharactersTeamsTab self.characters_tab = CharactersTeamsTab( @@ -846,19 +859,24 @@ def _create_characters_tab(self): self.layout_manager, settings_sync=self.settings_sync, # v2.2: Enable EVE folder scanning ) - self.tabs.addTab(self.characters_tab, "Roster") # Connect signals self.characters_tab.team_selected.connect(self._on_team_selected) def _create_hotkeys_tab(self): - """Create Cycle Control tab (hotkeys, cycling, alerts)""" + """Create the inner HotkeysTab used by the LAYOUTS container. + + The Cycle Control widget is *not* added to the QTabWidget + directly. The v3.3 IA container :class:`LayoutsContainer` wraps + it inside a 70/30 splitter beside :class:`LayoutsTab`. We keep + it as ``self.hotkeys_tab`` so cross-tab signal connections + remain stable. + """ from argus_overview.ui.hotkeys_tab import HotkeysTab self.hotkeys_tab = HotkeysTab( self.character_manager, self.settings_manager, main_tab=self.main_tab ) - self.tabs.addTab(self.hotkeys_tab, "Cycle Control") # Connect group changes to refresh layout sources in overview tab self.hotkeys_tab.group_changed.connect(self.main_tab.refresh_layout_groups) @@ -873,11 +891,17 @@ def _create_hotkeys_tab(self): self.hotkeys_tab.cycle_backward_edit.recordingStopped.connect(self.hotkey_manager.resume) def _create_intel_tab(self): - """Create Intel tab (chat log monitoring and alerts)""" + """Create the inner IntelTab used by the FLEET container. + + The Intel widget is *not* added to the QTabWidget directly. The + v3.3 IA container :class:`FleetTab` wraps it inside a 60/40 + splitter beside :class:`CharactersTeamsTab`. We keep it as + ``self.intel_tab`` so cross-tab signal connections remain + stable. + """ from argus_overview.ui.intel_tab import IntelTab self.intel_tab = IntelTab(self.settings_manager) - self.tabs.addTab(self.intel_tab, "Intel") # Connect alert signals to main window for visual feedback self.intel_tab.alert_triggered.connect(self._on_intel_alert) @@ -967,18 +991,30 @@ def _on_intel_received(self, report): pass def _create_settings_sync_tab(self): - """Create Sync tab (EVE settings sync) - formerly 'Settings Sync'""" + """Create the inner SettingsSyncTab used by the SYSTEM container. + + The Sync widget is *not* added to the QTabWidget directly. The + v3.3 IA container :class:`SystemTab` wraps it inside a 60/40 + splitter beside :class:`SettingsTab`. We keep it as + ``self.settings_sync_tab`` so cross-tab signal connections + remain stable. + """ from argus_overview.ui.settings_sync_tab import SettingsSyncTab self.settings_sync_tab = SettingsSyncTab(self.settings_sync, self.character_manager) - self.tabs.addTab(self.settings_sync_tab, "Sync") def _create_settings_tab(self): - """Create Settings tab (application settings)""" + """Create the inner SettingsTab used by the SYSTEM container. + + The Settings widget is *not* added to the QTabWidget directly. + The v3.3 IA container :class:`SystemTab` wraps it inside a 60/40 + splitter beside :class:`SettingsSyncTab`. We keep it as + ``self.settings_tab`` so cross-tab signal connections remain + stable. + """ from argus_overview.ui.settings_tab import SettingsTab self.settings_tab = SettingsTab(self.settings_manager, self.hotkey_manager) - self.tabs.addTab(self.settings_tab, "Settings") # Connect signals self.settings_tab.settings_changed.connect(self._apply_setting) diff --git a/tests/test_main_window_v21.py b/tests/test_main_window_v21.py index 521b937..ceddd3c 100644 --- a/tests/test_main_window_v21.py +++ b/tests/test_main_window_v21.py @@ -449,6 +449,69 @@ def test_settings_routes_to_system(self) -> None: assert "Settings" not in MainWindowV21._TAB_LABELS assert "System" in MainWindowV21._TAB_LABELS + def test_no_legacy_tab_labels_in_ia(self) -> None: + """Pin the legacy v2.2 labels out of the IA. + + Inner-widget factories (_create_main_tab, _create_characters_tab, + _create_hotkeys_tab, _create_intel_tab, _create_settings_tab, + _create_settings_sync_tab) MUST NOT register as top-level tabs — + they're consumed by the IA containers. If a regression re-adds an + addTab() to one of those factories, this test surfaces the drift + with a focused failure pointing at the offending label. + """ + forbidden = { + "Overview", + "Roster", + "Cycle Control", + "Intel", + "Sync", + "Settings", + } + from argus_overview.ui.main_window_v21 import MainWindowV21 + + assert forbidden.isdisjoint(MainWindowV21._TAB_LABELS), ( + f"Legacy labels leaking into IA: {forbidden & set(MainWindowV21._TAB_LABELS)}" + ) + + def test_four_ia_containers_register_tabs(self) -> None: + """Each IA container registers exactly one top-level tab. + + Catches the duplicate-tab regression: when an inner-widget + factory retains its legacy `self.tabs.addTab(...)` call, the + QTabWidget receives both the wrapper container and the inner + widget for the same surface, doubling the tab count. Pin the + container-side addTab count to 4 (one per IA container: + Command, Fleet, Layouts, System). + + The IA container classes are patched because their real + constructors call ``addWidget(MagicMock)`` on a QSplitter, which + Qt rejects at runtime. This test cares about the addTab call + surface, not the container internals (those are covered by + ``test_tab_containers.py``). + """ + from unittest.mock import MagicMock, patch + + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MagicMock() + window.tabs = MagicMock() + + with patch("argus_overview.ui.tabs.command_tab.CommandTab"), patch( + "argus_overview.ui.tabs.fleet_tab.FleetTab" + ), patch("argus_overview.ui.tabs.layouts_tab.LayoutsContainer"), patch( + "argus_overview.ui.tabs.system_tab.SystemTab" + ): + MainWindowV21._create_command_tab(window) + MainWindowV21._create_fleet_tab(window) + MainWindowV21._create_layouts_container(window) + MainWindowV21._create_system_tab(window) + + assert window.tabs.addTab.call_count == 4, ( + f"Expected 4 addTab calls (one per IA container), got {window.tabs.addTab.call_count}" + ) + labels = [call.args[1] for call in window.tabs.addTab.call_args_list] + assert labels == ["Command", "Fleet", "Layouts", "System"] + def test_create_layouts_tab_passes_main_tab_to_main_slot(self) -> None: """_create_layouts_tab must bind ``main_tab`` to LayoutsTab.main_tab. @@ -1630,7 +1693,15 @@ def test_create_main_tab(self, mock_tab_class): # Should create tab with correct arguments mock_tab_class.assert_called_once() - window.tabs.addTab.assert_called_once() + + # The inner factory must NOT addTab — the IA container (CommandTab) + # owns the QTabWidget slot. Registering here would double-register + # MainTab and the wrapper, producing duplicate tabs in the IA. + window.tabs.addTab.assert_not_called() + + # Inner widget still lives on the window as `main_tab` so cross-tab + # signals (character_detected, layout_applied) keep firing. + assert window.main_tab is mock_tab # Should connect signals assert mock_tab.character_detected.connect.called @@ -1660,7 +1731,11 @@ def test_create_characters_tab(self, mock_tab_class): # Should create tab mock_tab_class.assert_called_once() - window.tabs.addTab.assert_called_once() + # Inner factory must NOT addTab — the IA container (FleetTab) owns + # the QTabWidget slot. Registering here would double-register the + # Roster widget. + window.tabs.addTab.assert_not_called() + assert window.characters_tab is mock_tab # Should connect team_selected signal assert mock_tab.team_selected.connect.called @@ -1690,7 +1765,10 @@ def test_create_hotkeys_tab(self, mock_tab_class): # Should create tab mock_tab_class.assert_called_once() - window.tabs.addTab.assert_called_once() + # Inner factory must NOT addTab — the IA container (LayoutsContainer) + # owns the QTabWidget slot. + window.tabs.addTab.assert_not_called() + assert window.hotkeys_tab is mock_tab # Should connect group_changed signal assert mock_tab.group_changed.connect.called @@ -1722,7 +1800,10 @@ def test_create_settings_sync_tab(self, mock_tab_class): # Should create tab mock_tab_class.assert_called_once() - window.tabs.addTab.assert_called_once() + # Inner factory must NOT addTab — the IA container (SystemTab) owns + # the QTabWidget slot. + window.tabs.addTab.assert_not_called() + assert window.settings_sync_tab is mock_tab # Test _create_settings_tab @@ -1747,7 +1828,10 @@ def test_create_settings_tab(self, mock_tab_class): # Should create tab mock_tab_class.assert_called_once() - window.tabs.addTab.assert_called_once() + # Inner factory must NOT addTab — the IA container (SystemTab) owns + # the QTabWidget slot. + window.tabs.addTab.assert_not_called() + assert window.settings_tab is mock_tab # Should connect settings_changed signal assert mock_tab.settings_changed.connect.called @@ -1921,7 +2005,10 @@ def test_create_intel_tab(self, mock_tab_class): # Should create tab mock_tab_class.assert_called_once_with(window.settings_manager) - window.tabs.addTab.assert_called_once() + # Inner factory must NOT addTab — the IA container (FleetTab) owns + # the QTabWidget slot. + window.tabs.addTab.assert_not_called() + assert window.intel_tab is mock_tab # Should connect signals assert mock_tab.alert_triggered.connect.called diff --git a/truth-baseline.json b/truth-baseline.json index 35001f6..c92b22d 100644 --- a/truth-baseline.json +++ b/truth-baseline.json @@ -1,6 +1,6 @@ { "project": "Argus_Overview", - "timestamp": "2026-07-27T01:10:19.429673+00:00", + "timestamp": "2026-08-06T09:30:40.178528+00:00", "summary": { "pass": 7, "fail": 0, @@ -12,14 +12,14 @@ "name": "version_consistency", "check_type": "version_consistency", "status": "PASS", - "expected": "3.2.0", + "expected": "3.3.0", "actual": { - "pyproject.toml": "3.2.0", - "README.md": "3.2.0", - "CLAUDE.md": "3.2.0" + "pyproject.toml": "3.3.0", + "README.md": "3.3.0", + "CLAUDE.md": "3.3.0" }, "claim_source": "", - "message": "Versions: {'pyproject.toml': '3.2.0', 'README.md': '3.2.0', 'CLAUDE.md': '3.2.0'}" + "message": "Versions: {'pyproject.toml': '3.3.0', 'README.md': '3.3.0', 'CLAUDE.md': '3.3.0'}" }, { "name": "v6_artifacts_absent",