diff --git a/core/main.py b/core/main.py index 26488b1..3f7f03e 100644 --- a/core/main.py +++ b/core/main.py @@ -33,7 +33,7 @@ except ImportError: from core.plugin_manager import PluginManager from core.perfect_ux import create_perfect_window -from core.floating_icon import FloatingIcon +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 @@ -118,20 +118,23 @@ class EUUtilityApp: self.plugin_manager.overlay = self.dashboard self.dashboard.show() - # Create floating icon - print("Creating floating icon...") - self.floating_icon = FloatingIcon() - self.floating_icon.clicked.connect(self._toggle_overlay) - self.floating_icon.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) + # Create Activity Bar (in-game overlay) - hidden by default print("Creating Activity Bar...") from core.activity_bar import get_activity_bar self.activity_bar = get_activity_bar(self.plugin_manager) if self.activity_bar and self.activity_bar.config.enabled: - print("[Core] Activity Bar 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") @@ -149,7 +152,6 @@ class EUUtilityApp: 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("Or double-click the floating icon") print(f"Loaded {len(self.plugin_manager.get_all_plugins())} plugins") # Show Event Bus stats @@ -316,11 +318,7 @@ class EUUtilityApp: def _on_activity_bar_hotkey(self): """Called when activity bar hotkey is pressed.""" - if self.activity_bar: - if self.activity_bar.isVisible(): - self.activity_bar.hide() - else: - self.activity_bar.show() + self._toggle_activity_bar() def _on_toggle_signal(self): """Handle toggle signal in main thread.""" @@ -365,6 +363,58 @@ class EUUtilityApp: self.dashboard.raise_() self.dashboard.activateWindow() + def _toggle_activity_bar(self): + """Toggle activity bar visibility.""" + if self.activity_bar: + if self.activity_bar.isVisible(): + self.activity_bar.hide() + self.tray_icon.set_activity_bar_checked(False) + else: + self.activity_bar.show() + self.tray_icon.set_activity_bar_checked(True) + + 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) + self.eu_focus_timer.timeout.connect(self._check_eu_focus) + self.eu_focus_timer.start(500) # Check every 500ms + 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.""" + if not self.activity_bar or not hasattr(self, 'window_manager'): + return + + if not self.window_manager.is_available(): + return + + try: + eu_window = self.window_manager.find_eu_window() + if eu_window: + is_focused = eu_window.is_focused() + + if is_focused != self._last_eu_focused: + self._last_eu_focused = is_focused + + if is_focused: + # EU just got focused - show activity bar + if not self.activity_bar.isVisible(): + self.activity_bar.show() + self.tray_icon.set_activity_bar_checked(True) + print("[Core] EU focused - Activity Bar shown") + else: + # EU lost focus - hide activity bar + if self.activity_bar.isVisible(): + self.activity_bar.hide() + self.tray_icon.set_activity_bar_checked(False) + print("[Core] EU unfocused - Activity Bar hidden") + 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', {}) @@ -389,6 +439,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 log reader if hasattr(self, 'log_reader'): self.log_reader.stop() diff --git a/core/tray_icon.py b/core/tray_icon.py new file mode 100644 index 0000000..b170d1a --- /dev/null +++ b/core/tray_icon.py @@ -0,0 +1,160 @@ +""" +EU-Utility - System Tray Icon +============================= + +System tray implementation with right-click menu. +Replaces the floating icon with a proper tray icon. +""" + +from PyQt6.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication +from PyQt6.QtCore import QTimer, pyqtSignal, QObject +from PyQt6.QtGui import QIcon, QColor, QPainter, QFont, QFontMetrics + +from core.logger import get_logger + +logger = get_logger(__name__) + + +class TrayIcon(QObject): + """ + System tray icon for EU-Utility. + + Features: + - Right-click context menu + - Show/hide dashboard + - Toggle activity bar + - Settings access + - Quit option + """ + + show_dashboard = pyqtSignal() + toggle_activity_bar = pyqtSignal() + open_settings = pyqtSignal() + quit_app = pyqtSignal() + + def __init__(self, app: QApplication, parent=None): + super().__init__(parent) + self.app = app + self.tray_icon = None + self.menu = None + + self._create_icon() + self._setup_menu() + self._setup_visibility_timer() + + def _create_icon(self): + """Create the tray icon with EU logo.""" + # Create a simple colored circle icon with "EU" text + pixmap = QIcon.fromTheme("applications-system") + + # If no system icon, create custom + if pixmap.isNull(): + from PyQt6.QtGui import QPixmap, QPainter, QColor, QFont + px = QPixmap(64, 64) + px.fill(QColor(255, 140, 66)) # Orange background + + painter = QPainter(px) + painter.setPen(QColor(255, 255, 255)) + font = QFont("Segoe UI", 24, QFont.Weight.Bold) + painter.setFont(font) + painter.drawText(px.rect(), Qt.AlignmentFlag.AlignCenter, "EU") + painter.end() + + pixmap = QIcon(px) + + self.tray_icon = QSystemTrayIcon(self) + self.tray_icon.setIcon(pixmap) + self.tray_icon.setToolTip("EU-Utility") + + # Show the icon + self.tray_icon.show() + + # Connect activation (double-click) + self.tray_icon.activated.connect(self._on_activated) + + def _setup_menu(self): + """Setup the right-click context menu.""" + self.menu = QMenu() + self.menu.setStyleSheet(""" + QMenu { + background: rgba(35, 35, 35, 0.98); + color: white; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 8px; + } + QMenu::item { + padding: 10px 24px; + border-radius: 6px; + } + QMenu::item:selected { + background: rgba(255, 140, 66, 0.3); + } + QMenu::separator { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 8px 16px; + } + """) + + # Dashboard action + self.dashboard_action = QAction("📊 Dashboard", self) + self.dashboard_action.triggered.connect(self.show_dashboard) + self.menu.addAction(self.dashboard_action) + + # Activity Bar toggle + self.activity_bar_action = QAction("🎮 Activity Bar", self) + self.activity_bar_action.setCheckable(True) + self.activity_bar_action.triggered.connect(self.toggle_activity_bar) + self.menu.addAction(self.activity_bar_action) + + self.menu.addSeparator() + + # Settings + self.settings_action = QAction("⚙️ Settings", self) + self.settings_action.triggered.connect(self.open_settings) + self.menu.addAction(self.settings_action) + + self.menu.addSeparator() + + # Quit + self.quit_action = QAction("❌ Quit", self) + self.quit_action.triggered.connect(self.quit_app) + self.menu.addAction(self.quit_action) + + # Set menu + self.tray_icon.setContextMenu(self.menu) + + def _setup_visibility_timer(self): + """Setup timer to update menu state.""" + self.update_timer = QTimer(self) + self.update_timer.timeout.connect(self._update_menu_state) + self.update_timer.start(1000) # Update every second + + def _update_menu_state(self): + """Update menu checkbox states.""" + # This will be connected to actual state + pass + + def _on_activated(self, reason): + """Handle tray icon activation.""" + if reason == QSystemTrayIcon.ActivationReason.DoubleClick: + self.show_dashboard.emit() + + def show_notification(self, title: str, message: str, duration: int = 3000): + """Show a system notification.""" + if self.tray_icon and self.tray_icon.supportsMessages(): + self.tray_icon.showMessage( + title, + message, + QSystemTrayIcon.MessageIcon.Information, + duration + ) + + def set_activity_bar_checked(self, checked: bool): + """Update activity bar menu item state.""" + self.activity_bar_action.setChecked(checked) + + def is_visible(self) -> bool: + """Check if tray icon is visible.""" + return self.tray_icon and self.tray_icon.isVisible()