diff --git a/core/main.py b/core/main.py index eab2099..49355d2 100644 --- a/core/main.py +++ b/core/main.py @@ -165,27 +165,47 @@ class EUUtilityApp: self.tray_icon.quit_app.connect(self.quit) debug_logger.end_timer("MAIN_create_tray") - # Create Activity Bar (in-game overlay) - hidden by default + # Create Activity Bar (in-game overlay) - controlled by overlay controller debug_logger.start_timer("MAIN_create_activitybar") print("Creating Activity Bar...") try: from core.activity_bar import get_activity_bar + from core.overlay_controller import get_overlay_controller, OverlayMode + self.activity_bar = get_activity_bar(self.plugin_manager) if self.activity_bar: - if self.activity_bar.config.enabled: - print("[Core] Activity Bar created (will show when EU is focused)") - # Connect signals - self.activity_bar.widget_requested.connect(self._on_activity_bar_widget) - # Start EU focus detection - self._start_eu_focus_detection() - else: - print("[Core] Activity Bar disabled in config") + # Connect widget request signal + self.activity_bar.widget_requested.connect(self._on_activity_bar_widget) + + # Get overlay controller with window manager + self.overlay_controller = get_overlay_controller( + self.activity_bar, + getattr(self, 'window_manager', None) + ) + + # Set mode from settings (default: hotkey toggle) + settings = get_settings() + mode_str = settings.get('activity_bar.overlay_mode', 'overlay_toggle') + try: + mode = OverlayMode(mode_str) + except ValueError: + mode = OverlayMode.OVERLAY_HOTKEY_TOGGLE + + 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") + else: print("[Core] Activity Bar not available") self.activity_bar = None + self.overlay_controller = None except Exception as e: print(f"[Core] Failed to create Activity Bar: {e}") + debug_logger.error("MAIN", f"ActivityBar creation failed: {e}") self.activity_bar = None + self.overlay_controller = None debug_logger.end_timer("MAIN_create_activitybar") # Connect hotkey signals @@ -206,12 +226,28 @@ class EUUtilityApp: debug_logger.info("MAIN", f"=== TOTAL STARTUP TIME: {total_startup:.2f}ms ===") print("EU-Utility started!") - print("Dashboard window is open") + print("Dashboard window is open (Desktop App)") print("Press Ctrl+Shift+U to toggle dashboard") print("Press Ctrl+Shift+H to hide all overlays") - print("Press Ctrl+Shift+B to toggle activity bar") + print("Press Ctrl+Shift+B to toggle activity bar overlay") print(f"Loaded {len(self.plugin_manager.get_all_plugins())} plugins") + # Show overlay mode info + if hasattr(self, 'overlay_controller') and self.overlay_controller: + mode = self.overlay_controller._mode + print(f"\nActivity Bar Mode: {mode.value}") + if mode.value == 'overlay_toggle': + print(" - Overlay starts hidden") + print(" - Press Ctrl+Shift+B to toggle visibility") + elif mode.value == 'overlay_temp': + print(" - Press Ctrl+Shift+B to show for 8 seconds") + elif mode.value == 'overlay_game': + print(" - Overlay auto-shows when EU game is focused") + elif mode.value == 'overlay_always': + print(" - Overlay always visible") + elif mode.value == 'desktop_app': + print(" - Activity bar only in desktop app") + # Show Event Bus stats self._print_event_bus_stats() @@ -436,140 +472,25 @@ class EUUtilityApp: debug_logger.end_timer("MAIN_toggle_overlay") def _toggle_activity_bar(self): - """Toggle activity bar visibility.""" + """Toggle activity bar visibility via overlay controller.""" debug_logger.start_timer("MAIN_toggle_activity_bar") - debug_logger.debug("MAIN", f"_toggle_activity_bar called, activity_bar exists: {hasattr(self, 'activity_bar')}") + debug_logger.debug("MAIN", f"_toggle_activity_bar called, controller exists: {hasattr(self, 'overlay_controller') and self.overlay_controller is not None}") - if hasattr(self, 'activity_bar') and self.activity_bar: + if hasattr(self, 'overlay_controller') and self.overlay_controller: try: - if self.activity_bar.isVisible(): - debug_logger.debug("MAIN", "Hiding activity bar...") - self.activity_bar.hide() - if hasattr(self, 'tray_icon') and self.tray_icon: - self.tray_icon.set_activity_bar_checked(False) - debug_logger.debug("MAIN", "Activity bar hidden") - else: - debug_logger.debug("MAIN", "Showing activity bar...") - self.activity_bar.show() - if hasattr(self, 'tray_icon') and self.tray_icon: - self.tray_icon.set_activity_bar_checked(True) - debug_logger.debug("MAIN", "Activity bar shown") + self.overlay_controller.toggle() + # Update tray icon checkbox + is_visible = self.activity_bar.isVisible() if self.activity_bar else False + if hasattr(self, 'tray_icon') and self.tray_icon: + self.tray_icon.set_activity_bar_checked(is_visible) + debug_logger.debug("MAIN", f"Activity bar toggled, now visible: {is_visible}") except Exception as e: debug_logger.error("MAIN", f"Error toggling activity bar: {e}") else: - debug_logger.warn("MAIN", "Activity Bar not available") + debug_logger.warn("MAIN", "Overlay Controller not available") debug_logger.end_timer("MAIN_toggle_activity_bar") - def _start_eu_focus_detection(self): - """Start timer to detect EU window focus and show/hide activity bar.""" - from PyQt6.QtCore import QTimer - - # Check if disabled in settings - settings = get_settings() - if not settings.get('activity_bar.auto_show_on_focus', False): - debug_logger.info("MAIN", "EU focus detection DISABLED in settings (activity_bar.auto_show_on_focus=false)") - return - - debug_logger.info("MAIN", "Starting EU focus detection...") - debug_logger.start_timer("MAIN_start_eu_focus") - - self.eu_focus_timer = QTimer(self.app) - self.eu_focus_timer.timeout.connect(self._check_eu_focus) - self.eu_focus_timer.start(5000) # Check every 5 seconds - self._last_eu_focused = False - self._focus_detection_disabled = False # Will be set to True if too slow - - debug_logger.end_timer("MAIN_start_eu_focus") - debug_logger.info("MAIN", "EU focus detection started (5s interval)") - - def _check_eu_focus(self): - """Check if EU window is focused and show/hide activity bar.""" - # HARD DISABLE: If focus detection has been slow before, don't run it again - if getattr(self, '_focus_detection_disabled', False): - return - - debug_logger.start_timer("MAIN_check_eu_focus") - - try: - if not hasattr(self, 'activity_bar') or not self.activity_bar: - debug_logger.debug("MAIN", "No activity bar, skipping focus check") - return - - if not hasattr(self, 'window_manager') or not self.window_manager: - debug_logger.debug("MAIN", "No window manager, skipping focus check") - return - - if not self.window_manager.is_available(): - debug_logger.debug("MAIN", "Window manager not available, skipping") - return - - debug_logger.debug("MAIN", "Finding EU window...") - eu_window = self.window_manager.find_eu_window() - - if eu_window: - debug_logger.debug("MAIN", "EU window found, checking focus...") - is_focused = eu_window.is_focused # Property, not method! - debug_logger.debug("MAIN", f"EU focused: {is_focused}, last: {getattr(self, '_last_eu_focused', None)}") - - if is_focused != getattr(self, '_last_eu_focused', False): - self._last_eu_focused = is_focused - - if is_focused: - # EU just got focused - show activity bar - if not self.activity_bar.isVisible(): - try: - debug_logger.debug("MAIN", "Showing activity bar (EU focused)") - self.activity_bar.show() - if hasattr(self, 'tray_icon') and self.tray_icon: - self.tray_icon.set_activity_bar_checked(True) - debug_logger.info("MAIN", "EU focused - Activity Bar shown") - except Exception as e: - debug_logger.error("MAIN", f"Error showing activity bar: {e}") - else: - # EU lost focus - hide activity bar - if self.activity_bar.isVisible(): - try: - debug_logger.debug("MAIN", "Hiding activity bar (EU unfocused)") - self.activity_bar.hide() - if hasattr(self, 'tray_icon') and self.tray_icon: - self.tray_icon.set_activity_bar_checked(False) - debug_logger.info("MAIN", "EU unfocused - Activity Bar hidden") - except Exception as e: - debug_logger.error("MAIN", f"Error hiding activity bar: {e}") - - # Reset fail count since we found EU - self._eu_not_found_count = 0 - else: - debug_logger.debug("MAIN", "EU window not found") - - # Track consecutive failures - self._eu_not_found_count = getattr(self, '_eu_not_found_count', 0) + 1 - - # If EU not found for 3 consecutive checks (15 seconds), slow down polling - if self._eu_not_found_count >= 3: - debug_logger.warn("MAIN", f"EU not found {self._eu_not_found_count} times - slowing down focus detection") - # Slow down to once per minute when EU isn't running - if hasattr(self, 'eu_focus_timer'): - self.eu_focus_timer.stop() - self.eu_focus_timer.start(60000) # 60 seconds - debug_logger.info("MAIN", "EU focus detection slowed to 60s interval") - - except Exception as e: - debug_logger.error("MAIN", f"Error in EU focus check: {e}") - - elapsed = debug_logger.end_timer("MAIN_check_eu_focus") - if elapsed > 100: # Log if taking more than 100ms - debug_logger.warn("MAIN", f"EU focus check took {elapsed:.2f}ms - SLOW!") - - # HARD DISABLE: If focus detection is slow, permanently disable it - if elapsed > 1000: # More than 1 second - this is killing the UI - debug_logger.error("MAIN", "FOCUS DETECTION TOO SLOW - DISABLING PERMANENTLY!") - self._focus_detection_disabled = True - if hasattr(self, 'eu_focus_timer'): - self.eu_focus_timer.stop() - debug_logger.info("MAIN", "EU focus detection DISABLED to prevent UI freezing") - def _load_overlay_widgets(self): """Load saved overlay widgets.""" widget_settings = self.settings.get('overlay_widgets', {}) @@ -594,9 +515,10 @@ class EUUtilityApp: """Quit the application.""" print("[Core] Shutting down...") - # Stop EU focus timer - if hasattr(self, 'eu_focus_timer'): - self.eu_focus_timer.stop() + # Stop overlay controller + if hasattr(self, 'overlay_controller') and self.overlay_controller: + print("[Core] Stopping Overlay Controller...") + self.overlay_controller.stop() # Stop log reader if hasattr(self, 'log_reader'): diff --git a/core/overlay_controller.py b/core/overlay_controller.py new file mode 100644 index 0000000..54329f4 --- /dev/null +++ b/core/overlay_controller.py @@ -0,0 +1,200 @@ +""" +EU-Utility - Activity Bar Overlay Modes +======================================== + +GW2/Blish-style overlay system with multiple visibility modes: + +Modes: +- DESKTOP_APP: Activity bar shows in desktop app only +- OVERLAY_ALWAYS: Activity bar always visible as overlay +- OVERLAY_GAME_FOCUSED: Only visible when EU window is focused +- OVERLAY_HOTKEY_TOGGLE: Toggle with hotkey (Ctrl+Shift+B) +- OVERLAY_TEMPORARY: Show for 7-10 seconds on hotkey, then hide +""" + +from enum import Enum +from dataclasses import dataclass +from typing import Optional +from PyQt6.QtCore import QTimer, pyqtSignal + + +class OverlayMode(Enum): + """Activity bar visibility modes.""" + DESKTOP_APP = "desktop_app" # Only in desktop app + OVERLAY_ALWAYS = "overlay_always" # Always visible overlay + OVERLAY_GAME_FOCUSED = "overlay_game" # Only when EU game focused + OVERLAY_HOTKEY_TOGGLE = "overlay_toggle" # Toggle with hotkey + OVERLAY_TEMPORARY = "overlay_temp" # Show 7-10s on hotkey + + +@dataclass +class OverlayConfig: + """Configuration for activity bar overlay behavior.""" + mode: OverlayMode = OverlayMode.OVERLAY_HOTKEY_TOGGLE + temporary_duration: int = 8000 # milliseconds (8 seconds default) + game_focus_poll_interval: int = 2000 # ms (2 seconds) + + def to_dict(self): + return { + 'mode': self.mode.value, + 'temporary_duration': self.temporary_duration, + 'game_focus_poll_interval': self.game_focus_poll_interval + } + + @classmethod + def from_dict(cls, data): + mode_str = data.get('mode', 'overlay_toggle') + try: + mode = OverlayMode(mode_str) + except ValueError: + mode = OverlayMode.OVERLAY_HOTKEY_TOGGLE + + return cls( + mode=mode, + temporary_duration=data.get('temporary_duration', 8000), + game_focus_poll_interval=data.get('game_focus_poll_interval', 2000) + ) + + +class OverlayController: + """ + Controls activity bar visibility based on selected mode. + + Usage: + controller = OverlayController(activity_bar, window_manager) + controller.set_mode(OverlayMode.OVERLAY_TEMPORARY) + controller.start() + """ + + visibility_changed = pyqtSignal(bool) # is_visible + + def __init__(self, activity_bar, window_manager=None): + self.activity_bar = activity_bar + self.window_manager = window_manager + self.config = OverlayConfig() + + # State + self._mode = OverlayMode.OVERLAY_HOTKEY_TOGGLE + self._is_visible = False + self._game_focused = False + + # Timers + self._game_focus_timer = QTimer() + self._game_focus_timer.timeout.connect(self._check_game_focus) + + self._temporary_timer = QTimer() + self._temporary_timer.setSingleShot(True) + self._temporary_timer.timeout.connect(self._hide_temporary) + + def set_mode(self, mode: OverlayMode): + """Change overlay mode.""" + self._mode = mode + self._apply_mode() + + def start(self): + """Start the overlay controller based on current mode.""" + self._apply_mode() + + def stop(self): + """Stop all timers and hide overlay.""" + self._game_focus_timer.stop() + self._temporary_timer.stop() + if self.activity_bar: + self.activity_bar.hide() + + def _apply_mode(self): + """Apply current mode settings.""" + self._game_focus_timer.stop() + self._temporary_timer.stop() + + if self._mode == OverlayMode.DESKTOP_APP: + # Only show in desktop app - hide overlay + self._hide() + + elif self._mode == OverlayMode.OVERLAY_ALWAYS: + # Always show + self._show() + + elif self._mode == OverlayMode.OVERLAY_GAME_FOCUSED: + # Show only when game focused + self._game_focus_timer.start(self.config.game_focus_poll_interval) + self._check_game_focus() # Check immediately + + elif self._mode == OverlayMode.OVERLAY_HOTKEY_TOGGLE: + # Hotkey toggle - start hidden + self._hide() + + elif self._mode == OverlayMode.OVERLAY_TEMPORARY: + # Temporary mode - start hidden + self._hide() + + def toggle(self): + """Toggle overlay visibility (for hotkey).""" + if self._mode == OverlayMode.OVERLAY_TEMPORARY: + self.show_temporary() + else: + if self._is_visible: + self._hide() + else: + self._show() + + def show_temporary(self): + """Show overlay temporarily (for temporary mode).""" + self._show() + self._temporary_timer.start(self.config.temporary_duration) + + def _hide_temporary(self): + """Hide after temporary duration.""" + self._hide() + + def _show(self): + """Show the activity bar.""" + if self.activity_bar and not self._is_visible: + self.activity_bar.show() + self._is_visible = True + self.visibility_changed.emit(True) + + def _hide(self): + """Hide the activity bar.""" + if self.activity_bar and self._is_visible: + self.activity_bar.hide() + self._is_visible = False + self.visibility_changed.emit(False) + + def _check_game_focus(self): + """Check if EU game window is focused.""" + if not self.window_manager: + return + + try: + # Quick check without blocking + eu_window = self.window_manager.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: + self._show() + else: + self._hide() + else: + # No EU window - hide if visible + if self._is_visible: + self._hide() + self._game_focused = False + except Exception: + # Silently ignore errors + pass + + +# Singleton instance +_overlay_controller = None + +def get_overlay_controller(activity_bar=None, window_manager=None): + """Get or create the overlay controller singleton.""" + global _overlay_controller + if _overlay_controller is None: + if activity_bar is None: + raise ValueError("activity_bar required for first initialization") + _overlay_controller = OverlayController(activity_bar, window_manager) + return _overlay_controller