""" 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)