""" EU-Utility - Dashboard Widget System Customizable dashboard with draggable widgets from plugins. Each plugin can provide widgets for the dashboard. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QFrame, QScrollArea, QSizePolicy, QGraphicsDropShadowEffect ) from PyQt6.QtCore import Qt, pyqtSignal, QMimeData from PyQt6.QtGui import QColor, QDrag, QPainter, QPixmap, QIcon from PyQt6.QtCore import QSize from core.icon_manager import get_icon_manager from core.eu_styles import get_color, EU_TYPOGRAPHY, EU_SIZES class DashboardWidget(QFrame): """Base class for dashboard widgets.""" # Widget metadata name = "Widget" description = "Base widget" icon_name = "target" size = (1, 1) # Grid size (cols, rows) def __init__(self, plugin=None, parent=None): super().__init__(parent) self.plugin = plugin self.dragging = False self.icon_manager = get_icon_manager() 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) self._setup_ui() def _setup_ui(self): """Setup widget UI. Override in subclass.""" layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) header_layout = QHBoxLayout() # Icon icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=16) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(16, 16) header_layout.addWidget(icon_label) # Title header = QLabel(self.name) header.setStyleSheet(""" color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold; """) header_layout.addWidget(header) header_layout.addStretch() layout.addLayout(header_layout) content = QLabel("Widget Content") content.setStyleSheet("color: rgba(255, 255, 255, 150);") layout.addWidget(content) layout.addStretch() def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.dragging = True self.drag_start_pos = event.pos() def mouseMoveEvent(self, event): if self.dragging and (event.pos() - self.drag_start_pos).manhattanLength() > 10: self.dragging = False # Emit signal to parent to handle reordering if self.parent(): self.parent().start_widget_drag(self) def mouseReleaseEvent(self, event): self.dragging = False def update_data(self, data): """Update widget with new data. Override in subclass.""" pass class SpotifyWidget(DashboardWidget): """Spotify now playing widget.""" name = "Now Playing" description = "Shows current Spotify track" icon_name = "music" size = (2, 1) def __init__(self, plugin=None, parent=None): self.track_info = None super().__init__(plugin, parent) def _setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) layout.setSpacing(8) # Track name self.track_label = QLabel("Not Playing") self.track_label.setStyleSheet(""" color: white; font-size: 13px; font-weight: bold; """) self.track_label.setWordWrap(True) layout.addWidget(self.track_label) # Artist self.artist_label = QLabel("-") self.artist_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 11px;") layout.addWidget(self.artist_label) # Progress bar self.progress = QLabel("▬▬▬▬▬▬▬▬▬▬") self.progress.setStyleSheet("color: #1db954; font-size: 10px;") layout.addWidget(self.progress) def update_data(self, track_data): """Update with track info.""" if track_data: self.track_label.setText(track_data.get('name', 'Unknown')) self.track_label.setStyleSheet(""" color: #1db954; font-size: 13px; font-weight: bold; """) artists = track_data.get('artists', []) self.artist_label.setText(', '.join(artists) if artists else '-') class SkillProgressWidget(DashboardWidget): """Skill progress tracker widget.""" name = "Skill Progress" description = "Track skill gains" icon_name = "bar-chart" size = (1, 1) def _setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) layout.setSpacing(6) header_layout = QHBoxLayout() # Icon icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=16) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(16, 16) header_layout.addWidget(icon_label) header = QLabel("Recent Gains") header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;") header_layout.addWidget(header) header_layout.addStretch() layout.addLayout(header_layout) self.gains_list = QLabel("No gains yet") self.gains_list.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 10px;") self.gains_list.setWordWrap(True) layout.addWidget(self.gains_list) layout.addStretch() def update_data(self, gains): """Update with recent skill gains.""" if gains: text = "\n".join([f"+{g['points']} {g['skill']}" for g in gains[:3]]) self.gains_list.setText(text) class PEDTrackerWidget(DashboardWidget): """PED balance tracker widget.""" name = "PED Balance" description = "Track PED from inventory scans" icon_name = "ped" size = (1, 1) def _setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) layout.setSpacing(6) header_layout = QHBoxLayout() # Icon icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=16) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(16, 16) header_layout.addWidget(icon_label) header = QLabel("PED Balance") header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;") header_layout.addWidget(header) header_layout.addStretch() layout.addLayout(header_layout) self.balance_label = QLabel("0.00 PED") self.balance_label.setStyleSheet(""" color: #ffc107; font-size: 18px; font-weight: bold; """) layout.addWidget(self.balance_label) self.change_label = QLabel("Scan inventory to update") self.change_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;") layout.addWidget(self.change_label) layout.addStretch() def update_data(self, balance_data): """Update with balance info.""" if balance_data: self.balance_label.setText(f"{balance_data['ped']:.2f} PED") change = balance_data.get('change', 0) if change != 0: color = "#4caf50" if change > 0 else "#f44336" sign = "+" if change > 0 else "" self.change_label.setText(f"{sign}{change:.2f}") class QuickActionsWidget(DashboardWidget): """Quick action buttons widget.""" name = "Quick Actions" description = "One-click actions" icon_name = "zap" size = (2, 1) def __init__(self, actions=None, parent=None): self.actions = actions or [] super().__init__(None, parent) def _setup_ui(self): layout = QHBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) layout.setSpacing(8) for action in self.actions: btn = QPushButton() # Use icon if name matches an icon, otherwise use text icon_name = action.get('icon', 'target') icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=18) btn.setIcon(QIcon(icon_pixmap)) btn.setIconSize(QSize(18, 18)) btn.setFixedSize(36, 36) btn.setStyleSheet(""" QPushButton { background-color: rgba(255, 255, 255, 15); border: 1px solid rgba(255, 255, 255, 30); border-radius: 8px; } QPushButton:hover { background-color: rgba(255, 255, 255, 30); } """) btn.setToolTip(action['name']) btn.clicked.connect(action['callback']) layout.addWidget(btn) layout.addStretch() class Dashboard(QWidget): """Main dashboard with customizable widgets.""" widget_added = pyqtSignal(object) widget_removed = pyqtSignal(object) def __init__(self, plugin_manager=None, parent=None): super().__init__(parent) self.plugin_manager = plugin_manager self.widgets = [] self.dragged_widget = None self._setup_ui() def _setup_ui(self): """Setup dashboard UI.""" self.setStyleSheet("background: transparent;") layout = QVBoxLayout(self) layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15) # Header c = get_color header_layout = QHBoxLayout() # Icon header_icon = QLabel() icon_pixmap = self.icon_manager.get_pixmap('zap', size=24) header_icon.setPixmap(icon_pixmap) header_icon.setFixedSize(24, 24) header_layout.addWidget(header_icon) header = QLabel("Dashboard") header.setStyleSheet(f""" color: {c('text_primary')}; font-size: 20px; font-weight: bold; """) header_layout.addWidget(header) header_layout.addStretch() layout.addLayout(header_layout) # Subtitle subtitle = QLabel("Customize your EU-Utility experience") subtitle.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;") layout.addWidget(subtitle) # Widget grid container scroll = QScrollArea() scroll.setWidgetResizable(True) 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(12) self.grid_layout.setContentsMargins(0, 0, 0, 0) scroll.setWidget(self.grid_widget) layout.addWidget(scroll) # Add default widgets self._add_default_widgets() def _add_default_widgets(self): """Add default widgets to dashboard.""" # Quick actions actions = [ {'icon': 'search', 'name': 'Search', 'callback': lambda: None}, {'icon': 'camera', 'name': 'Scan', 'callback': lambda: None}, {'icon': 'bar-chart', 'name': 'Skills', 'callback': lambda: None}, {'icon': 'music', 'name': 'Music', 'callback': lambda: None}, ] self.add_widget(QuickActionsWidget(actions)) # Spotify widget (if available) self.add_widget(SpotifyWidget()) # PED tracker self.add_widget(PEDTrackerWidget()) # Skill progress self.add_widget(SkillProgressWidget()) def add_widget(self, widget): """Add a widget to the dashboard.""" row = self.grid_layout.rowCount() col = 0 # Find next available position while self.grid_layout.itemAtPosition(row, col): col += 1 if col >= 2: # Max 2 columns col = 0 row += 1 self.grid_layout.addWidget(widget, row, col, widget.size[1], widget.size[0]) self.widgets.append(widget) self.widget_added.emit(widget) def remove_widget(self, widget): """Remove a widget from the dashboard.""" self.grid_layout.removeWidget(widget) widget.deleteLater() self.widgets.remove(widget) self.widget_removed.emit(widget) def start_widget_drag(self, widget): """Start dragging a widget.""" self.dragged_widget = widget def get_widget_positions(self): """Get current widget positions for saving.""" positions = [] for widget in self.widgets: idx = self.grid_layout.indexOf(widget) if idx >= 0: row, col, rowSpan, colSpan = self.grid_layout.getItemPosition(idx) positions.append({ 'type': widget.__class__.__name__, 'row': row, 'col': col, 'size': widget.size }) return positions