EU-Utility/core/widgets/dashboard_widgets.py

699 lines
24 KiB
Python

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