feat: Rainmeter-style Widget System - draggable, resizable overlay widgets for plugins
This commit is contained in:
parent
fe7b4c9d94
commit
b9611d6965
|
|
@ -0,0 +1,577 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Widget System (Core Framework Component)
|
||||||
|
|
||||||
|
Rainmeter-inspired overlay widgets for plugins.
|
||||||
|
Widgets can be positioned, styled, and configured by users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Callable, Any
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QPushButton, QFrame, QMenu, QInputDialog
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal, QObject
|
||||||
|
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetAnchor(Enum):
|
||||||
|
"""Widget positioning anchor."""
|
||||||
|
TOP_LEFT = "top_left"
|
||||||
|
TOP_RIGHT = "top_right"
|
||||||
|
BOTTOM_LEFT = "bottom_left"
|
||||||
|
BOTTOM_RIGHT = "bottom_right"
|
||||||
|
CENTER = "center"
|
||||||
|
FREE = "free" # User positioned
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WidgetConfig:
|
||||||
|
"""Widget configuration."""
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
width: int = 200
|
||||||
|
height: int = 100
|
||||||
|
anchor: WidgetAnchor = WidgetAnchor.TOP_RIGHT
|
||||||
|
opacity: float = 1.0
|
||||||
|
scale: float = 1.0
|
||||||
|
visible: bool = True
|
||||||
|
locked: bool = False # If True, can't be moved
|
||||||
|
style: Dict[str, Any] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.style is None:
|
||||||
|
self.style = {}
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWidget(QFrame):
|
||||||
|
"""Base class for all overlay widgets.
|
||||||
|
|
||||||
|
Similar to Rainmeter skins - draggable, resizable, stylable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
moved = pyqtSignal(int, int) # x, y
|
||||||
|
resized = pyqtSignal(int, int) # width, height
|
||||||
|
closed = pyqtSignal()
|
||||||
|
settings_requested = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, widget_id: str, name: str, config: Optional[WidgetConfig] = None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.widget_id = widget_id
|
||||||
|
self.name = name
|
||||||
|
self.config = config or WidgetConfig()
|
||||||
|
|
||||||
|
# Window flags for overlay
|
||||||
|
self.setWindowFlags(
|
||||||
|
Qt.WindowType.FramelessWindowHint |
|
||||||
|
Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.Tool |
|
||||||
|
Qt.WindowType.WindowDoesNotAcceptFocus
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transparent background
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||||
|
|
||||||
|
# Dragging state
|
||||||
|
self._dragging = False
|
||||||
|
self._drag_offset = QPoint()
|
||||||
|
self._resizing = False
|
||||||
|
self._resize_start = QPoint()
|
||||||
|
self._start_size = QSize()
|
||||||
|
|
||||||
|
# Setup UI
|
||||||
|
self._setup_base_ui()
|
||||||
|
self._apply_style()
|
||||||
|
self._apply_geometry()
|
||||||
|
|
||||||
|
# Context menu
|
||||||
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
|
self.customContextMenuRequested.connect(self._show_context_menu)
|
||||||
|
|
||||||
|
def _setup_base_ui(self):
|
||||||
|
"""Setup the base widget UI."""
|
||||||
|
# Main layout
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
self.main_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Header (draggable area)
|
||||||
|
self.header = QFrame()
|
||||||
|
self.header.setFixedHeight(24)
|
||||||
|
self.header.setCursor(Qt.CursorShape.OpenHandCursor)
|
||||||
|
self.header.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: rgba(255, 140, 66, 200);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
header_layout = QHBoxLayout(self.header)
|
||||||
|
header_layout.setContentsMargins(8, 2, 8, 2)
|
||||||
|
header_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
self.title_label = QLabel(self.name)
|
||||||
|
self.title_label.setStyleSheet("color: white; font-weight: bold; font-size: 11px;")
|
||||||
|
header_layout.addWidget(self.title_label)
|
||||||
|
|
||||||
|
header_layout.addStretch()
|
||||||
|
|
||||||
|
# Settings button
|
||||||
|
self.settings_btn = QPushButton("⚙")
|
||||||
|
self.settings_btn.setFixedSize(18, 18)
|
||||||
|
self.settings_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 50);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.settings_btn.clicked.connect(self.settings_requested.emit)
|
||||||
|
header_layout.addWidget(self.settings_btn)
|
||||||
|
|
||||||
|
# Close button
|
||||||
|
self.close_btn = QPushButton("×")
|
||||||
|
self.close_btn.setFixedSize(18, 18)
|
||||||
|
self.close_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(255, 71, 71, 200);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.close_btn.clicked.connect(self.hide)
|
||||||
|
header_layout.addWidget(self.close_btn)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.header)
|
||||||
|
|
||||||
|
# Content area (subclasses add widgets here)
|
||||||
|
self.content = QFrame()
|
||||||
|
self.content.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: rgba(35, 40, 55, 220);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.content_layout = QVBoxLayout(self.content)
|
||||||
|
self.content_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
self.main_layout.addWidget(self.content, 1)
|
||||||
|
|
||||||
|
# Resize handle
|
||||||
|
self.resize_handle = QLabel("⟲")
|
||||||
|
self.resize_handle.setFixedSize(16, 16)
|
||||||
|
self.resize_handle.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.resize_handle.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: rgba(255, 255, 255, 150);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QLabel:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(255, 255, 255, 30);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.resize_handle.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
||||||
|
|
||||||
|
handle_container = QHBoxLayout()
|
||||||
|
handle_container.addStretch()
|
||||||
|
handle_container.addWidget(self.resize_handle)
|
||||||
|
self.main_layout.addLayout(handle_container)
|
||||||
|
|
||||||
|
def _apply_style(self):
|
||||||
|
"""Apply widget styling."""
|
||||||
|
style = self.config.style or {}
|
||||||
|
|
||||||
|
# Background color
|
||||||
|
bg_color = style.get('background_color', 'rgba(35, 40, 55, 220)')
|
||||||
|
border_color = style.get('border_color', 'rgba(100, 110, 130, 100)')
|
||||||
|
border_width = style.get('border_width', 1)
|
||||||
|
border_radius = style.get('border_radius', 8)
|
||||||
|
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
BaseWidget {{
|
||||||
|
background-color: {bg_color};
|
||||||
|
border: {border_width}px solid {border_color};
|
||||||
|
border-radius: {border_radius}px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Opacity
|
||||||
|
self.setWindowOpacity(self.config.opacity)
|
||||||
|
|
||||||
|
def _apply_geometry(self):
|
||||||
|
"""Apply widget geometry."""
|
||||||
|
self.setGeometry(
|
||||||
|
self.config.x,
|
||||||
|
self.config.y,
|
||||||
|
int(self.config.width * self.config.scale),
|
||||||
|
int(self.config.height * self.config.scale)
|
||||||
|
)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QMouseEvent):
|
||||||
|
"""Handle mouse press for dragging."""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
# Check if clicking resize handle
|
||||||
|
if self.resize_handle.geometry().contains(event.pos()):
|
||||||
|
self._resizing = True
|
||||||
|
self._resize_start = event.globalPosition().toPoint()
|
||||||
|
self._start_size = self.size()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if clicking header
|
||||||
|
if self.header.geometry().contains(event.pos()) and not self.config.locked:
|
||||||
|
self._dragging = True
|
||||||
|
self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||||
|
self.header.setCursor(Qt.CursorShape.ClosedHandCursor)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event: QMouseEvent):
|
||||||
|
"""Handle mouse move for dragging/resizing."""
|
||||||
|
if self._dragging:
|
||||||
|
new_pos = event.globalPosition().toPoint() - self._drag_offset
|
||||||
|
self.move(new_pos)
|
||||||
|
self.config.x = new_pos.x()
|
||||||
|
self.config.y = new_pos.y()
|
||||||
|
self.moved.emit(self.config.x, self.config.y)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._resizing:
|
||||||
|
delta = event.globalPosition().toPoint() - self._resize_start
|
||||||
|
new_size = QSize(
|
||||||
|
max(100, self._start_size.width() + delta.x()),
|
||||||
|
max(50, self._start_size.height() + delta.y())
|
||||||
|
)
|
||||||
|
self.resize(new_size)
|
||||||
|
self.config.width = int(new_size.width() / self.config.scale)
|
||||||
|
self.config.height = int(new_size.height() / self.config.scale)
|
||||||
|
self.resized.emit(self.config.width, self.config.height)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QMouseEvent):
|
||||||
|
"""Handle mouse release."""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
if self._dragging:
|
||||||
|
self._dragging = False
|
||||||
|
self.header.setCursor(Qt.CursorShape.OpenHandCursor)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._resizing:
|
||||||
|
self._resizing = False
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
def _show_context_menu(self, pos):
|
||||||
|
"""Show context menu."""
|
||||||
|
menu = QMenu(self)
|
||||||
|
menu.setStyleSheet("""
|
||||||
|
QMenu {
|
||||||
|
background-color: #232837;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(100, 110, 130, 100);
|
||||||
|
}
|
||||||
|
QMenu::item:selected {
|
||||||
|
background-color: #4a9eff;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Opacity submenu
|
||||||
|
opacity_menu = menu.addMenu("Opacity")
|
||||||
|
for opacity in [0.25, 0.5, 0.75, 1.0]:
|
||||||
|
action = opacity_menu.addAction(f"{int(opacity * 100)}%")
|
||||||
|
action.triggered.connect(lambda checked, o=opacity: self.set_opacity(o))
|
||||||
|
|
||||||
|
# Scale submenu
|
||||||
|
scale_menu = menu.addMenu("Scale")
|
||||||
|
for scale in [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]:
|
||||||
|
action = scale_menu.addAction(f"{int(scale * 100)}%")
|
||||||
|
action.triggered.connect(lambda checked, s=scale: self.set_scale(s))
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Lock position
|
||||||
|
lock_action = menu.addAction("🔒 Lock Position" if not self.config.locked else "🔓 Unlock Position")
|
||||||
|
lock_action.triggered.connect(self.toggle_lock)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
settings_action = menu.addAction("⚙ Settings...")
|
||||||
|
settings_action.triggered.connect(self.settings_requested.emit)
|
||||||
|
|
||||||
|
menu.exec(self.mapToGlobal(pos))
|
||||||
|
|
||||||
|
def set_opacity(self, opacity: float):
|
||||||
|
"""Set widget opacity."""
|
||||||
|
self.config.opacity = opacity
|
||||||
|
self.setWindowOpacity(opacity)
|
||||||
|
|
||||||
|
def set_scale(self, scale: float):
|
||||||
|
"""Set widget scale."""
|
||||||
|
self.config.scale = scale
|
||||||
|
self._apply_geometry()
|
||||||
|
|
||||||
|
def toggle_lock(self):
|
||||||
|
"""Toggle position lock."""
|
||||||
|
self.config.locked = not self.config.locked
|
||||||
|
|
||||||
|
def save_config(self) -> dict:
|
||||||
|
"""Save widget configuration."""
|
||||||
|
return {
|
||||||
|
'widget_id': self.widget_id,
|
||||||
|
'name': self.name,
|
||||||
|
'config': asdict(self.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_config(cls, data: dict) -> 'BaseWidget':
|
||||||
|
"""Load widget from configuration."""
|
||||||
|
config = WidgetConfig(**data.get('config', {}))
|
||||||
|
return cls(data['widget_id'], data['name'], config)
|
||||||
|
|
||||||
|
|
||||||
|
class ClockWidget(BaseWidget):
|
||||||
|
"""Example clock widget - demonstrates the system."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__("clock", "Clock", parent=parent)
|
||||||
|
|
||||||
|
self.time_label = QLabel("--:--:--")
|
||||||
|
self.time_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.time_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(self.time_label)
|
||||||
|
|
||||||
|
self.date_label = QLabel("----/--/--")
|
||||||
|
self.date_label.setStyleSheet("color: rgba(255,255,255,150); font-size: 12px;")
|
||||||
|
self.date_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(self.date_label)
|
||||||
|
|
||||||
|
# Update timer
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self._update)
|
||||||
|
self.timer.start(1000) # Update every second
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
"""Update clock display."""
|
||||||
|
from datetime import datetime
|
||||||
|
now = datetime.now()
|
||||||
|
self.time_label.setText(now.strftime("%H:%M:%S"))
|
||||||
|
self.date_label.setText(now.strftime("%Y-%m-%d"))
|
||||||
|
|
||||||
|
|
||||||
|
class SystemMonitorWidget(BaseWidget):
|
||||||
|
"""Example system monitor widget."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__("system", "System Monitor", parent=parent)
|
||||||
|
|
||||||
|
self.cpu_label = QLabel("CPU: --%")
|
||||||
|
self.cpu_label.setStyleSheet("color: #4ecdc4; font-size: 14px;")
|
||||||
|
self.content_layout.addWidget(self.cpu_label)
|
||||||
|
|
||||||
|
self.ram_label = QLabel("RAM: --%")
|
||||||
|
self.ram_label.setStyleSheet("color: #ff8c42; font-size: 14px;")
|
||||||
|
self.content_layout.addWidget(self.ram_label)
|
||||||
|
|
||||||
|
# Update timer
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self._update)
|
||||||
|
self.timer.start(2000) # Update every 2 seconds
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
"""Update system stats."""
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
cpu = psutil.cpu_percent()
|
||||||
|
ram = psutil.virtual_memory().percent
|
||||||
|
|
||||||
|
self.cpu_label.setText(f"CPU: {cpu:.1f}%")
|
||||||
|
self.ram_label.setText(f"RAM: {ram:.1f}%")
|
||||||
|
except ImportError:
|
||||||
|
self.cpu_label.setText("CPU: N/A")
|
||||||
|
self.ram_label.setText("RAM: N/A")
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetManager(QObject):
|
||||||
|
"""Manager for overlay widgets.
|
||||||
|
|
||||||
|
Handles widget lifecycle, positioning, and persistence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
WIDGETS_FILE = Path("config/widgets.json")
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.widgets: Dict[str, BaseWidget] = {}
|
||||||
|
self.WIDGETS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Load saved widgets
|
||||||
|
self._load_widgets()
|
||||||
|
|
||||||
|
def register_widget_type(self, widget_id: str, widget_class: type, name: str):
|
||||||
|
"""Register a widget type that plugins can create.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget_id: Unique identifier for this widget type
|
||||||
|
widget_class: Class inheriting from BaseWidget
|
||||||
|
name: Human-readable name
|
||||||
|
"""
|
||||||
|
self._widget_types[widget_id] = {
|
||||||
|
'class': widget_class,
|
||||||
|
'name': name
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_widget(self, widget_id: str, instance_id: Optional[str] = None, config: Optional[WidgetConfig] = None) -> BaseWidget:
|
||||||
|
"""Create a new widget instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget_id: Type of widget to create
|
||||||
|
instance_id: Optional unique instance ID
|
||||||
|
config: Optional configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created widget
|
||||||
|
"""
|
||||||
|
if instance_id is None:
|
||||||
|
instance_id = f"{widget_id}_{len(self.widgets)}"
|
||||||
|
|
||||||
|
# Create widget (for now, just create built-in types)
|
||||||
|
if widget_id == "clock":
|
||||||
|
widget = ClockWidget()
|
||||||
|
elif widget_id == "system":
|
||||||
|
widget = SystemMonitorWidget()
|
||||||
|
else:
|
||||||
|
# For plugin-created widgets, they'd pass their own class
|
||||||
|
widget = BaseWidget(instance_id, widget_id, config)
|
||||||
|
|
||||||
|
widget.widget_id = instance_id
|
||||||
|
self.widgets[instance_id] = widget
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
widget.moved.connect(lambda x, y, wid=instance_id: self._on_widget_moved(wid, x, y))
|
||||||
|
widget.closed.connect(lambda wid=instance_id: self._on_widget_closed(wid))
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def show_widget(self, widget_id: str):
|
||||||
|
"""Show a widget by ID."""
|
||||||
|
if widget_id in self.widgets:
|
||||||
|
self.widgets[widget_id].show()
|
||||||
|
|
||||||
|
def hide_widget(self, widget_id: str):
|
||||||
|
"""Hide a widget by ID."""
|
||||||
|
if widget_id in self.widgets:
|
||||||
|
self.widgets[widget_id].hide()
|
||||||
|
|
||||||
|
def remove_widget(self, widget_id: str):
|
||||||
|
"""Remove and destroy a widget."""
|
||||||
|
if widget_id in self.widgets:
|
||||||
|
self.widgets[widget_id].close()
|
||||||
|
del self.widgets[widget_id]
|
||||||
|
self._save_widgets()
|
||||||
|
|
||||||
|
def _on_widget_moved(self, widget_id: str, x: int, y: int):
|
||||||
|
"""Handle widget moved."""
|
||||||
|
self._save_widgets()
|
||||||
|
|
||||||
|
def _on_widget_closed(self, widget_id: str):
|
||||||
|
"""Handle widget closed."""
|
||||||
|
if widget_id in self.widgets:
|
||||||
|
del self.widgets[widget_id]
|
||||||
|
self._save_widgets()
|
||||||
|
|
||||||
|
def _save_widgets(self):
|
||||||
|
"""Save widget configurations."""
|
||||||
|
data = {
|
||||||
|
'widgets': [
|
||||||
|
widget.save_config()
|
||||||
|
for widget in self.widgets.values()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(self.WIDGETS_FILE, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
def _load_widgets(self):
|
||||||
|
"""Load widget configurations."""
|
||||||
|
self._widget_types = {}
|
||||||
|
|
||||||
|
if not self.WIDGETS_FILE.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.WIDGETS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Recreate widgets (would need widget type registry for full implementation)
|
||||||
|
for widget_data in data.get('widgets', []):
|
||||||
|
# For now, skip loading - plugins would recreate their widgets
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WidgetManager] Failed to load widgets: {e}")
|
||||||
|
|
||||||
|
def get_all_widgets(self) -> List[BaseWidget]:
|
||||||
|
"""Get all managed widgets."""
|
||||||
|
return list(self.widgets.values())
|
||||||
|
|
||||||
|
def show_all(self):
|
||||||
|
"""Show all widgets."""
|
||||||
|
for widget in self.widgets.values():
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def hide_all(self):
|
||||||
|
"""Hide all widgets."""
|
||||||
|
for widget in self.widgets.values():
|
||||||
|
widget.hide()
|
||||||
|
|
||||||
|
|
||||||
|
# Global widget manager instance
|
||||||
|
_widget_manager: Optional[WidgetManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_widget_manager() -> WidgetManager:
|
||||||
|
"""Get the global widget manager instance."""
|
||||||
|
global _widget_manager
|
||||||
|
if _widget_manager is None:
|
||||||
|
_widget_manager = WidgetManager()
|
||||||
|
return _widget_manager
|
||||||
Loading…
Reference in New Issue