907 lines
27 KiB
Python
907 lines
27 KiB
Python
"""
|
|
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'
|
|
]
|