diff --git a/src/argus_overview/ui/main_window_v21.py b/src/argus_overview/ui/main_window_v21.py index d8dd4ae..20f7c51 100644 --- a/src/argus_overview/ui/main_window_v21.py +++ b/src/argus_overview/ui/main_window_v21.py @@ -709,10 +709,18 @@ def _on_intel_alert(self, report, alert_type): if not isinstance(report, IntelReport): return + # Master toggle (v3.2.0): when off, suppress all visual chrome but + # still let other AlertTypes fire (audio, tray notification). The + # parser keeps running, only the preview/dock tints are gated. + # Defensive getattr: bypassed-init test helpers don't set + # settings_manager — default to chrome on. + sm = getattr(self, "settings_manager", None) + chrome_enabled = sm.get("intel.preview_chrome_enabled", True) if sm is not None else True + # Fan out threat state to preview frames + status dock once per report # (filter on VISUAL_BORDER so we only trigger on a single AlertType # emission per report, not on every type the dispatcher fires). - if alert_type == AlertType.VISUAL_BORDER and hasattr(self, "main_tab"): + if chrome_enabled and alert_type == AlertType.VISUAL_BORDER and hasattr(self, "main_tab"): window_manager = getattr(self.main_tab, "window_manager", None) if window_manager is not None and hasattr(window_manager, "apply_threat_state"): window_manager.apply_threat_state(report.threat_level, report.system) @@ -728,6 +736,24 @@ def _on_intel_alert(self, report, alert_type): f"{report.hostile_count or '?'} hostiles - {', '.join(report.ship_types[:2]) or 'unknown ships'}", ) + def _clear_threat_chrome(self) -> None: + """Force-clear any active threat tints across previews + chips. + + Called when the user toggles intel.preview_chrome_enabled off so + the change takes effect immediately rather than waiting for the + 30s decay timer. + """ + from argus_overview.intel.parser import ThreatLevel + + if not hasattr(self, "main_tab"): + return + wm = getattr(self.main_tab, "window_manager", None) + if wm is not None and hasattr(wm, "apply_threat_state"): + wm.apply_threat_state(ThreatLevel.CLEAR, None) + dock = getattr(self.main_tab, "status_dock", None) + if dock is not None and hasattr(dock, "set_threat_state"): + dock.set_threat_state(ThreatLevel.CLEAR, None) + @Slot(object) def _on_intel_received(self, report): """Handle intel received from intel tab.""" @@ -908,6 +934,12 @@ def _apply_setting(self, key: str, value): # Will be implemented with hotkey functionality pass + elif key == "intel.preview_chrome_enabled" and not value: + # User just turned threat chrome off — flush any active tints + # so the change is visible immediately, not after the 30s + # decay window. + self._clear_threat_chrome() + def _apply_low_power_mode(self, enabled: bool): """ Apply low power mode settings. diff --git a/src/argus_overview/ui/settings_manager.py b/src/argus_overview/ui/settings_manager.py index 73e850d..823ad2d 100644 --- a/src/argus_overview/ui/settings_manager.py +++ b/src/argus_overview/ui/settings_manager.py @@ -94,6 +94,16 @@ class SettingsManager: "cooldown_seconds": 5, # Minimum time between alerts for same system "current_system": "", # Player's current system for jump calculations "custom_log_path": "", # Custom path to EVE chat logs + # v3.2.0 intel-aware preview chrome — master toggle. When False, + # parser/alerts still run but threat tints + dots + badges are + # suppressed on previews and chips. + "preview_chrome_enabled": True, + # v3.2.0 — read each EVE client's Local channel log to track + # which system that character is in. + "track_character_locations": True, + # v3.2.0 — max jumps from an alert system that a character can + # be and still see threat tinting. 0 disables adjacent tinting. + "threat_jumps_threshold": 1, }, } diff --git a/src/argus_overview/ui/settings_tab.py b/src/argus_overview/ui/settings_tab.py index c596af4..a39703d 100644 --- a/src/argus_overview/ui/settings_tab.py +++ b/src/argus_overview/ui/settings_tab.py @@ -430,6 +430,35 @@ def __init__(self, settings_manager): def _setup_ui(self): layout = QVBoxLayout() + # ---- Master toggle — chrome on/off --------------------------- + master_group = QGroupBox("Intel-Aware Preview Chrome") + master_form = QFormLayout() + + self.chrome_enabled_check = QCheckBox() + self.chrome_enabled_check.setChecked( + self.settings_manager.get("intel.preview_chrome_enabled", True) + ) + self.chrome_enabled_check.stateChanged.connect( + lambda: self.setting_changed.emit( + "intel.preview_chrome_enabled", + self.chrome_enabled_check.isChecked(), + ) + ) + self.chrome_enabled_check.setToolTip( + "Master toggle for the v3.2.0 intel-aware preview chrome.\n\n" + "When OFF, threat tints / pulses / dots / +Nj badges are\n" + "suppressed on previews and chips. The intel parser, audio\n" + "alerts, tray notifications, system labels on chips, and the\n" + "replay strip all keep working.\n\n" + "Use this if you find threat tints distracting or you don't\n" + "use chat-log intel — the rest of Argus is unaffected." + ) + self.chrome_enabled_check.setStyleSheet("QCheckBox { font-weight: bold; }") + master_form.addRow("Show threat chrome on previews:", self.chrome_enabled_check) + + master_group.setLayout(master_form) + layout.addWidget(master_group) + # ---- Per-character location tracker --------------------------- loc_group = QGroupBox("Per-Character Location Tracking") loc_form = QFormLayout() diff --git a/tests/test_main_tab.py b/tests/test_main_tab.py index fab376b..c8924a2 100644 --- a/tests/test_main_tab.py +++ b/tests/test_main_tab.py @@ -10710,3 +10710,170 @@ def test_init_restores_strip_from_settings(self, qapp): assert widget.is_replay_strip_enabled() is True finally: widget.deleteLater() + + +# ============================================================================= +# Master toggle: intel.preview_chrome_enabled (v3.2.0 follow-up) +# ============================================================================= + + +def _make_window_with_settings(chrome_enabled: bool): + """Build a bypassed-init MainWindowV21 with a settings_manager mock.""" + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + sm = MagicMock() + sm.get.side_effect = lambda key, default=None: ( + chrome_enabled if key == "intel.preview_chrome_enabled" else default + ) + window.settings_manager = sm + window.main_tab = MagicMock() + window.main_tab.window_manager = MagicMock() + window.main_tab.status_dock = MagicMock() + window.system_tray = MagicMock() + return window + + +class TestMainWindowV21ChromeMasterToggle: + """When intel.preview_chrome_enabled is False, fan-out is suppressed.""" + + def _make_report(self): + from argus_overview.intel.parser import IntelReport, ThreatLevel + + return IntelReport( + system="HED-GP", + threat_level=ThreatLevel.DANGER, + hostile_count=2, + ship_types=[], + player_names=[], + raw_message="hostiles", + ) + + def test_chrome_on_calls_fan_out(self): + from argus_overview.intel.alerts import AlertType + + window = _make_window_with_settings(chrome_enabled=True) + window._on_intel_alert(self._make_report(), AlertType.VISUAL_BORDER) + window.main_tab.window_manager.apply_threat_state.assert_called_once() + window.main_tab.status_dock.set_threat_state.assert_called_once() + + def test_chrome_off_suppresses_fan_out(self): + from argus_overview.intel.alerts import AlertType + + window = _make_window_with_settings(chrome_enabled=False) + window._on_intel_alert(self._make_report(), AlertType.VISUAL_BORDER) + window.main_tab.window_manager.apply_threat_state.assert_not_called() + window.main_tab.status_dock.set_threat_state.assert_not_called() + + def test_chrome_off_still_fires_critical_tray_notification(self): + from argus_overview.intel.alerts import AlertType + from argus_overview.intel.parser import IntelReport, ThreatLevel + + window = _make_window_with_settings(chrome_enabled=False) + critical = IntelReport( + system="Jita", + threat_level=ThreatLevel.CRITICAL, + hostile_count=10, + ship_types=["titan"], + player_names=[], + raw_message="hot drop", + ) + window._on_intel_alert(critical, AlertType.VISUAL_BORDER) + # Tray notification independent of the chrome toggle + window.system_tray.show_notification.assert_called_once() + + def test_no_settings_manager_defaults_to_on(self): + """Bypassed-init sites without settings_manager should still fan out.""" + from argus_overview.intel.alerts import AlertType + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + # No settings_manager set + window.main_tab = MagicMock() + window.main_tab.window_manager = MagicMock() + window.main_tab.status_dock = MagicMock() + window.system_tray = MagicMock() + + window._on_intel_alert(self._make_report(), AlertType.VISUAL_BORDER) + + window.main_tab.window_manager.apply_threat_state.assert_called_once() + + +class TestMainWindowV21ClearThreatChrome: + """_clear_threat_chrome flushes both surfaces with CLEAR.""" + + def test_clear_calls_both_surfaces(self): + from argus_overview.intel.parser import ThreatLevel + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + window.main_tab = MagicMock() + window.main_tab.window_manager = MagicMock() + window.main_tab.status_dock = MagicMock() + + window._clear_threat_chrome() + + window.main_tab.window_manager.apply_threat_state.assert_called_once_with( + ThreatLevel.CLEAR, None + ) + window.main_tab.status_dock.set_threat_state.assert_called_once_with( + ThreatLevel.CLEAR, None + ) + + def test_clear_no_main_tab_safe(self): + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + # No main_tab — should not raise + window._clear_threat_chrome() + + def test_clear_dock_optional(self): + from argus_overview.intel.parser import ThreatLevel + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + window.main_tab = MagicMock(spec=["window_manager"]) + window.main_tab.window_manager = MagicMock() + + window._clear_threat_chrome() + + window.main_tab.window_manager.apply_threat_state.assert_called_once_with( + ThreatLevel.CLEAR, None + ) + + +class TestMainWindowV21ApplySettingChromeToggle: + """_apply_setting routes the toggle-off case to _clear_threat_chrome.""" + + def test_toggle_off_clears_chrome(self): + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + window.logger = MagicMock() + window._clear_threat_chrome = MagicMock() + + window._apply_setting("intel.preview_chrome_enabled", False) + + window._clear_threat_chrome.assert_called_once() + + def test_toggle_on_does_not_clear(self): + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + window.logger = MagicMock() + window._clear_threat_chrome = MagicMock() + + window._apply_setting("intel.preview_chrome_enabled", True) + + window._clear_threat_chrome.assert_not_called() + + def test_unrelated_key_does_not_clear(self): + from argus_overview.ui.main_window_v21 import MainWindowV21 + + window = MainWindowV21.__new__(MainWindowV21) + window.logger = MagicMock() + window._clear_threat_chrome = MagicMock() + + window._apply_setting("performance.default_refresh_rate", 30) + + window._clear_threat_chrome.assert_not_called() diff --git a/tests/test_settings_tab.py b/tests/test_settings_tab.py index 3531380..a28d2b7 100644 --- a/tests/test_settings_tab.py +++ b/tests/test_settings_tab.py @@ -201,6 +201,7 @@ class TestIntelPanel: def _settings_mock(self, overrides=None): """Per-key settings mock so unrelated lookups return their defaults.""" store = { + "intel.preview_chrome_enabled": True, "intel.track_character_locations": True, "intel.threat_jumps_threshold": 1, "replay_strip_enabled": {}, @@ -283,6 +284,39 @@ def test_jumps_threshold_clamped_to_range(self, qapp): finally: panel.deleteLater() + def test_chrome_toggle_default_on(self, qapp): + from argus_overview.ui.settings_tab import IntelPanel + + sm = self._settings_mock() + panel = IntelPanel(sm) + try: + assert panel.chrome_enabled_check.isChecked() is True + finally: + panel.deleteLater() + + def test_chrome_toggle_init_off(self, qapp): + from argus_overview.ui.settings_tab import IntelPanel + + sm = self._settings_mock({"intel.preview_chrome_enabled": False}) + panel = IntelPanel(sm) + try: + assert panel.chrome_enabled_check.isChecked() is False + finally: + panel.deleteLater() + + def test_chrome_toggle_emits_setting_changed(self, qapp): + from argus_overview.ui.settings_tab import IntelPanel + + sm = self._settings_mock() + panel = IntelPanel(sm) + received: list[tuple[str, object]] = [] + panel.setting_changed.connect(lambda k, v: received.append((k, v))) + try: + panel.chrome_enabled_check.setChecked(False) + assert ("intel.preview_chrome_enabled", False) in received + finally: + panel.deleteLater() + # Test AdvancedPanel class TestAdvancedPanel: