EU-Utility/core/widgets/widget_gallery.py

556 lines
19 KiB
Python

"""
EU-Utility - Widget Gallery
Display available widgets, create widget instances, and manage widget configuration.
"""
from typing import Dict, List, Optional, Callable
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QScrollArea, QFrame, QGridLayout, QDialog, QComboBox,
QSpinBox, QFormLayout, QDialogButtonBox, QMessageBox,
QGraphicsDropShadowEffect, QSizePolicy
)
from PyQt6.QtCore import Qt, pyqtSignal, QSize
from PyQt6.QtGui import QColor
from core.icon_manager import get_icon_manager
from core.data.sqlite_store import get_sqlite_store
from core.widgets.dashboard_widgets import (
DashboardWidget, WIDGET_TYPES, create_widget,
SystemStatusWidget, QuickActionsWidget, RecentActivityWidget, PluginGridWidget
)
class WidgetGalleryItem(QFrame):
"""Widget gallery item showing available widget."""
add_clicked = pyqtSignal(str)
def __init__(self, widget_type: str, widget_class: type, parent=None):
super().__init__(parent)
self.widget_type = widget_type
self.widget_class = widget_class
self.icon_manager = get_icon_manager()
self._setup_ui()
def _setup_ui(self):
"""Setup gallery item UI."""
self.setFixedSize(200, 140)
self.setStyleSheet("""
WidgetGalleryItem {
background-color: rgba(35, 40, 55, 200);
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 12px;
}
WidgetGalleryItem:hover {
border: 1px solid #4a9eff;
background-color: rgba(45, 50, 70, 200);
}
""")
# Shadow
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(15)
shadow.setColor(QColor(0, 0, 0, 60))
shadow.setOffset(0, 3)
self.setGraphicsEffect(shadow)
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(8)
# Icon
icon_widget = QLabel()
try:
icon_name = getattr(self.widget_class, 'icon_name', 'box')
icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=32)
icon_widget.setPixmap(icon_pixmap)
except:
icon_widget.setText("📦")
icon_widget.setStyleSheet("font-size: 24px;")
icon_widget.setFixedSize(32, 32)
icon_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_widget, alignment=Qt.AlignmentFlag.AlignCenter)
# Name
name = QLabel(getattr(self.widget_class, 'name', 'Widget'))
name.setStyleSheet("color: white; font-size: 13px; font-weight: bold;")
name.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(name)
# Description
desc = QLabel(getattr(self.widget_class, 'description', '')[:50] + "...")
desc.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 10px;")
desc.setWordWrap(True)
desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(desc)
layout.addStretch()
# Add button
add_btn = QPushButton("+ Add to Dashboard")
add_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 11px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3a8eef;
}
""")
add_btn.clicked.connect(self._on_add_clicked)
layout.addWidget(add_btn)
def _on_add_clicked(self):
"""Handle add button click."""
self.add_clicked.emit(self.widget_type)
class WidgetConfigDialog(QDialog):
"""Dialog for configuring widget position and size."""
def __init__(self, widget_type: str, widget_class: type, parent=None):
super().__init__(parent)
self.widget_type = widget_type
self.widget_class = widget_class
self.config = {}
self.setWindowTitle(f"Configure {widget_class.name}")
self.setMinimumSize(300, 200)
self._setup_ui()
def _setup_ui(self):
"""Setup dialog UI."""
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Form
form = QFormLayout()
form.setSpacing(10)
# Position
self.row_spin = QSpinBox()
self.row_spin.setRange(0, 10)
self.row_spin.setValue(0)
form.addRow("Row:", self.row_spin)
self.col_spin = QSpinBox()
self.col_spin.setRange(0, 3)
self.col_spin.setValue(0)
form.addRow("Column:", self.col_spin)
# Size (if widget supports variable size)
size = getattr(self.widget_class, 'size', (1, 1))
self.width_spin = QSpinBox()
self.width_spin.setRange(1, 3)
self.width_spin.setValue(size[0])
form.addRow("Width (cols):", self.width_spin)
self.height_spin = QSpinBox()
self.height_spin.setRange(1, 3)
self.height_spin.setValue(size[1])
form.addRow("Height (rows):", self.height_spin)
layout.addLayout(form)
# Info label
info = QLabel(f"Default size: {size[0]}x{size[1]} cols x rows")
info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px; font-style: italic;")
layout.addWidget(info)
layout.addStretch()
# Buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_config(self) -> Dict:
"""Get widget configuration."""
return {
'widget_type': self.widget_type,
'position': {
'row': self.row_spin.value(),
'col': self.col_spin.value()
},
'size': {
'width': self.width_spin.value(),
'height': self.height_spin.value()
}
}
class WidgetGallery(QFrame):
"""Widget Gallery for browsing and adding widgets."""
widget_added = pyqtSignal(str, dict) # widget_type, config
def __init__(self, parent=None):
super().__init__(parent)
self.icon_manager = get_icon_manager()
self._setup_ui()
def _setup_ui(self):
"""Setup gallery UI."""
self.setStyleSheet("""
WidgetGallery {
background-color: rgba(25, 30, 40, 250);
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 12px;
}
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
# Header
header_layout = QHBoxLayout()
header = QLabel("🎨 Widget Gallery")
header.setStyleSheet("font-size: 20px; font-weight: bold; color: white;")
header_layout.addWidget(header)
header_layout.addStretch()
# Close button
close_btn = QPushButton("")
close_btn.setFixedSize(28, 28)
close_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255, 255, 255, 10);
color: rgba(255, 255, 255, 150);
border: none;
border-radius: 14px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgba(255, 71, 71, 150);
color: white;
}
""")
close_btn.clicked.connect(self.hide)
header_layout.addWidget(close_btn)
layout.addLayout(header_layout)
# Description
desc = QLabel("Click on a widget to add it to your dashboard.")
desc.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 12px;")
layout.addWidget(desc)
# Widget grid
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
grid_widget = QWidget()
grid_layout = QGridLayout(grid_widget)
grid_layout.setSpacing(15)
grid_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
# Add widget items
cols = 3
for i, (widget_type, widget_class) in enumerate(WIDGET_TYPES.items()):
item = WidgetGalleryItem(widget_type, widget_class)
item.add_clicked.connect(self._on_widget_add)
row = i // cols
col = i % cols
grid_layout.addWidget(item, row, col)
grid_layout.setColumnStretch(cols, 1)
grid_layout.setRowStretch((len(WIDGET_TYPES) // cols) + 1, 1)
scroll.setWidget(grid_widget)
layout.addWidget(scroll)
def _on_widget_add(self, widget_type: str):
"""Handle widget add request."""
widget_class = WIDGET_TYPES.get(widget_type)
if not widget_class:
return
# Show config dialog
dialog = WidgetConfigDialog(widget_type, widget_class, self)
if dialog.exec():
config = dialog.get_config()
self.widget_added.emit(widget_type, config)
# Log activity
store = get_sqlite_store()
store.log_activity('ui', 'widget_added', f"Type: {widget_type}")
class DashboardWidgetManager(QWidget):
"""Manager for dashboard widgets with drag-drop and configuration."""
widget_created = pyqtSignal(str, QWidget) # widget_id, widget
def __init__(self, plugin_manager=None, parent=None):
super().__init__(parent)
self.plugin_manager = plugin_manager
self.widgets: Dict[str, DashboardWidget] = {}
self.widget_configs: Dict[str, dict] = {}
self._setup_ui()
self._load_saved_widgets()
def _setup_ui(self):
"""Setup widget manager UI."""
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(15)
# Controls bar
controls_layout = QHBoxLayout()
self.gallery_btn = QPushButton("🎨 Widget Gallery")
self.gallery_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3a8eef;
}
""")
self.gallery_btn.clicked.connect(self._show_gallery)
controls_layout.addWidget(self.gallery_btn)
controls_layout.addStretch()
# Reset button
reset_btn = QPushButton("↺ Reset")
reset_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255, 255, 255, 10);
color: rgba(255, 255, 255, 150);
padding: 8px 16px;
border: none;
border-radius: 6px;
}
QPushButton:hover {
background-color: rgba(255, 71, 71, 100);
color: white;
}
""")
reset_btn.clicked.connect(self._reset_widgets)
controls_layout.addWidget(reset_btn)
layout.addLayout(controls_layout)
# Widget grid container
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("""
QScrollArea {
background: transparent;
border: none;
}
QScrollBar:vertical {
background: rgba(0, 0, 0, 50);
width: 8px;
border-radius: 4px;
}
QScrollBar::handle:vertical {
background: rgba(255, 255, 255, 30);
border-radius: 4px;
}
""")
self.grid_widget = QWidget()
self.grid_layout = QGridLayout(self.grid_widget)
self.grid_layout.setSpacing(15)
self.grid_layout.setContentsMargins(0, 0, 0, 0)
scroll.setWidget(self.grid_widget)
layout.addWidget(scroll)
# Widget gallery (hidden by default)
self.gallery = WidgetGallery(self)
self.gallery.hide()
self.gallery.widget_added.connect(self._add_widget_from_gallery)
layout.addWidget(self.gallery)
def _show_gallery(self):
"""Show the widget gallery."""
if self.gallery.isVisible():
self.gallery.hide()
self.gallery_btn.setText("🎨 Widget Gallery")
else:
self.gallery.show()
self.gallery_btn.setText("✕ Hide Gallery")
def _add_widget_from_gallery(self, widget_type: str, config: dict):
"""Add a widget from the gallery."""
widget_id = f"{widget_type}_{len(self.widgets)}"
self.add_widget(widget_type, widget_id, config)
# Save to database
self._save_widget_config(widget_id, widget_type, config)
def add_widget(self, widget_type: str, widget_id: str, config: dict = None) -> Optional[QWidget]:
"""Add a widget to the dashboard."""
# Create widget
if widget_type == 'plugin_grid':
widget = create_widget(widget_type, plugin_manager=self.plugin_manager, parent=self)
else:
widget = create_widget(widget_type, parent=self)
if not widget:
return None
# Store widget
self.widgets[widget_id] = widget
self.widget_configs[widget_id] = config or {}
# Get position and size from config
pos = config.get('position', {'row': 0, 'col': 0}) if config else {'row': 0, 'col': 0}
size = config.get('size', {'width': 1, 'height': 1}) if config else {'width': 1, 'height': 1}
# Add to grid
self.grid_layout.addWidget(
widget,
pos.get('row', 0),
pos.get('col', 0),
size.get('height', 1),
size.get('width', 1)
)
self.widget_created.emit(widget_id, widget)
return widget
def remove_widget(self, widget_id: str) -> bool:
"""Remove a widget from the dashboard."""
if widget_id not in self.widgets:
return False
widget = self.widgets[widget_id]
self.grid_layout.removeWidget(widget)
widget.deleteLater()
del self.widgets[widget_id]
del self.widget_configs[widget_id]
# Remove from database
store = get_sqlite_store()
store.delete_widget(widget_id)
return True
def _save_widget_config(self, widget_id: str, widget_type: str, config: dict):
"""Save widget configuration to database."""
store = get_sqlite_store()
pos = config.get('position', {'row': 0, 'col': 0})
size = config.get('size', {'width': 1, 'height': 1})
store.save_widget_config(
widget_id=widget_id,
widget_type=widget_type,
row=pos.get('row', 0),
col=pos.get('col', 0),
width=size.get('width', 1),
height=size.get('height', 1),
config=config
)
def _load_saved_widgets(self):
"""Load saved widget configurations."""
store = get_sqlite_store()
configs = store.load_widget_configs()
# Clear default grid first
while self.grid_layout.count():
item = self.grid_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
self.widgets.clear()
self.widget_configs.clear()
# Load saved widgets
for config in configs:
widget_type = config.get('widget_type')
widget_id = config.get('widget_id')
if widget_type and widget_id:
self.add_widget(widget_type, widget_id, config)
# Add default widgets if none loaded
if not self.widgets:
self._add_default_widgets()
def _add_default_widgets(self):
"""Add default widgets."""
defaults = [
('system_status', {'position': {'row': 0, 'col': 0}, 'size': {'width': 2, 'height': 1}}),
('quick_actions', {'position': {'row': 0, 'col': 2}, 'size': {'width': 2, 'height': 1}}),
('recent_activity', {'position': {'row': 1, 'col': 0}, 'size': {'width': 1, 'height': 2}}),
('plugin_grid', {'position': {'row': 1, 'col': 1}, 'size': {'width': 3, 'height': 2}}),
]
for i, (widget_type, config) in enumerate(defaults):
widget_id = f"{widget_type}_default_{i}"
self.add_widget(widget_type, widget_id, config)
self._save_widget_config(widget_id, widget_type, config)
def _reset_widgets(self):
"""Reset to default widgets."""
reply = QMessageBox.question(
self, "Reset Widgets",
"This will remove all custom widgets and reset to defaults.\n\nContinue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
# Clear all widgets
for widget_id in list(self.widgets.keys()):
self.remove_widget(widget_id)
# Add defaults
self._add_default_widgets()
# Log
store = get_sqlite_store()
store.log_activity('ui', 'widgets_reset')
def get_widget(self, widget_id: str) -> Optional[DashboardWidget]:
"""Get a widget by ID."""
return self.widgets.get(widget_id)
def get_all_widgets(self) -> Dict[str, DashboardWidget]:
"""Get all widgets."""
return self.widgets.copy()
def set_plugin_manager(self, plugin_manager):
"""Set the plugin manager."""
self.plugin_manager = plugin_manager
# Update plugin grid widgets
for widget in self.widgets.values():
if isinstance(widget, PluginGridWidget):
widget.set_plugin_manager(plugin_manager)