feat: Add system tray, settings window, and fix window detection for 'Entropia Universe Client'
This commit is contained in:
parent
b6d33127e4
commit
b2856639dd
|
|
@ -45,6 +45,10 @@ from premium.widgets.dashboard_widget import DashboardWidget
|
|||
from premium.eu_integration.game_client import GameClient
|
||||
from premium.eu_integration.log_parser import LogParser
|
||||
from premium.eu_integration.events import GameEvent, LootEvent, SkillEvent
|
||||
from premium.eu_integration.window_tracker import WindowTracker
|
||||
|
||||
# UI
|
||||
from premium.ui.tray import TrayIcon, SettingsWindow
|
||||
|
||||
|
||||
class EUUtilityApp:
|
||||
|
|
@ -60,16 +64,30 @@ class EUUtilityApp:
|
|||
|
||||
def __init__(self, config_path=None):
|
||||
self.config_path = config_path
|
||||
self.config = {}
|
||||
self.plugin_manager = None
|
||||
self.state_store = None
|
||||
self.event_bus = None
|
||||
self.game_client = None
|
||||
self.dashboard = None
|
||||
self.tray_icon = None
|
||||
self.settings_window = None
|
||||
self.qt_app = None
|
||||
self._initialized = False
|
||||
self._running = False
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize all subsystems."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
# Load config
|
||||
if self.config_path:
|
||||
try:
|
||||
with open(self.config_path, 'r') as f:
|
||||
self.config = json.load(f)
|
||||
except:
|
||||
self.config = {}
|
||||
|
||||
# Create event bus
|
||||
self.event_bus = EventBus()
|
||||
|
|
@ -95,18 +113,36 @@ class EUUtilityApp:
|
|||
)
|
||||
|
||||
# Create game client
|
||||
self.game_client = GameClient(event_bus=self.event_bus)
|
||||
self.game_client = GameClient(
|
||||
game_path=Path(self.config.get('game_path', '')) if self.config.get('game_path') else None,
|
||||
event_bus=self.event_bus
|
||||
)
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def run(self):
|
||||
"""Run the application."""
|
||||
"""Run the application with Qt GUI."""
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
import sys
|
||||
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
# Start event bus first (captures main event loop)
|
||||
# Create Qt Application
|
||||
self.qt_app = QApplication(sys.argv)
|
||||
self.qt_app.setQuitOnLastWindowClosed(False) # Keep running when window closed
|
||||
|
||||
# Start event bus
|
||||
self.event_bus.start()
|
||||
|
||||
# Create system tray icon
|
||||
self.tray_icon = TrayIcon()
|
||||
self.tray_icon.toggle_overlay.connect(self._toggle_overlay)
|
||||
self.tray_icon.show_settings.connect(self._show_settings)
|
||||
self.tray_icon.show_dashboard.connect(self._show_dashboard)
|
||||
self.tray_icon.quit_requested.connect(self._quit)
|
||||
self.tray_icon.show()
|
||||
|
||||
# Discover and load plugins
|
||||
self.plugin_manager.discover_all()
|
||||
self.plugin_manager.load_all(auto_activate=True)
|
||||
|
|
@ -114,12 +150,70 @@ class EUUtilityApp:
|
|||
# Start game client
|
||||
self.game_client.start()
|
||||
|
||||
# Run main loop
|
||||
self._running = True
|
||||
|
||||
# Update tray status periodically
|
||||
self._update_status_timer()
|
||||
|
||||
# Run Qt event loop
|
||||
try:
|
||||
self._main_loop()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(self.qt_app.exec())
|
||||
except:
|
||||
self.shutdown()
|
||||
|
||||
def _update_status_timer(self):
|
||||
"""Update tray status periodically."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
# Update status
|
||||
if self.game_client.is_connected:
|
||||
status = "Connected"
|
||||
else:
|
||||
status = "Waiting for game..."
|
||||
|
||||
if self.tray_icon:
|
||||
self.tray_icon.update_status(status)
|
||||
|
||||
# Schedule next update
|
||||
from PyQt6.QtCore import QTimer
|
||||
QTimer.singleShot(5000, self._update_status_timer)
|
||||
|
||||
def _toggle_overlay(self):
|
||||
"""Toggle overlay visibility."""
|
||||
# TODO: Implement overlay toggle
|
||||
if self.tray_icon:
|
||||
self.tray_icon.notify(
|
||||
"EU-Utility",
|
||||
"Overlay toggle not yet implemented",
|
||||
QSystemTrayIcon.MessageIcon.Warning
|
||||
)
|
||||
|
||||
def _show_settings(self):
|
||||
"""Show settings window."""
|
||||
if not self.settings_window:
|
||||
self.settings_window = SettingsWindow(self.config)
|
||||
self.settings_window.show()
|
||||
self.settings_window.raise_()
|
||||
|
||||
def _show_dashboard(self):
|
||||
"""Show dashboard window."""
|
||||
if self.tray_icon:
|
||||
self.tray_icon.notify(
|
||||
"EU-Utility",
|
||||
"Dashboard not yet implemented",
|
||||
QSystemTrayIcon.MessageIcon.Warning
|
||||
)
|
||||
|
||||
def _quit(self):
|
||||
"""Quit the application."""
|
||||
from PyQt6.QtWidgets import QSystemTrayIcon
|
||||
|
||||
self._running = False
|
||||
self.shutdown()
|
||||
if self.qt_app:
|
||||
self.qt_app.quit()
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the application gracefully."""
|
||||
if self.game_client:
|
||||
|
|
@ -131,8 +225,6 @@ class EUUtilityApp:
|
|||
|
||||
def _root_reducer(self, state, action):
|
||||
"""Root state reducer."""
|
||||
# Default reducer - just returns state
|
||||
# Plugins can register their own reducers
|
||||
return state or {}
|
||||
|
||||
def _get_initial_state(self):
|
||||
|
|
@ -151,12 +243,6 @@ class EUUtilityApp:
|
|||
'plugins': {},
|
||||
}
|
||||
|
||||
def _main_loop(self):
|
||||
"""Main application loop."""
|
||||
import time
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Version
|
||||
|
|
@ -175,4 +261,7 @@ __all__ = [
|
|||
'Widget', 'WidgetConfig', 'Dashboard',
|
||||
# EU Integration
|
||||
'GameClient', 'LogParser', 'GameEvent', 'LootEvent', 'SkillEvent',
|
||||
'WindowTracker',
|
||||
# UI
|
||||
'TrayIcon', 'SettingsWindow',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -87,11 +87,24 @@ class WindowTracker:
|
|||
|
||||
if self._process_id:
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
return pid == self._process_id
|
||||
else:
|
||||
# Check window title
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
return 'entropia' in title.lower()
|
||||
if pid == self._process_id:
|
||||
return True
|
||||
|
||||
# Check window title - supports multiple patterns
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
title_lower = title.lower()
|
||||
|
||||
# Match various EU window titles:
|
||||
# - "Entropia Universe Client (64 bit)"
|
||||
# - "Entropia Universe Client (64 bit) - Planet Calypso"
|
||||
# - "Entropia.exe"
|
||||
patterns = [
|
||||
'entropia universe client',
|
||||
'entropia.exe',
|
||||
'entropia universe'
|
||||
]
|
||||
|
||||
return any(pattern in title_lower for pattern in patterns)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -0,0 +1,291 @@
|
|||
"""
|
||||
System Tray Integration for EU-Utility
|
||||
=======================================
|
||||
|
||||
Provides system tray icon with menu for controlling the application.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Callable, Optional
|
||||
from PyQt6.QtWidgets import (
|
||||
QSystemTrayIcon, QMenu, QAction, QWidget, QApplication,
|
||||
QMessageBox, QInputDialog
|
||||
)
|
||||
from PyQt6.QtCore import pyqtSignal, QObject
|
||||
from PyQt6.QtGui import QIcon, QAction as QActionGui
|
||||
|
||||
|
||||
class TrayIcon(QObject):
|
||||
"""System tray icon with menu.
|
||||
|
||||
Signals:
|
||||
toggle_overlay: Emitted when overlay toggle is clicked
|
||||
show_settings: Emitted when settings is clicked
|
||||
show_dashboard: Emitted when dashboard is clicked
|
||||
quit: Emitted when quit is clicked
|
||||
"""
|
||||
|
||||
toggle_overlay = pyqtSignal()
|
||||
show_settings = pyqtSignal()
|
||||
show_dashboard = pyqtSignal()
|
||||
quit_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.tray_icon: Optional[QSystemTrayIcon] = None
|
||||
self.menu: Optional[QMenu] = None
|
||||
|
||||
self._setup_tray()
|
||||
|
||||
def _setup_tray(self):
|
||||
"""Setup the system tray icon."""
|
||||
# Create tray icon
|
||||
self.tray_icon = QSystemTrayIcon(self.parent())
|
||||
|
||||
# Try to use a custom icon, fallback to system icon
|
||||
icon = self._load_icon()
|
||||
self.tray_icon.setIcon(icon)
|
||||
self.tray_icon.setToolTip("EU-Utility - Entropia Universe Overlay")
|
||||
|
||||
# Create menu
|
||||
self.menu = QMenu()
|
||||
self._build_menu()
|
||||
|
||||
self.tray_icon.setContextMenu(self.menu)
|
||||
|
||||
# Handle left click
|
||||
self.tray_icon.activated.connect(self._on_activated)
|
||||
|
||||
def _load_icon(self) -> QIcon:
|
||||
"""Load tray icon."""
|
||||
from pathlib import Path
|
||||
|
||||
# Try custom icon first
|
||||
icon_paths = [
|
||||
Path(__file__).parent.parent.parent / "assets" / "icon.ico",
|
||||
Path(__file__).parent.parent.parent / "assets" / "icon.png",
|
||||
]
|
||||
|
||||
for path in icon_paths:
|
||||
if path.exists():
|
||||
return QIcon(str(path))
|
||||
|
||||
# Fallback to system icon
|
||||
return QIcon.fromTheme("applications-games")
|
||||
|
||||
def _build_menu(self):
|
||||
"""Build the context menu."""
|
||||
# Toggle Overlay
|
||||
toggle_action = QActionGui("Toggle Overlay (Ctrl+Shift+B)", self)
|
||||
toggle_action.triggered.connect(self.toggle_overlay.emit)
|
||||
self.menu.addAction(toggle_action)
|
||||
|
||||
self.menu.addSeparator()
|
||||
|
||||
# Dashboard
|
||||
dashboard_action = QActionGui("Open Dashboard", self)
|
||||
dashboard_action.triggered.connect(self.show_dashboard.emit)
|
||||
self.menu.addAction(dashboard_action)
|
||||
|
||||
# Settings
|
||||
settings_action = QActionGui("Settings...", self)
|
||||
settings_action.triggered.connect(self.show_settings.emit)
|
||||
self.menu.addAction(settings_action)
|
||||
|
||||
self.menu.addSeparator()
|
||||
|
||||
# Status
|
||||
self.status_action = QActionGui("Status: Running", self)
|
||||
self.status_action.setEnabled(False)
|
||||
self.menu.addAction(self.status_action)
|
||||
|
||||
self.menu.addSeparator()
|
||||
|
||||
# Quit
|
||||
quit_action = QActionGui("Quit", self)
|
||||
quit_action.triggered.connect(self.quit_requested.emit)
|
||||
self.menu.addAction(quit_action)
|
||||
|
||||
def _on_activated(self, reason: QSystemTrayIcon.ActivationReason):
|
||||
"""Handle tray icon activation."""
|
||||
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
||||
# Left click - toggle overlay
|
||||
self.toggle_overlay.emit()
|
||||
elif reason == QSystemTrayIcon.ActivationReason.Context:
|
||||
# Right click - show menu (handled automatically)
|
||||
pass
|
||||
|
||||
def show(self):
|
||||
"""Show the tray icon."""
|
||||
if self.tray_icon:
|
||||
self.tray_icon.show()
|
||||
self.tray_icon.showMessage(
|
||||
"EU-Utility",
|
||||
"Application is running in system tray.\n"
|
||||
"Press Ctrl+Shift+B to toggle overlay.",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
3000
|
||||
)
|
||||
|
||||
def hide(self):
|
||||
"""Hide the tray icon."""
|
||||
if self.tray_icon:
|
||||
self.tray_icon.hide()
|
||||
|
||||
def update_status(self, status: str):
|
||||
"""Update the status menu item."""
|
||||
if hasattr(self, 'status_action'):
|
||||
self.status_action.setText(f"Status: {status}")
|
||||
|
||||
def notify(self, title: str, message: str, icon=QSystemTrayIcon.MessageIcon.Information):
|
||||
"""Show a tray notification."""
|
||||
if self.tray_icon and self.tray_icon.isVisible():
|
||||
self.tray_icon.showMessage(title, message, icon, 3000)
|
||||
|
||||
|
||||
class SettingsWindow(QWidget):
|
||||
"""Desktop settings window."""
|
||||
|
||||
def __init__(self, config: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.config = config
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the settings UI."""
|
||||
from PyQt6.QtWidgets import (
|
||||
QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QCheckBox, QComboBox, QSpinBox,
|
||||
QGroupBox, QTabWidget, QFileDialog
|
||||
)
|
||||
|
||||
self.setWindowTitle("EU-Utility Settings")
|
||||
self.setMinimumSize(500, 400)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Tabs
|
||||
tabs = QTabWidget()
|
||||
layout.addWidget(tabs)
|
||||
|
||||
# General tab
|
||||
general_tab = QWidget()
|
||||
general_layout = QVBoxLayout(general_tab)
|
||||
|
||||
# Game Path
|
||||
path_group = QGroupBox("Game Path")
|
||||
path_layout = QHBoxLayout(path_group)
|
||||
|
||||
self.path_edit = QLineEdit(self.config.get('game_path', ''))
|
||||
path_layout.addWidget(self.path_edit)
|
||||
|
||||
browse_btn = QPushButton("Browse...")
|
||||
browse_btn.clicked.connect(self._browse_game_path)
|
||||
path_layout.addWidget(browse_btn)
|
||||
|
||||
general_layout.addWidget(path_group)
|
||||
|
||||
# Overlay Settings
|
||||
overlay_group = QGroupBox("Overlay Settings")
|
||||
overlay_layout = QVBoxLayout(overlay_group)
|
||||
|
||||
self.overlay_checkbox = QCheckBox("Enable Overlay")
|
||||
self.overlay_checkbox.setChecked(self.config.get('overlay_enabled', True))
|
||||
overlay_layout.addWidget(self.overlay_checkbox)
|
||||
|
||||
# Hotkey
|
||||
hotkey_layout = QHBoxLayout()
|
||||
hotkey_layout.addWidget(QLabel("Toggle Hotkey:"))
|
||||
self.hotkey_edit = QLineEdit(self.config.get('hotkey', 'ctrl+shift+b'))
|
||||
hotkey_layout.addWidget(self.hotkey_edit)
|
||||
overlay_layout.addLayout(hotkey_layout)
|
||||
|
||||
# Overlay Mode
|
||||
mode_layout = QHBoxLayout()
|
||||
mode_layout.addWidget(QLabel("Overlay Mode:"))
|
||||
self.mode_combo = QComboBox()
|
||||
self.mode_combo.addItems([
|
||||
'overlay_toggle',
|
||||
'overlay_game',
|
||||
'overlay_always',
|
||||
'overlay_temp'
|
||||
])
|
||||
self.mode_combo.setCurrentText(self.config.get('overlay_mode', 'overlay_toggle'))
|
||||
mode_layout.addWidget(self.mode_combo)
|
||||
overlay_layout.addLayout(mode_layout)
|
||||
|
||||
general_layout.addWidget(overlay_group)
|
||||
|
||||
# Theme Settings
|
||||
theme_group = QGroupBox("Appearance")
|
||||
theme_layout = QVBoxLayout(theme_group)
|
||||
|
||||
theme_layout.addWidget(QLabel("Theme:"))
|
||||
self.theme_combo = QComboBox()
|
||||
self.theme_combo.addItems(['dark', 'light'])
|
||||
self.theme_combo.setCurrentText(self.config.get('ui', {}).get('theme', 'dark'))
|
||||
theme_layout.addWidget(self.theme_combo)
|
||||
|
||||
general_layout.addWidget(theme_group)
|
||||
|
||||
general_layout.addStretch()
|
||||
|
||||
tabs.addTab(general_tab, "General")
|
||||
|
||||
# Plugins tab
|
||||
plugins_tab = QWidget()
|
||||
plugins_layout = QVBoxLayout(plugins_tab)
|
||||
plugins_layout.addWidget(QLabel("Plugin settings will appear here"))
|
||||
plugins_layout.addStretch()
|
||||
tabs.addTab(plugins_tab, "Plugins")
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
save_btn = QPushButton("Save")
|
||||
save_btn.clicked.connect(self._save_settings)
|
||||
button_layout.addWidget(save_btn)
|
||||
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self.close)
|
||||
button_layout.addWidget(cancel_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def _browse_game_path(self):
|
||||
"""Browse for game path."""
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
|
||||
path = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Select Entropia Universe Directory",
|
||||
self.path_edit.text() or "C:/"
|
||||
)
|
||||
|
||||
if path:
|
||||
self.path_edit.setText(path)
|
||||
|
||||
def _save_settings(self):
|
||||
"""Save settings to config file."""
|
||||
self.config['game_path'] = self.path_edit.text()
|
||||
self.config['overlay_enabled'] = self.overlay_checkbox.isChecked()
|
||||
self.config['hotkey'] = self.hotkey_edit.text()
|
||||
self.config['overlay_mode'] = self.mode_combo.currentText()
|
||||
|
||||
if 'ui' not in self.config:
|
||||
self.config['ui'] = {}
|
||||
self.config['ui']['theme'] = self.theme_combo.currentText()
|
||||
|
||||
# Save to file
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path.home() / '.eu-utility' / 'config.json'
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.config, f, indent=2)
|
||||
|
||||
self.close()
|
||||
Loading…
Reference in New Issue