""" EU-Utility - Widget API ================ The WidgetAPI provides a comprehensive interface for creating, managing, and interacting with overlay widgets. Widgets are floating, draggable UI components that appear over the game. They can display real-time data, controls, or mini-versions of plugins. Quick Start: ----------- ```python from core.api.widget_api import get_widget_api class MyPlugin(BasePlugin): def initialize(self): self.widget_api = get_widget_api() def create_widget(self): widget = self.widget_api.create_widget( name="my_widget", title="My Widget", size=(300, 200) ) widget.show() ``` Widget Types: ------------ - MiniWidget - Small info display - ControlWidget - Interactive controls - ChartWidget - Data visualization - AlertWidget - Notifications/overlays For full documentation, see: docs/WIDGET_API.md """ import json from pathlib import Path from typing import Optional, Dict, List, Callable, Any, Tuple, Union from dataclasses import dataclass, asdict from enum import Enum from datetime import datetime from core.logger import get_logger logger = get_logger(__name__) class WidgetType(Enum): """Types of overlay widgets.""" MINI = "mini" # Small info display CONTROL = "control" # Interactive controls CHART = "chart" # Data visualization ALERT = "alert" # Notification overlay CUSTOM = "custom" # Custom widget class WidgetAnchor(Enum): """Widget anchor positions.""" TOP_LEFT = "top_left" TOP_CENTER = "top_center" TOP_RIGHT = "top_right" CENTER_LEFT = "center_left" CENTER = "center" CENTER_RIGHT = "center_right" BOTTOM_LEFT = "bottom_left" BOTTOM_CENTER = "bottom_center" BOTTOM_RIGHT = "bottom_right" @dataclass class WidgetConfig: """Configuration for a widget.""" name: str title: str widget_type: WidgetType = WidgetType.MINI size: Tuple[int, int] = (300, 200) position: Tuple[int, int] = (100, 100) anchor: WidgetAnchor = WidgetAnchor.TOP_LEFT opacity: float = 0.95 always_on_top: bool = True locked: bool = False # If True, cannot be moved resizable: bool = True minimizable: bool = True closable: bool = True show_in_taskbar: bool = False snap_to_grid: bool = False grid_size: int = 10 def to_dict(self) -> Dict: """Convert to dictionary.""" return { 'name': self.name, 'title': self.title, 'widget_type': self.widget_type.value, 'size': self.size, 'position': self.position, 'anchor': self.anchor.value, 'opacity': self.opacity, 'always_on_top': self.always_on_top, 'locked': self.locked, 'resizable': self.resizable, 'minimizable': self.minimizable, 'closable': self.closable, 'show_in_taskbar': self.show_in_taskbar, 'snap_to_grid': self.snap_to_grid, 'grid_size': self.grid_size } @classmethod def from_dict(cls, data: Dict) -> 'WidgetConfig': """Create from dictionary.""" return cls( name=data.get('name', 'unnamed'), title=data.get('title', 'Widget'), widget_type=WidgetType(data.get('widget_type', 'mini')), size=tuple(data.get('size', [300, 200])), position=tuple(data.get('position', [100, 100])), anchor=WidgetAnchor(data.get('anchor', 'top_left')), opacity=data.get('opacity', 0.95), always_on_top=data.get('always_on_top', True), locked=data.get('locked', False), resizable=data.get('resizable', True), minimizable=data.get('minimizable', True), closable=data.get('closable', True), show_in_taskbar=data.get('show_in_taskbar', False), snap_to_grid=data.get('snap_to_grid', False), grid_size=data.get('grid_size', 10) ) class Widget: """ Widget instance - a floating overlay window. This is a wrapper around the actual QWidget that provides a clean API for plugin developers. Example: -------- ```python widget = widget_api.create_widget( name="loot_tracker", title="Loot Tracker", size=(400, 300) ) # Set content widget.set_content(my_widget_content) # Show widget widget.show() # Update position widget.move(500, 200) ``` """ def __init__(self, config: WidgetConfig, qt_widget=None): self.config = config self._qt_widget = qt_widget self._content = None self._callbacks: Dict[str, List[Callable]] = {} self._created_at = datetime.now() self._last_moved = None @property def name(self) -> str: """Widget name (unique identifier).""" return self.config.name @property def title(self) -> str: """Widget title (displayed in header).""" return self.config.title @property def visible(self) -> bool: """Whether widget is currently visible.""" if self._qt_widget: return self._qt_widget.isVisible() return False @property def position(self) -> Tuple[int, int]: """Current widget position (x, y).""" if self._qt_widget: pos = self._qt_widget.pos() return (pos.x(), pos.y()) return self.config.position @property def size(self) -> Tuple[int, int]: """Current widget size (width, height).""" if self._qt_widget: sz = self._qt_widget.size() return (sz.width(), sz.height()) return self.config.size # ===================================================================== # Widget Operations # ===================================================================== def show(self) -> None: """Show the widget.""" if self._qt_widget: self._qt_widget.show() logger.debug(f"[Widget] Showed: {self.name}") def hide(self) -> None: """Hide the widget.""" if self._qt_widget: self._qt_widget.hide() logger.debug(f"[Widget] Hid: {self.name}") def close(self) -> bool: """ Close and destroy the widget. Returns: True if closed successfully """ if self._qt_widget: self._trigger_callback('closing') self._qt_widget.close() self._qt_widget.deleteLater() self._qt_widget = None self._trigger_callback('closed') logger.debug(f"[Widget] Closed: {self.name}") return True return False def move(self, x: int, y: int) -> None: """ Move widget to position. Args: x: X coordinate y: Y coordinate """ if self._qt_widget: # Apply grid snapping if enabled if self.config.snap_to_grid: x = round(x / self.config.grid_size) * self.config.grid_size y = round(y / self.config.grid_size) * self.config.grid_size self._qt_widget.move(x, y) self.config.position = (x, y) self._last_moved = datetime.now() self._trigger_callback('moved', {'x': x, 'y': y}) def resize(self, width: int, height: int) -> None: """ Resize widget. Args: width: New width height: New height """ if self._qt_widget: self._qt_widget.resize(width, height) self.config.size = (width, height) self._trigger_callback('resized', {'width': width, 'height': height}) def set_opacity(self, opacity: float) -> None: """ Set widget opacity. Args: opacity: 0.0 to 1.0 """ opacity = max(0.0, min(1.0, opacity)) self.config.opacity = opacity if self._qt_widget: self._qt_widget.setWindowOpacity(opacity) def set_title(self, title: str) -> None: """Update widget title.""" self.config.title = title if self._qt_widget: self._qt_widget.setWindowTitle(title) def set_locked(self, locked: bool) -> None: """ Lock/unlock widget position. When locked, the widget cannot be moved by dragging. """ self.config.locked = locked if self._qt_widget: # Update internal flag setattr(self._qt_widget, '_locked', locked) def minimize(self) -> None: """Minimize widget.""" if self._qt_widget and self.config.minimizable: self._qt_widget.showMinimized() def maximize(self) -> None: """Maximize widget.""" if self._qt_widget: self._qt_widget.showMaximized() def restore(self) -> None: """Restore from minimized/maximized state.""" if self._qt_widget: self._qt_widget.showNormal() def raise_widget(self) -> None: """Bring widget to front.""" if self._qt_widget: self._qt_widget.raise_() self._qt_widget.activateWindow() def lower_widget(self) -> None: """Send widget to back.""" if self._qt_widget: self._qt_widget.lower() # ===================================================================== # Content Management # ===================================================================== def set_content(self, widget) -> None: """ Set the main content widget. Args: widget: QWidget to display inside this widget """ self._content = widget if self._qt_widget: # Assuming the qt_widget has a content area if hasattr(self._qt_widget, 'set_content'): self._qt_widget.set_content(widget) def get_content(self) -> Optional[Any]: """Get the current content widget.""" return self._content def update_content(self, data: Any) -> None: """ Update widget content with new data. This triggers the 'update' callback. Args: data: New data to display """ self._trigger_callback('update', data) def flash(self, duration_ms: int = 1000, color: str = "#ff8c42") -> None: """ Flash widget to draw attention. Args: duration_ms: Flash duration color: Flash color (hex) """ if self._qt_widget and hasattr(self._qt_widget, 'flash'): self._qt_widget.flash(duration_ms, color) # ===================================================================== # Event Handling # ===================================================================== def on(self, event: str, callback: Callable) -> None: """ Register event callback. Events: - 'moved': Widget was moved (data: {'x': int, 'y': int}) - 'resized': Widget was resized (data: {'width': int, 'height': int}) - 'closing': Widget is about to close - 'closed': Widget was closed - 'update': Content should update (data: new data) - 'focus': Widget gained focus - 'blur': Widget lost focus Args: event: Event name callback: Function to call """ if event not in self._callbacks: self._callbacks[event] = [] self._callbacks[event].append(callback) def off(self, event: str, callback: Callable = None) -> None: """ Unregister event callback. Args: event: Event name callback: Specific callback to remove (if None, removes all) """ if event in self._callbacks: if callback: self._callbacks[event] = [ cb for cb in self._callbacks[event] if cb != callback ] else: del self._callbacks[event] def _trigger_callback(self, event: str, data: Any = None) -> None: """Internal: Trigger event callbacks.""" if event in self._callbacks: for callback in self._callbacks[event]: try: if data is not None: callback(data) else: callback() except Exception as e: logger.error(f"[Widget] Callback error for {event}: {e}") # ===================================================================== # Persistence # ===================================================================== def save_state(self) -> Dict: """ Save widget state to dictionary. Returns: State dictionary for persistence """ return { 'config': self.config.to_dict(), 'position': self.position, 'size': self.size, 'visible': self.visible, 'created_at': self._created_at.isoformat(), 'last_moved': self._last_moved.isoformat() if self._last_moved else None } def load_state(self, state: Dict) -> None: """ Restore widget state from dictionary. Args: state: State dictionary from save_state() """ if 'config' in state: self.config = WidgetConfig.from_dict(state['config']) if 'position' in state: x, y = state['position'] self.move(x, y) if 'size' in state: width, height = state['size'] self.resize(width, height) if state.get('visible'): self.show() class WidgetAPI: """ WidgetAPI - API for widget management. Provides methods to create, manage, and interact with overlay widgets. Example: -------- ```python from core.api.widget_api import get_widget_api api = get_widget_api() # Create a widget widget = api.create_widget( name="loot_counter", title="Loot Counter", size=(250, 150) ) # Configure it widget.set_opacity(0.9) widget.move(100, 100) # Show it widget.show() # Later... api.hide_all_widgets() # Hide all api.show_widget("loot_counter") # Show specific one ``` """ _instance: Optional['WidgetAPI'] = None _widgets: Dict[str, Widget] = {} _widget_factory: Optional[Callable] = None _presets: Dict[str, WidgetConfig] = {} def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance # ===================================================================== # Widget Creation # ===================================================================== def create_widget(self, name: str, title: str = None, size: Tuple[int, int] = (300, 200), position: Tuple[int, int] = (100, 100), widget_type: WidgetType = WidgetType.MINI, **kwargs) -> Widget: """ Create a new overlay widget. Args: name: Unique widget identifier title: Display title (default: same as name) size: (width, height) tuple position: (x, y) tuple widget_type: Type of widget **kwargs: Additional config options Returns: Widget instance Raises: ValueError: If widget name already exists Example: >>> widget = api.create_widget( ... name="my_tracker", ... title="My Tracker", ... size=(400, 300), ... opacity=0.9 ... ) """ if name in self._widgets: raise ValueError(f"Widget '{name}' already exists") config = WidgetConfig( name=name, title=title or name.replace('_', ' ').title(), widget_type=widget_type, size=size, position=position, **{k: v for k, v in kwargs.items() if k in WidgetConfig.__dataclass_fields__} ) # Create actual Qt widget via factory qt_widget = None if self._widget_factory: qt_widget = self._widget_factory(config) widget = Widget(config, qt_widget) self._widgets[name] = widget logger.info(f"[WidgetAPI] Created widget: {name}") return widget def create_from_preset(self, preset_name: str, name: str = None) -> Optional[Widget]: """ Create widget from a preset configuration. Args: preset_name: Name of preset to use name: Override widget name (optional) Returns: Widget instance or None if preset not found Example: >>> widget = api.create_from_preset("loot_tracker", "my_loot") """ if preset_name not in self._presets: logger.error(f"[WidgetAPI] Preset not found: {preset_name}") return None preset = self._presets[preset_name] widget_name = name or f"{preset_name}_{len(self._widgets)}" config = WidgetConfig( name=widget_name, title=preset.title, widget_type=preset.widget_type, size=preset.size, position=preset.position, **{k: getattr(preset, k) for k in ['opacity', 'always_on_top', 'locked', 'resizable', 'minimizable', 'closable']} ) qt_widget = None if self._widget_factory: qt_widget = self._widget_factory(config) widget = Widget(config, qt_widget) self._widgets[widget_name] = widget logger.info(f"[WidgetAPI] Created widget from preset: {preset_name}") return widget def register_preset(self, name: str, config: WidgetConfig) -> None: """ Register a widget preset for reuse. Args: name: Preset name config: WidgetConfig to use as template """ self._presets[name] = config logger.debug(f"[WidgetAPI] Registered preset: {name}") # ===================================================================== # Widget Access # ===================================================================== def get_widget(self, name: str) -> Optional[Widget]: """ Get widget by name. Args: name: Widget name Returns: Widget instance or None if not found """ return self._widgets.get(name) def get_all_widgets(self) -> List[Widget]: """Get all widgets.""" return list(self._widgets.values()) def get_visible_widgets(self) -> List[Widget]: """Get all visible widgets.""" return [w for w in self._widgets.values() if w.visible] def widget_exists(self, name: str) -> bool: """Check if widget exists.""" return name in self._widgets # ===================================================================== # Widget Management # ===================================================================== def show_widget(self, name: str) -> bool: """ Show a specific widget. Args: name: Widget name Returns: True if successful """ widget = self._widgets.get(name) if widget: widget.show() return True return False def hide_widget(self, name: str) -> bool: """ Hide a specific widget. Args: name: Widget name Returns: True if successful """ widget = self._widgets.get(name) if widget: widget.hide() return True return False def close_widget(self, name: str) -> bool: """ Close and destroy a widget. Args: name: Widget name Returns: True if closed """ widget = self._widgets.get(name) if widget: widget.close() del self._widgets[name] return True return False def show_all_widgets(self) -> None: """Show all widgets.""" for widget in self._widgets.values(): widget.show() def hide_all_widgets(self) -> None: """Hide all widgets.""" for widget in self._widgets.values(): widget.hide() def close_all_widgets(self) -> None: """Close all widgets.""" for widget in list(self._widgets.values()): widget.close() self._widgets.clear() def minimize_all(self) -> None: """Minimize all widgets.""" for widget in self._widgets.values(): widget.minimize() def restore_all(self) -> None: """Restore all minimized widgets.""" for widget in self._widgets.values(): widget.restore() def set_all_opacity(self, opacity: float) -> None: """ Set opacity for all widgets. Args: opacity: 0.0 to 1.0 """ for widget in self._widgets.values(): widget.set_opacity(opacity) def lock_all(self) -> None: """Lock all widgets (prevent moving).""" for widget in self._widgets.values(): widget.set_locked(True) def unlock_all(self) -> None: """Unlock all widgets.""" for widget in self._widgets.values(): widget.set_locked(False) # ===================================================================== # Layout Helpers # ===================================================================== def arrange_widgets(self, layout: str = "grid", spacing: int = 10) -> None: """ Automatically arrange visible widgets. Args: layout: 'grid', 'horizontal', 'vertical', 'cascade' spacing: Space between widgets """ visible = self.get_visible_widgets() if not visible: return if layout == "grid": self._arrange_grid(visible, spacing) elif layout == "horizontal": self._arrange_horizontal(visible, spacing) elif layout == "vertical": self._arrange_vertical(visible, spacing) elif layout == "cascade": self._arrange_cascade(visible, spacing) def _arrange_grid(self, widgets: List[Widget], spacing: int) -> None: """Arrange widgets in a grid.""" import math cols = math.ceil(math.sqrt(len(widgets))) x, y = 100, 100 for i, widget in enumerate(widgets): col = i % cols row = i // cols widget_x = x + col * (300 + spacing) widget_y = y + row * (200 + spacing) widget.move(widget_x, widget_y) def _arrange_horizontal(self, widgets: List[Widget], spacing: int) -> None: """Arrange widgets horizontally.""" x, y = 100, 100 for widget in widgets: widget.move(x, y) x += widget.size[0] + spacing def _arrange_vertical(self, widgets: List[Widget], spacing: int) -> None: """Arrange widgets vertically.""" x, y = 100, 100 for widget in widgets: widget.move(x, y) y += widget.size[1] + spacing def _arrange_cascade(self, widgets: List[Widget], spacing: int) -> None: """Arrange widgets in cascade.""" x, y = 100, 100 for widget in widgets: widget.move(x, y) x += spacing y += spacing def snap_to_grid(self, grid_size: int = 10) -> None: """ Snap all widgets to grid. Args: grid_size: Grid size in pixels """ for widget in self._widgets.values(): x, y = widget.position x = round(x / grid_size) * grid_size y = round(y / grid_size) * grid_size widget.move(x, y) # ===================================================================== # Persistence # ===================================================================== def save_all_states(self, filepath: str = None) -> Dict: """ Save all widget states. Args: filepath: Optional file to save to Returns: State dictionary """ states = { name: widget.save_state() for name, widget in self._widgets.items() } if filepath: Path(filepath).parent.mkdir(parents=True, exist_ok=True) with open(filepath, 'w') as f: json.dump(states, f, indent=2) return states def load_all_states(self, data: Union[str, Dict]) -> None: """ Load widget states. Args: data: File path or state dictionary """ if isinstance(data, str): with open(data, 'r') as f: states = json.load(f) else: states = data for name, state in states.items(): if name in self._widgets: self._widgets[name].load_state(state) # ===================================================================== # Internal # ===================================================================== def _set_widget_factory(self, factory: Callable) -> None: """ Set the factory function for creating Qt widgets. Internal use only - called by core system. Args: factory: Function that takes WidgetConfig and returns QWidget """ self._widget_factory = factory # Global instance _widget_api_instance: Optional[WidgetAPI] = None def get_widget_api() -> WidgetAPI: """ Get the global WidgetAPI instance. Returns: WidgetAPI singleton Example: >>> from core.api.widget_api import get_widget_api >>> api = get_widget_api() >>> widget = api.create_widget("my_widget", "My Widget") """ global _widget_api_instance if _widget_api_instance is None: _widget_api_instance = WidgetAPI() return _widget_api_instance # Convenience exports __all__ = [ 'WidgetAPI', 'get_widget_api', 'Widget', 'WidgetConfig', 'WidgetType', 'WidgetAnchor' ]