""" EU-Utility - Dashboard Widgets System Status, Quick Actions, Recent Activity, and Plugin Grid widgets. """ import os from datetime import datetime from typing import Dict, List, Callable, Optional try: import psutil HAS_PSUTIL = True except ImportError: HAS_PSUTIL = False psutil = None from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar, QFrame, QGridLayout, QSizePolicy, QScrollArea, QGraphicsDropShadowEffect ) from PyQt6.QtCore import Qt, QTimer, pyqtSignal from PyQt6.QtGui import QColor, QPixmap from core.icon_manager import get_icon_manager from core.eu_styles import get_color from core.data.sqlite_store import get_sqlite_store class DashboardWidget(QFrame): """Base class for dashboard widgets.""" name = "Widget" description = "Base widget" icon_name = "target" size = (1, 1) # Grid size (cols, rows) def __init__(self, parent=None): super().__init__(parent) self.icon_manager = get_icon_manager() self._setup_frame() self._setup_ui() def _setup_frame(self): """Setup widget frame styling.""" self.setFrameStyle(QFrame.Shape.NoFrame) self.setStyleSheet(""" DashboardWidget { background-color: rgba(30, 35, 45, 200); border: 1px solid rgba(100, 150, 200, 60); border-radius: 12px; } DashboardWidget:hover { border: 1px solid rgba(100, 180, 255, 100); } """) # Shadow effect shadow = QGraphicsDropShadowEffect() shadow.setBlurRadius(20) shadow.setColor(QColor(0, 0, 0, 80)) shadow.setOffset(0, 4) self.setGraphicsEffect(shadow) def _setup_ui(self): """Setup widget UI. Override in subclass.""" layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) header = QLabel(self.name) header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;") layout.addWidget(header) content = QLabel("Widget Content") content.setStyleSheet("color: rgba(255, 255, 255, 150);") layout.addWidget(content) layout.addStretch() class SystemStatusWidget(DashboardWidget): """System Status widget showing CPU, RAM, and service status.""" name = "System Status" description = "Monitor system resources and service status" icon_name = "activity" size = (2, 1) def __init__(self, parent=None): self.services = {} super().__init__(parent) # Update timer self.timer = QTimer(self) self.timer.timeout.connect(self._update_stats) self.timer.start(2000) # Update every 2 seconds self._update_stats() def _setup_ui(self): """Setup system status UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # Header header_layout = QHBoxLayout() icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(18, 18) header_layout.addWidget(icon_label) header = QLabel(self.name) header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;") header_layout.addWidget(header) header_layout.addStretch() self.status_indicator = QLabel("●") self.status_indicator.setStyleSheet("color: #4ecdc4; font-size: 12px;") header_layout.addWidget(self.status_indicator) layout.addLayout(header_layout) # Stats grid stats_layout = QGridLayout() stats_layout.setSpacing(10) # CPU cpu_label = QLabel("CPU") cpu_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px;") stats_layout.addWidget(cpu_label, 0, 0) self.cpu_bar = QProgressBar() self.cpu_bar.setRange(0, 100) self.cpu_bar.setValue(0) self.cpu_bar.setTextVisible(False) self.cpu_bar.setFixedHeight(6) self.cpu_bar.setStyleSheet(""" QProgressBar { background-color: rgba(255, 255, 255, 30); border-radius: 3px; } QProgressBar::chunk { background-color: #4ecdc4; border-radius: 3px; } """) stats_layout.addWidget(self.cpu_bar, 0, 1) self.cpu_value = QLabel("0%") self.cpu_value.setStyleSheet("color: #4ecdc4; font-size: 11px; font-weight: bold;") stats_layout.addWidget(self.cpu_value, 0, 2) # RAM ram_label = QLabel("RAM") ram_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px;") stats_layout.addWidget(ram_label, 1, 0) self.ram_bar = QProgressBar() self.ram_bar.setRange(0, 100) self.ram_bar.setValue(0) self.ram_bar.setTextVisible(False) self.ram_bar.setFixedHeight(6) self.ram_bar.setStyleSheet(""" QProgressBar { background-color: rgba(255, 255, 255, 30); border-radius: 3px; } QProgressBar::chunk { background-color: #ff8c42; border-radius: 3px; } """) stats_layout.addWidget(self.ram_bar, 1, 1) self.ram_value = QLabel("0%") self.ram_value.setStyleSheet("color: #ff8c42; font-size: 11px; font-weight: bold;") stats_layout.addWidget(self.ram_value, 1, 2) # Disk disk_label = QLabel("Disk") disk_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px;") stats_layout.addWidget(disk_label, 2, 0) self.disk_bar = QProgressBar() self.disk_bar.setRange(0, 100) self.disk_bar.setValue(0) self.disk_bar.setTextVisible(False) self.disk_bar.setFixedHeight(6) self.disk_bar.setStyleSheet(""" QProgressBar { background-color: rgba(255, 255, 255, 30); border-radius: 3px; } QProgressBar::chunk { background-color: #4a9eff; border-radius: 3px; } """) stats_layout.addWidget(self.disk_bar, 2, 1) self.disk_value = QLabel("0%") self.disk_value.setStyleSheet("color: #4a9eff; font-size: 11px; font-weight: bold;") stats_layout.addWidget(self.disk_value, 2, 2) stats_layout.setColumnStretch(1, 1) layout.addLayout(stats_layout) # Service status services_label = QLabel("Services") services_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px; margin-top: 5px;") layout.addWidget(services_label) self.services_layout = QHBoxLayout() self.services_layout.setSpacing(8) layout.addLayout(self.services_layout) layout.addStretch() def _update_stats(self): """Update system statistics.""" if not HAS_PSUTIL: # Fallback when psutil is not available self.cpu_value.setText("N/A") self.ram_value.setText("N/A") self.disk_value.setText("N/A") self._update_services() return try: # CPU cpu_percent = psutil.cpu_percent(interval=0.1) self.cpu_bar.setValue(int(cpu_percent)) self.cpu_value.setText(f"{cpu_percent:.1f}%") # RAM ram = psutil.virtual_memory() self.ram_bar.setValue(ram.percent) self.ram_value.setText(f"{ram.percent}%") # Disk disk = psutil.disk_usage('/') disk_percent = (disk.used / disk.total) * 100 self.disk_bar.setValue(int(disk_percent)) self.disk_value.setText(f"{disk_percent:.1f}%") # Update services self._update_services() except Exception as e: print(f"[SystemStatus] Error updating stats: {e}") def _update_services(self): """Update service status indicators.""" # Clear existing while self.services_layout.count(): item = self.services_layout.takeAt(0) if item.widget(): item.widget().deleteLater() # Core services services = [ ("Overlay", True), ("Plugins", True), ("Hotkeys", True), ("Data Store", True), ] for name, status in services: service_widget = QLabel(f"{'●' if status else '○'} {name}") color = "#4ecdc4" if status else "#ff4757" service_widget.setStyleSheet(f"color: {color}; font-size: 10px;") self.services_layout.addWidget(service_widget) self.services_layout.addStretch() def set_service(self, name: str, status: bool): """Set a service status.""" self.services[name] = status self._update_services() class QuickActionsWidget(DashboardWidget): """Quick Actions widget with functional buttons.""" name = "Quick Actions" description = "One-click access to common actions" icon_name = "zap" size = (2, 1) action_triggered = pyqtSignal(str) def __init__(self, parent=None): self.actions = [] super().__init__(parent) def _setup_ui(self): """Setup quick actions UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # Header header_layout = QHBoxLayout() icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(18, 18) header_layout.addWidget(icon_label) header = QLabel(self.name) header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;") header_layout.addWidget(header) header_layout.addStretch() layout.addLayout(header_layout) # Actions grid self.actions_grid = QGridLayout() self.actions_grid.setSpacing(8) layout.addLayout(self.actions_grid) layout.addStretch() # Default actions self.set_actions([ {'id': 'search', 'name': 'Search', 'icon': 'search'}, {'id': 'screenshot', 'name': 'Screenshot', 'icon': 'camera'}, {'id': 'settings', 'name': 'Settings', 'icon': 'settings'}, {'id': 'plugins', 'name': 'Plugins', 'icon': 'grid'}, ]) def set_actions(self, actions: List[Dict]): """Set the quick actions.""" self.actions = actions self._render_actions() def _render_actions(self): """Render action buttons.""" # Clear existing while self.actions_grid.count(): item = self.actions_grid.takeAt(0) if item.widget(): item.widget().deleteLater() # Add buttons cols = 4 for i, action in enumerate(self.actions): btn = QPushButton() btn.setFixedSize(48, 48) # Try to get icon icon_name = action.get('icon', 'circle') try: icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=24) btn.setIcon(QPixmap(icon_pixmap)) btn.setIconSize(Qt.QSize(24, 24)) except: btn.setText(action['name'][0]) btn.setStyleSheet(""" QPushButton { background-color: rgba(255, 255, 255, 10); border: 1px solid rgba(255, 255, 255, 20); border-radius: 10px; color: white; font-size: 16px; font-weight: bold; } QPushButton:hover { background-color: rgba(255, 140, 66, 150); border: 1px solid rgba(255, 140, 66, 200); } QPushButton:pressed { background-color: rgba(255, 140, 66, 200); } """) btn.setToolTip(action['name']) action_id = action.get('id', action['name']) btn.clicked.connect(lambda checked, aid=action_id: self._on_action_clicked(aid)) row = i // cols col = i % cols self.actions_grid.addWidget(btn, row, col) def _on_action_clicked(self, action_id: str): """Handle action button click.""" self.action_triggered.emit(action_id) # Log activity store = get_sqlite_store() store.log_activity('ui', 'quick_action', f"Action: {action_id}") class RecentActivityWidget(DashboardWidget): """Recent Activity widget showing real data feed.""" name = "Recent Activity" description = "Shows recent system and plugin activity" icon_name = "clock" size = (1, 2) def __init__(self, parent=None): super().__init__(parent) # Update timer self.timer = QTimer(self) self.timer.timeout.connect(self._refresh_activity) self.timer.start(5000) # Refresh every 5 seconds self._refresh_activity() def _setup_ui(self): """Setup recent activity UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # Header header_layout = QHBoxLayout() icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(18, 18) header_layout.addWidget(icon_label) header = QLabel(self.name) header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;") header_layout.addWidget(header) header_layout.addStretch() layout.addLayout(header_layout) # Activity list self.activity_container = QWidget() self.activity_layout = QVBoxLayout(self.activity_container) self.activity_layout.setSpacing(6) self.activity_layout.setContentsMargins(0, 0, 0, 0) self.activity_layout.setAlignment(Qt.AlignmentFlag.AlignTop) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setStyleSheet("background: transparent; border: none;") scroll.setWidget(self.activity_container) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) layout.addWidget(scroll) def _refresh_activity(self): """Refresh the activity list.""" # Clear existing while self.activity_layout.count(): item = self.activity_layout.takeAt(0) if item.widget(): item.widget().deleteLater() # Get recent activity from database store = get_sqlite_store() activities = store.get_recent_activity(limit=10) if not activities: # Show placeholder placeholder = QLabel("No recent activity") placeholder.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px; font-style: italic;") self.activity_layout.addWidget(placeholder) else: for activity in activities: item = self._create_activity_item(activity) self.activity_layout.addWidget(item) self.activity_layout.addStretch() def _create_activity_item(self, activity: Dict) -> QFrame: """Create an activity item widget.""" frame = QFrame() frame.setStyleSheet(""" QFrame { background-color: rgba(255, 255, 255, 5); border-radius: 6px; } QFrame:hover { background-color: rgba(255, 255, 255, 10); } """) layout = QHBoxLayout(frame) layout.setContentsMargins(8, 6, 8, 6) layout.setSpacing(8) # Icon based on category category_icons = { 'plugin': '🔌', 'ui': '🖱', 'system': '⚙️', 'error': '❌', 'success': '✅', } icon = category_icons.get(activity.get('category', ''), '•') icon_label = QLabel(icon) icon_label.setStyleSheet("font-size: 12px;") layout.addWidget(icon_label) # Action text action_text = activity.get('action', 'Unknown') action_label = QLabel(action_text) action_label.setStyleSheet("color: rgba(255, 255, 255, 180); font-size: 11px;") layout.addWidget(action_label, 1) # Timestamp timestamp = activity.get('timestamp', '') if timestamp: try: dt = datetime.fromisoformat(timestamp) time_str = dt.strftime("%H:%M") except: time_str = timestamp[:5] if len(timestamp) >= 5 else timestamp time_label = QLabel(time_str) time_label.setStyleSheet("color: rgba(255, 255, 255, 80); font-size: 10px;") layout.addWidget(time_label) return frame class PluginGridWidget(DashboardWidget): """Plugin Grid showing actual plugin cards.""" name = "Installed Plugins" description = "Grid of installed plugins with status" icon_name = "grid" size = (2, 2) plugin_clicked = pyqtSignal(str) def __init__(self, plugin_manager=None, parent=None): self.plugin_manager = plugin_manager super().__init__(parent) # Update timer self.timer = QTimer(self) self.timer.timeout.connect(self._refresh_plugins) self.timer.start(3000) # Refresh every 3 seconds self._refresh_plugins() def _setup_ui(self): """Setup plugin grid UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # Header with stats header_layout = QHBoxLayout() icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(18, 18) header_layout.addWidget(icon_label) header = QLabel(self.name) header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;") header_layout.addWidget(header) header_layout.addStretch() self.stats_label = QLabel("0 plugins") self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") header_layout.addWidget(self.stats_label) layout.addLayout(header_layout) # Plugin grid scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setStyleSheet("background: transparent; border: none;") scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.grid_widget = QWidget() self.grid_layout = QGridLayout(self.grid_widget) self.grid_layout.setSpacing(8) self.grid_layout.setContentsMargins(0, 0, 0, 0) scroll.setWidget(self.grid_widget) layout.addWidget(scroll) def _refresh_plugins(self): """Refresh the plugin grid.""" # Clear existing while self.grid_layout.count(): item = self.grid_layout.takeAt(0) if item.widget(): item.widget().deleteLater() if not self.plugin_manager: placeholder = QLabel("Plugin manager not available") placeholder.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") self.grid_layout.addWidget(placeholder, 0, 0) self.stats_label.setText("No plugins") return # Get plugins discovered = self.plugin_manager.get_all_discovered_plugins() loaded = self.plugin_manager.get_all_plugins() self.stats_label.setText(f"{len(loaded)}/{len(discovered)} enabled") if not discovered: placeholder = QLabel("No plugins installed") placeholder.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") self.grid_layout.addWidget(placeholder, 0, 0) return # Create plugin cards cols = 2 for i, (plugin_id, plugin_class) in enumerate(discovered.items()): card = self._create_plugin_card(plugin_id, plugin_class, plugin_id in loaded) row = i // cols col = i % cols self.grid_layout.addWidget(card, row, col) def _create_plugin_card(self, plugin_id: str, plugin_class, is_loaded: bool) -> QFrame: """Create a plugin card.""" card = QFrame() card.setStyleSheet(""" QFrame { background-color: rgba(255, 255, 255, 8); border: 1px solid rgba(255, 255, 255, 15); border-radius: 8px; } QFrame:hover { background-color: rgba(255, 255, 255, 12); border: 1px solid rgba(255, 255, 255, 25); } """) card.setFixedHeight(70) card.setCursor(Qt.CursorShape.PointingHandCursor) layout = QHBoxLayout(card) layout.setContentsMargins(10, 8, 10, 8) layout.setSpacing(10) # Icon icon = QLabel(getattr(plugin_class, 'icon', '📦')) icon.setStyleSheet("font-size: 20px;") layout.addWidget(icon) # Info info_layout = QVBoxLayout() info_layout.setSpacing(2) name = QLabel(plugin_class.name) name.setStyleSheet("color: white; font-size: 12px; font-weight: bold;") info_layout.addWidget(name) version = QLabel(f"v{plugin_class.version}") version.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;") info_layout.addWidget(version) layout.addLayout(info_layout, 1) # Status indicator status_color = "#4ecdc4" if is_loaded else "#ff8c42" status_text = "●" if is_loaded else "○" status = QLabel(status_text) status.setStyleSheet(f"color: {status_color}; font-size: 14px;") status.setToolTip("Enabled" if is_loaded else "Disabled") layout.addWidget(status) # Click handler card.mousePressEvent = lambda event, pid=plugin_id: self.plugin_clicked.emit(pid) return card def set_plugin_manager(self, plugin_manager): """Set the plugin manager.""" self.plugin_manager = plugin_manager self._refresh_plugins() # Widget factory WIDGET_TYPES = { 'system_status': SystemStatusWidget, 'quick_actions': QuickActionsWidget, 'recent_activity': RecentActivityWidget, 'plugin_grid': PluginGridWidget, } def create_widget(widget_type: str, **kwargs) -> Optional[DashboardWidget]: """Create a widget by type.""" widget_class = WIDGET_TYPES.get(widget_type) if widget_class: return widget_class(**kwargs) return None