chore: prepare for premium architecture transformation

This commit is contained in:
devmatrix 2026-02-16 21:42:53 +00:00
parent 0e5a7148fd
commit 5e44355e52
1 changed files with 107 additions and 64 deletions

View File

@ -12,9 +12,12 @@ Modes:
- OVERLAY_TEMPORARY: Show for 7-10 seconds on hotkey, then hide - OVERLAY_TEMPORARY: Show for 7-10 seconds on hotkey, then hide
""" """
import threading
import time
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from functools import lru_cache
from PyQt6.QtCore import QTimer, pyqtSignal, QObject from PyQt6.QtCore import QTimer, pyqtSignal, QObject
@ -33,7 +36,7 @@ class OverlayConfig:
# Default to game-focused mode (Blish HUD style) # Default to game-focused mode (Blish HUD style)
mode: OverlayMode = OverlayMode.OVERLAY_GAME_FOCUSED 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 = 1000 # ms (1 second - faster response) game_focus_poll_interval: int = 5000 # ms (5 seconds - reduces CPU load)
def to_dict(self): def to_dict(self):
return { return {
@ -53,7 +56,7 @@ class OverlayConfig:
return cls( return cls(
mode=mode, mode=mode,
temporary_duration=data.get('temporary_duration', 8000), temporary_duration=data.get('temporary_duration', 8000),
game_focus_poll_interval=data.get('game_focus_poll_interval', 2000) game_focus_poll_interval=data.get('game_focus_poll_interval', 5000)
) )
@ -80,9 +83,20 @@ class OverlayController(QObject):
self._is_visible = False self._is_visible = False
self._game_focused = False self._game_focused = False
# Caching for performance
self._cached_window_state = None
self._last_check_time = 0
self._cache_ttl = 2.0 # Cache window state for 2 seconds
# Threading for non-blocking focus checks
self._focus_check_thread = None
self._focus_check_lock = threading.Lock()
self._focus_check_running = False
self._pending_focus_result = None
# Timers # Timers
self._game_focus_timer = QTimer() self._game_focus_timer = QTimer()
self._game_focus_timer.timeout.connect(self._check_game_focus) self._game_focus_timer.timeout.connect(self._check_game_focus_async)
self._temporary_timer = QTimer() self._temporary_timer = QTimer()
self._temporary_timer.setSingleShot(True) self._temporary_timer.setSingleShot(True)
@ -101,6 +115,8 @@ class OverlayController(QObject):
"""Stop all timers and hide overlay.""" """Stop all timers and hide overlay."""
self._game_focus_timer.stop() self._game_focus_timer.stop()
self._temporary_timer.stop() self._temporary_timer.stop()
# Stop any running focus check thread
self._focus_check_running = False
if self.activity_bar: if self.activity_bar:
self.activity_bar.hide() self.activity_bar.hide()
@ -118,9 +134,9 @@ class OverlayController(QObject):
self._show() self._show()
elif self._mode == OverlayMode.OVERLAY_GAME_FOCUSED: elif self._mode == OverlayMode.OVERLAY_GAME_FOCUSED:
# Show only when game focused # Show only when game focused - use 5 second poll interval
self._game_focus_timer.start(self.config.game_focus_poll_interval) self._game_focus_timer.start(self.config.game_focus_poll_interval)
self._check_game_focus() # Check immediately self._check_game_focus_async() # Check immediately (non-blocking)
elif self._mode == OverlayMode.OVERLAY_HOTKEY_TOGGLE: elif self._mode == OverlayMode.OVERLAY_HOTKEY_TOGGLE:
# Hotkey toggle - start hidden # Hotkey toggle - start hidden
@ -163,79 +179,106 @@ class OverlayController(QObject):
self._is_visible = False self._is_visible = False
self.visibility_changed.emit(False) self.visibility_changed.emit(False)
def _check_game_focus(self): def _check_game_focus_async(self):
"""Check if EU game window is focused - non-blocking using QTimer.singleShot.""" """Start focus check in background thread to avoid blocking UI."""
# Run the actual check in next event loop iteration to not block # Use cached result if available and recent
from PyQt6.QtCore import QTimer current_time = time.time()
QTimer.singleShot(0, self._do_check_game_focus) if self._cached_window_state is not None and (current_time - self._last_check_time) < self._cache_ttl:
self._apply_focus_state(self._cached_window_state)
return
def _do_check_game_focus(self): # Don't start a new thread if one is already running
"""Actual focus check (non-blocking).""" with self._focus_check_lock:
if self._focus_check_running:
return
self._focus_check_running = True
# Start background thread for focus check
self._focus_check_thread = threading.Thread(
target=self._do_focus_check_threaded,
daemon=True,
name="FocusCheckThread"
)
self._focus_check_thread.start()
def _do_focus_check_threaded(self):
"""Background thread: Check focus without blocking main thread."""
start_time = time.perf_counter()
try:
# Check if window manager is available
if not self.window_manager: if not self.window_manager:
return return
# Track time for debugging # Use fast cached window find
import time is_focused = self._fast_focus_check()
start = time.perf_counter()
try: # Store result for main thread to apply
# Set a flag to prevent re-entrancy self._pending_focus_result = is_focused
if getattr(self, '_checking_focus', False):
return
self._checking_focus = True
# Quick check - limit window enumeration time # Update cache
eu_window = self._quick_find_eu_window() self._cached_window_state = is_focused
self._last_check_time = time.time()
if eu_window: # Schedule UI update on main thread
is_focused = eu_window.is_focused from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
if is_focused != self._game_focused: QMetaObject.invokeMethod(
self._game_focused = is_focused self,
if is_focused: "_apply_pending_focus_result",
print("[OverlayController] EU focused - showing overlay") Qt.ConnectionType.QueuedConnection
self._show() )
else:
print("[OverlayController] EU unfocused - hiding overlay")
self._hide()
else:
# 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
# Log if slow # Log performance (only if slow, for debugging)
elapsed = (time.perf_counter() - start) * 1000 elapsed = (time.perf_counter() - start_time) * 1000
if elapsed > 100: if elapsed > 50: # Log if >50ms (way above our 10ms target)
print(f"[OverlayController] Warning: Focus check took {elapsed:.1f}ms") print(f"[OverlayController] Warning: Focus check took {elapsed:.1f}ms")
except Exception as e: except Exception as e:
print(f"[OverlayController] Error checking focus: {e}") # Silently handle errors in background thread
pass
finally: finally:
self._checking_focus = False self._focus_check_running = False
def _quick_find_eu_window(self):
"""Quick window find with timeout protection."""
import time
start = time.perf_counter()
def _fast_focus_check(self) -> bool:
"""Fast focus check using cached window handle and psutil."""
try: try:
# Try window manager first # Use psutil for fast process-based detection (no window enumeration)
if hasattr(self.window_manager, 'find_eu_window'): if hasattr(self.window_manager, 'is_eu_focused_fast'):
# Wrap in timeout check return self.window_manager.is_eu_focused_fast()
result = self.window_manager.find_eu_window()
# If taking too long, skip future checks # Fallback to window manager's optimized method
elapsed = (time.perf_counter() - start) * 1000 if hasattr(self.window_manager, 'find_eu_window_cached'):
if elapsed > 500: # More than 500ms is too slow window = self.window_manager.find_eu_window_cached()
print(f"[OverlayController] Window find too slow ({elapsed:.1f}ms), disabling focus detection") if window:
self._game_focus_timer.stop() return window.is_focused
return None
return result # Last resort - standard find (slow)
except Exception as e: window = self.window_manager.find_eu_window()
print(f"[OverlayController] Window find error: {e}") if window:
return None return window.is_focused
return False
except Exception:
return False
def _apply_pending_focus_result(self):
"""Apply focus result from background thread (called on main thread)."""
if self._pending_focus_result is not None:
self._apply_focus_state(self._pending_focus_result)
self._pending_focus_result = None
def _apply_focus_state(self, is_focused: bool):
"""Apply focus state change (early exit if no change)."""
# Early exit - no change in state
if is_focused == self._game_focused:
return
# Update state and visibility
self._game_focused = is_focused
if is_focused:
self._show()
else:
self._hide()
# Singleton instance # Singleton instance