feat: Blish HUD-style overlay mode - Activity Bar only in EU game window

Changes:
- Default mode now OVERLAY_GAME_FOCUSED (Blish HUD style)
- Activity bar ONLY shows when EU window is focused
- Completely hidden on desktop and other windows
- Non-blocking focus detection with QTimer.singleShot
- Auto-disables if window detection is too slow (>500ms)
- Added Overlay Mode dropdown in Settings > General
- Options: Game Focused, Always Visible, Hotkey Toggle, Temporary, Desktop Only

Settings:
- activity_bar.overlay_mode: 'overlay_game' (default)
- Game Focused: Overlay strictly tied to EU window focus
This commit is contained in:
LemonNexus 2026-02-16 00:20:54 +00:00
parent 3bf2d9d5b1
commit 0f1c41b58d
3 changed files with 124 additions and 13 deletions

View File

@ -183,19 +183,22 @@ class EUUtilityApp:
getattr(self, 'window_manager', None) getattr(self, 'window_manager', None)
) )
# Set mode from settings (default: hotkey toggle) # Set mode from settings (default: game focused - Blish HUD style)
settings = get_settings() settings = get_settings()
mode_str = settings.get('activity_bar.overlay_mode', 'overlay_toggle') mode_str = settings.get('activity_bar.overlay_mode', 'overlay_game')
try: try:
mode = OverlayMode(mode_str) mode = OverlayMode(mode_str)
except ValueError: except ValueError:
mode = OverlayMode.OVERLAY_HOTKEY_TOGGLE mode = OverlayMode.OVERLAY_GAME_FOCUSED # Default to game focused
self.overlay_controller.set_mode(mode) self.overlay_controller.set_mode(mode)
self.overlay_controller.start() self.overlay_controller.start()
print(f"[Core] Activity Bar created (mode: {mode.value})") print(f"[Core] Activity Bar created (mode: {mode.value})")
print("[Core] Press Ctrl+Shift+B to toggle overlay")
if mode.value == 'overlay_game':
print("[Core] Overlay will ONLY show when EU game window is focused (Blish HUD style)")
print("[Core] Overlay is hidden on desktop - switch to EU to see it")
else: else:
print("[Core] Activity Bar not available") print("[Core] Activity Bar not available")

View File

