379 lines
11 KiB
Python
379 lines
11 KiB
Python
"""
|
|
EU-Utility Premium - Widget System
|
|
===================================
|
|
|
|
Dashboard widget system for creating plugin UIs.
|
|
|
|
Example:
|
|
from premium.widgets import Widget, WidgetConfig
|
|
|
|
class MyWidget(Widget):
|
|
def __init__(self):
|
|
super().__init__(WidgetConfig(
|
|
name="My Widget",
|
|
icon="📊",
|
|
size=(300, 200)
|
|
))
|
|
|
|
def create_ui(self, parent):
|
|
# Create and return your widget
|
|
label = QLabel("Hello World", parent)
|
|
return label
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
|
|
|
|
# =============================================================================
|
|
# WIDGET CONFIG
|
|
# =============================================================================
|
|
|
|
class WidgetSize(Enum):
|
|
"""Standard widget sizes."""
|
|
SMALL = (150, 100)
|
|
MEDIUM = (300, 200)
|
|
LARGE = (450, 300)
|
|
WIDE = (600, 200)
|
|
TALL = (300, 400)
|
|
FULL = (600, 400)
|
|
|
|
|
|
@dataclass
|
|
class WidgetConfig:
|
|
"""Configuration for a widget.
|
|
|
|
Attributes:
|
|
name: Display name of the widget
|
|
icon: Emoji or icon name
|
|
size: Widget size tuple (width, height)
|
|
resizable: Whether widget can be resized
|
|
collapsible: Whether widget can be collapsed
|
|
refresh_interval: Auto-refresh interval in seconds (0 = disabled)
|
|
settings: Widget-specific settings
|
|
"""
|
|
name: str = "Widget"
|
|
icon: str = "📦"
|
|
size: Tuple[int, int] = (300, 200)
|
|
resizable: bool = True
|
|
collapsible: bool = True
|
|
refresh_interval: float = 0.0
|
|
settings: Dict[str, Any] = field(default_factory=dict)
|
|
category: str = "General"
|
|
description: str = ""
|
|
author: str = ""
|
|
version: str = "1.0.0"
|
|
|
|
|
|
# =============================================================================
|
|
# WIDGET BASE CLASS
|
|
# =============================================================================
|
|
|
|
class Widget(ABC):
|
|
"""Base class for dashboard widgets.
|
|
|
|
Widgets are the UI components that plugins can create.
|
|
They are displayed in the dashboard overlay.
|
|
|
|
Example:
|
|
class StatsWidget(Widget):
|
|
def __init__(self):
|
|
super().__init__(WidgetConfig(
|
|
name="Player Stats",
|
|
icon="👤",
|
|
size=WidgetSize.MEDIUM.value
|
|
))
|
|
|
|
def create_ui(self, parent):
|
|
self.widget = QWidget(parent)
|
|
layout = QVBoxLayout(self.widget)
|
|
|
|
self.label = QLabel("Loading...")
|
|
layout.addWidget(self.label)
|
|
|
|
return self.widget
|
|
|
|
def update(self, data):
|
|
self.label.setText(f"Level: {data['level']}")
|
|
"""
|
|
|
|
def __init__(self, config: WidgetConfig):
|
|
self.config = config
|
|
self._ui: Optional[Any] = None
|
|
self._visible = True
|
|
self._collapsed = False
|
|
self._data: Dict[str, Any] = {}
|
|
self._refresh_timer = None
|
|
self._callbacks: Dict[str, List[Callable]] = {
|
|
'update': [],
|
|
'resize': [],
|
|
'collapse': [],
|
|
'close': [],
|
|
}
|
|
|
|
# ========== Abstract Methods ==========
|
|
|
|
@abstractmethod
|
|
def create_ui(self, parent: Any) -> Any:
|
|
"""Create the widget UI.
|
|
|
|
Args:
|
|
parent: Parent widget (QWidget)
|
|
|
|
Returns:
|
|
The created widget
|
|
"""
|
|
pass
|
|
|
|
# ========== Lifecycle Methods ==========
|
|
|
|
def initialize(self, parent: Any) -> Any:
|
|
"""Initialize the widget with a parent."""
|
|
self._ui = self.create_ui(parent)
|
|
|
|
if self.config.refresh_interval > 0:
|
|
self._start_refresh_timer()
|
|
|
|
return self._ui
|
|
|
|
def destroy(self) -> None:
|
|
"""Destroy the widget and clean up resources."""
|
|
self._stop_refresh_timer()
|
|
self._emit('close')
|
|
|
|
if self._ui:
|
|
self._ui.deleteLater()
|
|
self._ui = None
|
|
|
|
# ========== Update Methods ==========
|
|
|
|
def update(self, data: Dict[str, Any]) -> None:
|
|
"""Update widget with new data.
|
|
|
|
Override this to update your widget's display.
|
|
"""
|
|
self._data.update(data)
|
|
self._emit('update', data)
|
|
self.on_update(data)
|
|
|
|
def on_update(self, data: Dict[str, Any]) -> None:
|
|
"""Called when widget receives new data.
|
|
|
|
Override this instead of update() for custom behavior.
|
|
"""
|
|
pass
|
|
|
|
def refresh(self) -> None:
|
|
"""Manually trigger a refresh."""
|
|
self.on_refresh()
|
|
|
|
def on_refresh(self) -> None:
|
|
"""Called when widget should refresh its display."""
|
|
pass
|
|
|
|
# ========== State Methods ==========
|
|
|
|
def show(self) -> None:
|
|
"""Show the widget."""
|
|
self._visible = True
|
|
if self._ui:
|
|
self._ui.show()
|
|
|
|
def hide(self) -> None:
|
|
"""Hide the widget."""
|
|
self._visible = False
|
|
if self._ui:
|
|
self._ui.hide()
|
|
|
|
def collapse(self) -> None:
|
|
"""Collapse the widget."""
|
|
self._collapsed = True
|
|
self._emit('collapse', True)
|
|
if self._ui:
|
|
self._ui.setMaximumHeight(40)
|
|
|
|
def expand(self) -> None:
|
|
"""Expand the widget."""
|
|
self._collapsed = False
|
|
self._emit('collapse', False)
|
|
if self._ui:
|
|
self._ui.setMaximumHeight(self.config.size[1])
|
|
|
|
def toggle_collapse(self) -> None:
|
|
"""Toggle collapsed state."""
|
|
if self._collapsed:
|
|
self.expand()
|
|
else:
|
|
self.collapse()
|
|
|
|
# ========== Property Methods ==========
|
|
|
|
@property
|
|
def visible(self) -> bool:
|
|
"""Whether widget is visible."""
|
|
return self._visible
|
|
|
|
@property
|
|
def collapsed(self) -> bool:
|
|
"""Whether widget is collapsed."""
|
|
return self._collapsed
|
|
|
|
@property
|
|
def data(self) -> Dict[str, Any]:
|
|
"""Current widget data."""
|
|
return self._data.copy()
|
|
|
|
@property
|
|
def ui(self) -> Optional[Any]:
|
|
"""The UI widget."""
|
|
return self._ui
|
|
|
|
# ========== Event Handling ==========
|
|
|
|
def on(self, event: str, callback: Callable) -> Callable:
|
|
"""Subscribe to a widget event."""
|
|
if event in self._callbacks:
|
|
self._callbacks[event].append(callback)
|
|
return callback
|
|
|
|
def _emit(self, event: str, *args, **kwargs) -> None:
|
|
"""Emit an event to subscribers."""
|
|
for callback in self._callbacks.get(event, []):
|
|
try:
|
|
callback(*args, **kwargs)
|
|
except Exception as e:
|
|
print(f"Error in widget callback: {e}")
|
|
|
|
# ========== Timer Methods ==========
|
|
|
|
def _start_refresh_timer(self) -> None:
|
|
"""Start auto-refresh timer."""
|
|
if self.config.refresh_interval <= 0:
|
|
return
|
|
|
|
try:
|
|
from PyQt6.QtCore import QTimer
|
|
self._refresh_timer = QTimer()
|
|
self._refresh_timer.timeout.connect(self.refresh)
|
|
self._refresh_timer.start(int(self.config.refresh_interval * 1000))
|
|
except ImportError:
|
|
pass
|
|
|
|
def _stop_refresh_timer(self) -> None:
|
|
"""Stop auto-refresh timer."""
|
|
if self._refresh_timer:
|
|
self._refresh_timer.stop()
|
|
self._refresh_timer = None
|
|
|
|
|
|
# =============================================================================
|
|
# DASHBOARD
|
|
# =============================================================================
|
|
|
|
class Dashboard:
|
|
"""Dashboard for managing widgets.
|
|
|
|
The dashboard is the container for all plugin widgets.
|
|
It manages layout, visibility, and widget lifecycle.
|
|
"""
|
|
|
|
def __init__(self, parent: Any = None):
|
|
self.parent = parent
|
|
self._widgets: Dict[str, Widget] = {}
|
|
self._layout: Optional[Any] = None
|
|
self._visible = False
|
|
self._position: Tuple[int, int] = (100, 100)
|
|
|
|
def initialize(self, parent: Any) -> Any:
|
|
"""Initialize the dashboard."""
|
|
try:
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QScrollArea
|
|
|
|
self.container = QWidget(parent)
|
|
self.container.setWindowTitle("EU-Utility Dashboard")
|
|
self.container.setGeometry(*self._position, 350, 600)
|
|
|
|
scroll = QScrollArea(self.container)
|
|
scroll.setWidgetResizable(True)
|
|
|
|
self.widget_container = QWidget()
|
|
self._layout = QVBoxLayout(self.widget_container)
|
|
self._layout.setSpacing(10)
|
|
self._layout.addStretch()
|
|
|
|
scroll.setWidget(self.widget_container)
|
|
|
|
layout = QVBoxLayout(self.container)
|
|
layout.addWidget(scroll)
|
|
|
|
return self.container
|
|
|
|
except ImportError:
|
|
return None
|
|
|
|
def add_widget(self, widget_id: str, widget: Widget) -> bool:
|
|
"""Add a widget to the dashboard."""
|
|
if widget_id in self._widgets:
|
|
return False
|
|
|
|
self._widgets[widget_id] = widget
|
|
|
|
if self._layout:
|
|
ui = widget.initialize(self.widget_container)
|
|
if ui:
|
|
# Insert before the stretch
|
|
self._layout.insertWidget(self._layout.count() - 1, ui)
|
|
|
|
return True
|
|
|
|
def remove_widget(self, widget_id: str) -> bool:
|
|
"""Remove a widget from the dashboard."""
|
|
widget = self._widgets.pop(widget_id, None)
|
|
if widget:
|
|
widget.destroy()
|
|
return True
|
|
return False
|
|
|
|
def get_widget(self, widget_id: str) -> Optional[Widget]:
|
|
"""Get a widget by ID."""
|
|
return self._widgets.get(widget_id)
|
|
|
|
def show(self) -> None:
|
|
"""Show the dashboard."""
|
|
self._visible = True
|
|
if hasattr(self, 'container') and self.container:
|
|
self.container.show()
|
|
|
|
def hide(self) -> None:
|
|
"""Hide the dashboard."""
|
|
self._visible = False
|
|
if hasattr(self, 'container') and self.container:
|
|
self.container.hide()
|
|
|
|
def toggle(self) -> None:
|
|
"""Toggle dashboard visibility."""
|
|
if self._visible:
|
|
self.hide()
|
|
else:
|
|
self.show()
|
|
|
|
def get_all_widgets(self) -> Dict[str, Widget]:
|
|
"""Get all widgets."""
|
|
return self._widgets.copy()
|
|
|
|
|
|
# =============================================================================
|
|
# EXPORTS
|
|
# =============================================================================
|
|
|
|
__all__ = [
|
|
'WidgetSize',
|
|
'WidgetConfig',
|
|
'Widget',
|
|
'Dashboard',
|
|
]
|