Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/argus_overview/ui/main_window_v21.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +717 to +718
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for retrieving the intel.preview_chrome_enabled setting is repeated and slightly verbose. Consider adding a private helper property to encapsulate this check, which would also make gating other visual effects (like the border flash) cleaner.

    @property
    def _intel_chrome_enabled(self) -> bool:
        sm = getattr(self, "settings_manager", None)
        if sm is None:
            return True
        return sm.get("intel.preview_chrome_enabled", 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)
Expand All @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/argus_overview/ui/settings_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

Expand Down
29 changes: 29 additions & 0 deletions src/argus_overview/ui/settings_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
)
Comment on lines +441 to +446
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For boolean settings controlled by a QCheckBox, it is more idiomatic in PySide6 to use the toggled(bool) signal instead of stateChanged(int). This avoids the need to manually call isChecked() inside the lambda and provides a cleaner boolean interface.

        self.chrome_enabled_check.toggled.connect(
            lambda checked: self.setting_changed.emit(
                "intel.preview_chrome_enabled",
                checked,
            )
        )

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()
Expand Down
167 changes: 167 additions & 0 deletions tests/test_main_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
34 changes: 34 additions & 0 deletions tests/test_settings_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down Expand Up @@ -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:
Expand Down
Loading