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)
)
# Set mode from settings (default: hotkey toggle)
# Set mode from settings (default: game focused - Blish HUD style)
settings = get_settings()
mode_str = settings.get('activity_bar.overlay_mode', 'overlay_toggle')
mode_str = settings.get('activity_bar.overlay_mode', 'overlay_game')
try:
mode = OverlayMode(mode_str)
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.start()
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:
print("[Core] Activity Bar not available")

View File

@ -30,9 +30,10 @@ class OverlayMode(Enum):
@dataclass
class OverlayConfig:
"""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)
game_focus_poll_interval: int = 2000 # ms (2 seconds)
game_focus_poll_interval: int = 1000 # ms (1 second - faster response)
def to_dict(self):
return {
@ -162,29 +163,78 @@ class OverlayController:
self.visibility_changed.emit(False)
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:
return
# Track time for debugging
import time
start = time.perf_counter()
try:
# Quick check without blocking
eu_window = self.window_manager.find_eu_window()
# Set a flag to prevent re-entrancy
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:
is_focused = eu_window.is_focused
if is_focused != self._game_focused:
self._game_focused = is_focused
if is_focused:
print("[OverlayController] EU focused - showing overlay")
self._show()
else:
print("[OverlayController] EU unfocused - hiding overlay")
self._hide()
else:
# No EU window - hide if visible
# No EU window found - hide if visible
if self._is_visible:
print("[OverlayController] EU window not found - hiding overlay")
self._hide()
self._game_focused = False
except Exception:
# Silently ignore errors
pass
# Log if slow
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

View File

@ -121,6 +121,37 @@ class SettingsView(QWidget):
self.minimize_cb = QCheckBox("Minimize to tray on close")
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.addStretch()
@ -334,6 +365,33 @@ class SettingsView(QWidget):
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):
"""Export all data."""
path, _ = QFileDialog.getSaveFileName(