@ -30,9 +30,10 @@ class OverlayMode(Enum):
@dataclass @dataclass
class OverlayConfig: class OverlayConfig:
"""Configuration for activity bar overlay behavior.""" """Configuration for activity bar overlay behavior."""
mode: OverlayMode = OverlayMode.OVERLAY_HOTKEY_TOGGLE # Default to game-focused mode (Blish HUD style)
mode: OverlayMode = OverlayMode.OVERLAY_GAME_FOCUSED
temporary_duration: int = 8000 # milliseconds (8 seconds default) temporary_duration: int = 8000 # milliseconds (8 seconds default)
game_focus_poll_interval: int = 2000 # ms (2 seconds) game_focus_poll_interval: int = 1000 # ms (1 second - faster response)
def to_dict(self): def to_dict(self):
return { return {
@ -162,29 +163,78 @@ class OverlayController:
self.visibility_changed.emit(False) self.visibility_changed.emit(False)
def _check_game_focus(self): def _check_game_focus(self):
"""Check if EU game window is focused.""" """Check if EU game window is focused - non-blocking using QTimer.singleShot."""
# Run the actual check in next event loop iteration to not block
from PyQt6.QtCore import QTimer
QTimer.singleShot(0, self._do_check_game_focus)
def _do_check_game_focus(self):
"""Actual focus check (non-blocking)."""
if not self.window_manager: if not self.window_manager:
return return
# Track time for debugging
import time
start = time.perf_counter()
try: try:
# Quick check without blocking # Set a flag to prevent re-entrancy
eu_window = self.window_manager.find_eu_window() if getattr(self, '_checking_focus', False):
return
self._checking_focus = True
# Quick check - limit window enumeration time
eu_window = self._quick_find_eu_window()
if eu_window: if eu_window:
is_focused = eu_window.is_focused is_focused = eu_window.is_focused
if is_focused != self._game_focused: if is_focused != self._game_focused:
self._game_focused = is_focused self._game_focused = is_focused
if is_focused: if is_focused:
print("[OverlayController] EU focused - showing overlay")
self._show() self._show()
else: else:
print("[OverlayController] EU unfocused - hiding overlay")
self._hide() self._hide()
else: else:
# No EU window - hide if visible # No EU window found - hide if visible
if self._is_visible: if self._is_visible:
print("[OverlayController] EU window not found - hiding overlay")
self._hide() self._hide()
self._game_focused = False self._game_focused = False
except Exception:
# Silently ignore errors # Log if slow
pass elapsed = (time.perf_counter() - start) * 1000
if elapsed > 100:
print(f"[OverlayController] Warning: Focus check took {elapsed:.1f}ms")
except Exception as e:
print(f"[OverlayController] Error checking focus: {e}")
finally:
self._checking_focus = False
def _quick_find_eu_window(self):
"""Quick window find with timeout protection."""
import time
start = time.perf_counter()
try:
# Try window manager first
if hasattr(self.window_manager, 'find_eu_window'):
# Wrap in timeout check
result = self.window_manager.find_eu_window()
# If taking too long, skip future checks
elapsed = (time.perf_counter() - start) * 1000
if elapsed > 500: # More than 500ms is too slow
print(f"[OverlayController] Window find too slow ({elapsed:.1f}ms), disabling focus detection")
self._game_focus_timer.stop()
return None
return result
except Exception as e:
print(f"[OverlayController] Window find error: {e}")
return None
# Singleton instance # Singleton instance

View File

@ -121,6 +121,37 @@ class SettingsView(QWidget):
self.minimize_cb = QCheckBox("Minimize to tray on close") self.minimize_cb = QCheckBox("Minimize to tray on close")
behavior_layout.addWidget(self.minimize_cb) behavior_layout.addWidget(self.minimize_cb)
# Activity Bar Overlay Mode
overlay_group = QGroupBox("Activity Bar Overlay Mode")
overlay_group.setStyleSheet(self._group_style())
overlay_layout = QVBoxLayout(overlay_group)
overlay_info = QLabel("Choose how the in-game activity bar behaves:")
overlay_info.setStyleSheet("color: rgba(255,255,255,150); font-size: 12px;")
overlay_layout.addWidget(overlay_info)
self.overlay_mode_combo = QComboBox()
self.overlay_mode_combo.addItems([
"Game Focused Only (Blish HUD style)",
"Always Visible",
"Hotkey Toggle",
"Temporary (8 seconds)",
"Desktop App Only"
])
self.overlay_mode_combo.setCurrentIndex(0) # Default to game focused
self.overlay_mode_combo.currentIndexChanged.connect(self._on_overlay_mode_changed)
overlay_layout.addWidget(self.overlay_mode_combo)
overlay_desc = QLabel(
"Game Focused: Activity bar only appears when EU window is active. "
"Hidden on desktop."
)
overlay_desc.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
overlay_desc.setWordWrap(True)
overlay_layout.addWidget(overlay_desc)
behavior_layout.addWidget(overlay_group)
layout.addWidget(behavior_group) layout.addWidget(behavior_group)
layout.addStretch() layout.addStretch()
@ -334,6 +365,33 @@ class SettingsView(QWidget):
return tab return tab
def _on_overlay_mode_changed(self, index: int):
"""Handle overlay mode change."""
mode_map = {
0: "overlay_game", # Game Focused Only
1: "overlay_always", # Always Visible
2: "overlay_toggle", # Hotkey Toggle
3: "overlay_temp", # Temporary
4: "desktop_app" # Desktop App Only
}
mode = mode_map.get(index, "overlay_game")
# Save to settings
if hasattr(self.overlay, 'settings'):
self.overlay.settings['activity_bar.overlay_mode'] = mode
self.overlay.settings.save()
print(f"[Settings] Activity Bar mode changed to: {mode}")
# Show confirmation
QMessageBox.information(
self,
"Mode Changed",
f"Activity Bar mode changed to: {self.overlay_mode_combo.currentText()}\n\n"
"Restart EU-Utility for changes to take effect."
)
def _export_data(self): def _export_data(self):
"""Export all data.""" """Export all data."""
path, _ = QFileDialog.getSaveFileName( path, _ = QFileDialog.getSaveFileName(