EU-Utility/core/main.py

518 lines
20 KiB
Python

"""
EU-Utility - Main Entry Point
Launch the overlay, floating icon, dashboard, and plugin system.
"""
import sys
import os
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
try:
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import Qt, QObject, pyqtSignal
PYQT_AVAILABLE = True
except ImportError:
PYQT_AVAILABLE = False
print("Error: PyQt6 is required.")
print("Install with: pip install PyQt6")
sys.exit(1)
try:
import keyboard
KEYBOARD_AVAILABLE = True
except ImportError:
KEYBOARD_AVAILABLE = False
print("Warning: 'keyboard' library not installed.")
print("Global hotkeys won't work. Install with: pip install keyboard")
from core.plugin_manager import PluginManager
from core.perfect_ux import create_perfect_window
from core.tray_icon import TrayIcon
from core.settings import get_settings
from core.overlay_widgets import OverlayManager
from core.api import get_api, get_widget_api, get_external_api
from core.log_reader import get_log_reader
from core.ocr_service import get_ocr_service
from core.screenshot import get_screenshot_service
from core.notifications import get_notification_manager, NotificationManager
from core.nexus_api import get_nexus_api
from core.http_client import get_http_client
from core.window_manager import get_window_manager
from core.event_bus import get_event_bus
from core.tasks import get_task_manager
from core.audio import get_audio_manager
from core.clipboard import get_clipboard_manager
from core.data_store import get_data_store
class HotkeyHandler(QObject):
"""Signal bridge for thread-safe hotkey handling."""
toggle_signal = pyqtSignal()
hide_overlays_signal = pyqtSignal()
class EUUtilityApp:
"""Main application controller."""
def __init__(self):
self.app = None
self.overlay = None
self.floating_icon = None
self.plugin_manager = None
self.hotkey_handler = None
self.settings = None
self.overlay_manager = None
self.api = None
self.notification_manager = None
def run(self):
"""Start the application."""
# Create Qt Application
self.app = QApplication(sys.argv)
self.app.setQuitOnLastWindowClosed(False)
# Enable high DPI scaling (Qt6 has this enabled by default)
# This block is kept for backwards compatibility with Qt5 if ever needed
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
# In Qt6, this attribute is deprecated and always enabled
# The check prevents warnings on Qt6 while maintaining Qt5 compatibility
try:
self.app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
except (AttributeError, TypeError):
pass # Qt6+ doesn't need this
# Initialize Plugin API
print("Initializing Plugin API...")
self.api = get_api()
self._setup_api_services()
# Initialize Event Bus
print("Initializing Event Bus...")
self.event_bus = get_event_bus()
self._print_event_bus_stats()
# Initialize Notification Manager
print("Initializing Notification Manager...")
self.notification_manager = get_notification_manager()
self.notification_manager.initialize(self.app)
self.api.register_notification_service(self.notification_manager)
print("[Core] Notification service registered")
# Load settings
self.settings = get_settings()
# Create hotkey handler (must be in main thread)
self.hotkey_handler = HotkeyHandler()
# Initialize plugin manager
print("Loading plugins...")
self.plugin_manager = PluginManager(None)
self.plugin_manager.load_all_plugins()
# Create overlay manager
self.overlay_manager = OverlayManager(self.app)
# Create perfect UX main window
print("Creating main window with perfect UX...")
self.dashboard = create_perfect_window(self.plugin_manager)
self.plugin_manager.overlay = self.dashboard
self.dashboard.show()
# Create system tray icon (replaces floating icon)
print("Creating system tray icon...")
self.tray_icon = TrayIcon(self.app)
self.tray_icon.show_dashboard.connect(self._toggle_overlay)
self.tray_icon.toggle_activity_bar.connect(self._toggle_activity_bar)
self.tray_icon.quit_app.connect(self.quit)
# Create Activity Bar (in-game overlay) - hidden by default
print("Creating Activity Bar...")
try:
from core.activity_bar import get_activity_bar
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")
else:
print("[Core] Activity Bar not available")
self.activity_bar = None
except Exception as e:
print(f"[Core] Failed to create Activity Bar: {e}")
self.activity_bar = None
# Connect hotkey signals
self.hotkey_handler.toggle_signal.connect(self._on_toggle_signal)
# Setup global hotkeys
self._setup_hotkeys()
# Load saved overlay widgets
self._load_overlay_widgets()
print("EU-Utility started!")
print("Dashboard window is open")
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(f"Loaded {len(self.plugin_manager.get_all_plugins())} plugins")
# Show Event Bus stats
self._print_event_bus_stats()
# Run
return self.app.exec()
def _setup_api_services(self):
"""Setup shared API services - Window, OCR and Log are core services."""
# Initialize and start Log Reader
print("[Core] Initializing Log Reader...")
self.log_reader = get_log_reader()
if self.log_reader.start():
print(f"[Core] Log Reader started - watching: {self.log_reader.log_path}")
else:
print("[Core] Log Reader not available - log file not found")
# Register Log service with API
self.api.register_log_service(self.log_reader.read_lines)
# Initialize Window Manager (Windows-only, gracefully handles Linux)
print("[Core] Initializing Window Manager...")
self.window_manager = get_window_manager()
if self.window_manager.is_available():
# Try to find EU window on startup
eu_window = self.window_manager.find_eu_window()
if eu_window:
print(f"[Core] Found EU window: {eu_window.title} ({eu_window.width}x{eu_window.height})")
else:
print("[Core] EU window not found - will retry when needed")
# Register Window service with API
self.api.register_window_service(self.window_manager)
else:
print("[Core] Window Manager not available (Windows only)")
# Screenshot Service - Initialize on startup (lightweight)
print("[Core] Initializing Screenshot Service...")
self.screenshot_service = get_screenshot_service()
if self.screenshot_service.is_available():
self.api.register_screenshot_service(self.screenshot_service)
backends = self.screenshot_service.get_available_backends()
print(f"[Core] Screenshot Service ready (backends: {', '.join(backends)})")
else:
print("[Core] Screenshot Service not available - install pillow or pyautogui")
# OCR Service - LAZY INITIALIZATION (don't init on startup)
# It will initialize on first use
print("[Core] OCR Service configured (lazy init)")
self.ocr_service = get_ocr_service()
# Register OCR service with API (lazy - will init on first call)
self.api.register_ocr_service(self._lazy_ocr_handler)
# Initialize Nexus API Service
print("[Core] Initializing Nexus API Service...")
self.nexus_api = get_nexus_api()
self.api.register_nexus_service(self.nexus_api)
# HTTP Client - Initialize on startup
print("[Core] Initializing HTTP Client...")
try:
self.http_client = get_http_client(
cache_dir="cache/http",
default_cache_ttl=3600,
rate_limit_delay=0.1, # Small delay between requests
max_retries=3,
backoff_factor=0.5,
respect_cache_control=True
)
self.api.register_http_service(self.http_client)
print("[Core] HTTP Client initialized with caching")
except Exception as e:
print(f"[Core] HTTP Client initialization failed: {e}")
# Initialize Audio Service
print("[Core] Initializing Audio Service...")
self.audio_manager = get_audio_manager()
if self.audio_manager.is_available():
self.api.register_audio_service(self.audio_manager)
backend = self.audio_manager.get_backend()
volume = int(self.audio_manager.get_volume() * 100)
print(f"[Core] Audio Service ready (backend: {backend}, volume: {volume}%)")
else:
print("[Core] Audio Service not available - no audio backend found")
# Initialize Task Manager
print("[Core] Initializing Task Manager...")
try:
self.task_manager = get_task_manager(max_workers=4)
self.task_manager.initialize()
self.api.register_task_service(self.task_manager)
print("[Core] Task Manager initialized with 4 workers")
except Exception as e:
print(f"[Core] Task Manager initialization failed: {e}")
# Initialize Clipboard Manager
print("[Core] Initializing Clipboard Manager...")
self.clipboard_manager = get_clipboard_manager()
if self.clipboard_manager.is_available():
self.api.register_clipboard_service(self.clipboard_manager)
print("[Core] Clipboard Service ready")
else:
print("[Core] Clipboard Service not available - install pyperclip")
# Initialize Data Store
print("[Core] Initializing Data Store...")
self.data_store = get_data_store()
self.api.register_data_service(self.data_store)
print("[Core] Data Store ready")
print("[Core] API services registered: Window, Screenshot, OCR (lazy), Log, Nexus, HTTP, Audio, Tasks, Clipboard, Data")
def _lazy_ocr_handler(self, region=None):
"""Lazy OCR handler - triggers init on first use."""
return self.ocr_service.recognize(region=region)
def _print_event_bus_stats(self):
"""Print Event Bus statistics on startup."""
if not hasattr(self, 'event_bus') or not self.event_bus:
return
stats = self.event_bus.get_stats()
print("\n" + "=" * 50)
print("📊 Event Bus Statistics")
print("=" * 50)
print(f" Total Events Published: {stats.get('total_published', 0)}")
print(f" Total Events Delivered: {stats.get('total_delivered', 0)}")
print(f" Active Subscriptions: {stats.get('active_subscriptions', 0)}")
print(f" Events Per Minute: {stats.get('events_per_minute', 0)}")
print(f" Avg Delivery Time: {stats.get('avg_delivery_ms', 0)} ms")
print(f" Errors: {stats.get('errors', 0)}")
top_types = stats.get('top_event_types', {})
if top_types:
print(f"\n Top Event Types:")
for event_type, count in list(top_types.items())[:5]:
print(f"{event_type}: {count}")
print("=" * 50 + "\n")
def _setup_hotkeys(self):
"""Setup global hotkeys."""
if KEYBOARD_AVAILABLE:
try:
# Toggle main overlay
keyboard.add_hotkey('ctrl+shift+u', self._on_hotkey_pressed)
# Hide all overlays
keyboard.add_hotkey('ctrl+shift+h', self._on_hide_overlays_pressed)
# Toggle activity bar
keyboard.add_hotkey('ctrl+shift+b', self._on_activity_bar_hotkey)
except Exception as e:
print(f"Failed to register hotkey: {e}")
def _on_hotkey_pressed(self):
"""Called when toggle hotkey is pressed."""
if self.hotkey_handler:
self.hotkey_handler.toggle_signal.emit()
def _on_hide_overlays_pressed(self):
"""Called when hide hotkey is pressed."""
if self.overlay_manager:
self.overlay_manager.hide_all()
def _on_activity_bar_hotkey(self):
"""Called when activity bar hotkey is pressed."""
self._toggle_activity_bar()
def _on_toggle_signal(self):
"""Handle toggle signal in main thread."""
self._toggle_overlay()
def _on_activity_bar_widget(self, plugin_id: str):
"""Handle activity bar widget request."""
print(f"[Main] Activity bar requested plugin: {plugin_id}")
# Get plugin class
all_plugins = self.plugin_manager.get_all_discovered_plugins()
if plugin_id not in all_plugins:
print(f"[Main] Plugin not found: {plugin_id}")
return
plugin_class = all_plugins[plugin_id]
# Check if plugin has a mini widget
if hasattr(plugin_class, 'get_mini_widget'):
try:
# Create mini widget
widget = plugin_class.get_mini_widget()
if widget:
widget.show()
print(f"[Main] Created mini widget for {plugin_class.name}")
except Exception as e:
print(f"[Main] Error creating mini widget: {e}")
else:
# No mini widget, try to show main dashboard
self._toggle_overlay()
# Switch to this plugin
if self.dashboard:
self.dashboard.show_plugin(plugin_id)
def _toggle_overlay(self):
"""Toggle dashboard visibility."""
if self.dashboard:
if self.dashboard.isVisible():
self.dashboard.hide()
else:
self.dashboard.show()
self.dashboard.raise_()
self.dashboard.activateWindow()
def _toggle_activity_bar(self):
"""Toggle activity bar visibility."""
if hasattr(self, 'activity_bar') and self.activity_bar:
try:
if self.activity_bar.isVisible():
self.activity_bar.hide()
if hasattr(self, 'tray_icon') and self.tray_icon:
self.tray_icon.set_activity_bar_checked(False)
else:
self.activity_bar.show()
if hasattr(self, 'tray_icon') and self.tray_icon:
self.tray_icon.set_activity_bar_checked(True)
except Exception as e:
print(f"[Main] Error toggling activity bar: {e}")
else:
print("[Main] Activity Bar not available")
def _start_eu_focus_detection(self):
"""Start timer to detect EU window focus and show/hide activity bar."""
from PyQt6.QtCore import QTimer
self.eu_focus_timer = QTimer(self.app) # Use app as parent, not self
self.eu_focus_timer.timeout.connect(self._check_eu_focus)
self.eu_focus_timer.start(2000) # Check every 2 seconds (was 500ms - too frequent)
self._last_eu_focused = False
print("[Core] EU focus detection started")
def _check_eu_focus(self):
"""Check if EU window is focused and show/hide activity bar."""
try:
if not hasattr(self, 'activity_bar') or not self.activity_bar:
return
if not hasattr(self, 'window_manager') or not self.window_manager:
return
if not self.window_manager.is_available():
return
eu_window = self.window_manager.find_eu_window()
if eu_window:
is_focused = eu_window.is_focused()
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:
self.activity_bar.show()
if hasattr(self, 'tray_icon') and self.tray_icon:
self.tray_icon.set_activity_bar_checked(True)
print("[Core] EU focused - Activity Bar shown")
except Exception as e:
print(f"[Core] Error showing activity bar: {e}")
else:
# EU lost focus - hide activity bar
if self.activity_bar.isVisible():
try:
self.activity_bar.hide()
if hasattr(self, 'tray_icon') and self.tray_icon:
self.tray_icon.set_activity_bar_checked(False)
print("[Core] EU unfocused - Activity Bar hidden")
except Exception as e:
print(f"[Core] Error hiding activity bar: {e}")
except Exception as e:
# Silently ignore errors (EU window might not exist)
pass
def _load_overlay_widgets(self):
"""Load saved overlay widgets."""
widget_settings = self.settings.get('overlay_widgets', {})
for widget_name, config in widget_settings.items():
if config.get('enabled', False):
try:
self.overlay_manager.create_widget(
widget_name.replace('_overlay', ''),
widget_name
)
except Exception as e:
print(f"Failed to load overlay widget {widget_name}: {e}")
def create_overlay_widget(self, widget_type, name):
"""Create an overlay widget."""
if self.overlay_manager:
return self.overlay_manager.create_widget(widget_type, name)
return None
def quit(self):
"""Quit the application."""
print("[Core] Shutting down...")
# Stop EU focus timer
if hasattr(self, 'eu_focus_timer'):
self.eu_focus_timer.stop()
# Stop log reader
if hasattr(self, 'log_reader'):
self.log_reader.stop()
# Close all notifications
if self.notification_manager:
self.notification_manager.close_all()
# Shutdown Event Bus
if hasattr(self, 'event_bus') and self.event_bus:
print("[Core] Shutting down Event Bus...")
self.event_bus.shutdown()
# Shutdown Audio
if hasattr(self, 'audio_manager') and self.audio_manager:
print("[Core] Shutting down Audio...")
self.audio_manager.shutdown()
# Shutdown Task Manager
if hasattr(self, 'task_manager') and self.task_manager:
print("[Core] Shutting down Task Manager...")
self.task_manager.shutdown(wait=True, timeout=30.0)
# Window manager has no persistent resources to clean up
if self.overlay_manager:
self.overlay_manager.hide_all()
if self.plugin_manager:
self.plugin_manager.shutdown_all()
self.app.quit()
def main():
"""Entry point."""
app = EUUtilityApp()
sys.exit(app.run())
if __name__ == "__main__":
main()