feat: System Tray Icon + EU Focus Detection
1. System Tray Icon (replaces floating button): - Right-click menu with: Dashboard, Activity Bar, Settings, Quit - Orange 'EU' icon - Double-click to open dashboard - Notifications support 2. Removed Floating Icon: - No more floating button on desktop - All interaction through tray icon 3. EU Window Focus Detection: - Activity bar auto-shows when EU is focused - Activity bar auto-hides when EU loses focus - Checks every 500ms for focus changes - Tray icon checkbox reflects activity bar state New Files: - core/tray_icon.py: System tray implementation Modified: - core/main.py: Use tray instead of floating icon, add focus detection Usage: - Start EU-Utility: Tray icon appears in system tray - Open EU game: Activity bar appears automatically - Alt-tab away from EU: Activity bar hides - Right-click tray icon: Access settings, toggle bar, quit
This commit is contained in:
parent
2d8983ece3
commit
56a6a5c622
82
core/main.py
82
core/main.py
|
|
@ -33,7 +33,7 @@ except ImportError:
|
||||||
|
|
||||||
from core.plugin_manager import PluginManager
|
from core.plugin_manager import PluginManager
|
||||||
from core.perfect_ux import create_perfect_window
|
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.settings import get_settings
|
||||||
from core.overlay_widgets import OverlayManager
|
from core.overlay_widgets import OverlayManager
|
||||||
from core.api import get_api, get_widget_api, get_external_api
|
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.plugin_manager.overlay = self.dashboard
|
||||||
self.dashboard.show()
|
self.dashboard.show()
|
||||||
|
|
||||||
# Create floating icon
|
# Create system tray icon (replaces floating icon)
|
||||||
print("Creating floating icon...")
|
print("Creating system tray icon...")
|
||||||
self.floating_icon = FloatingIcon()
|
self.tray_icon = TrayIcon(self.app)
|
||||||
self.floating_icon.clicked.connect(self._toggle_overlay)
|
self.tray_icon.show_dashboard.connect(self._toggle_overlay)
|
||||||
self.floating_icon.show()
|
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...")
|
print("Creating Activity Bar...")
|
||||||
from core.activity_bar import get_activity_bar
|
from core.activity_bar import get_activity_bar
|
||||||
self.activity_bar = get_activity_bar(self.plugin_manager)
|
self.activity_bar = get_activity_bar(self.plugin_manager)
|
||||||
if self.activity_bar and self.activity_bar.config.enabled:
|
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
|
# Connect signals
|
||||||
self.activity_bar.widget_requested.connect(self._on_activity_bar_widget)
|
self.activity_bar.widget_requested.connect(self._on_activity_bar_widget)
|
||||||
|
# Start EU focus detection
|
||||||
|
self._start_eu_focus_detection()
|
||||||
else:
|
else:
|
||||||
print("[Core] Activity Bar disabled")
|
print("[Core] Activity Bar disabled")
|
||||||
|
|
||||||
|
|
@ -149,7 +152,6 @@ class EUUtilityApp:
|
||||||
print("Press Ctrl+Shift+U to toggle dashboard")
|
print("Press Ctrl+Shift+U to toggle dashboard")
|
||||||
print("Press Ctrl+Shift+H to hide all overlays")
|
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")
|
||||||
print("Or double-click the floating icon")
|
|
||||||
print(f"Loaded {len(self.plugin_manager.get_all_plugins())} plugins")
|
print(f"Loaded {len(self.plugin_manager.get_all_plugins())} plugins")
|
||||||
|
|
||||||
# Show Event Bus stats
|
# Show Event Bus stats
|
||||||
|
|
@ -316,11 +318,7 @@ class EUUtilityApp:
|
||||||
|
|
||||||
def _on_activity_bar_hotkey(self):
|
def _on_activity_bar_hotkey(self):
|
||||||
"""Called when activity bar hotkey is pressed."""
|
"""Called when activity bar hotkey is pressed."""
|
||||||
if self.activity_bar:
|
self._toggle_activity_bar()
|
||||||
if self.activity_bar.isVisible():
|
|
||||||
self.activity_bar.hide()
|
|
||||||
else:
|
|
||||||
self.activity_bar.show()
|
|
||||||
|
|
||||||
def _on_toggle_signal(self):
|
def _on_toggle_signal(self):
|
||||||
"""Handle toggle signal in main thread."""
|
"""Handle toggle signal in main thread."""
|
||||||
|
|
@ -365,6 +363,58 @@ class EUUtilityApp:
|
||||||
self.dashboard.raise_()
|
self.dashboard.raise_()
|
||||||
self.dashboard.activateWindow()
|
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):
|
def _load_overlay_widgets(self):
|
||||||
"""Load saved overlay widgets."""
|
"""Load saved overlay widgets."""
|
||||||
widget_settings = self.settings.get('overlay_widgets', {})
|
widget_settings = self.settings.get('overlay_widgets', {})
|
||||||
|
|
@ -389,6 +439,10 @@ class EUUtilityApp:
|
||||||
"""Quit the application."""
|
"""Quit the application."""
|
||||||
print("[Core] Shutting down...")
|
print("[Core] Shutting down...")
|
||||||
|
|
||||||
|
# Stop EU focus timer
|
||||||
|
if hasattr(self, 'eu_focus_timer'):
|
||||||
|
self.eu_focus_timer.stop()
|
||||||
|
|
||||||
# Stop log reader
|
# Stop log reader
|
||||||
if hasattr(self, 'log_reader'):
|
if hasattr(self, 'log_reader'):
|
||||||
self.log_reader.stop()
|
self.log_reader.stop()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue