""" EU-Utility - Perfect UX Design System ==================================== Based on Nielsen's 10 Usability Heuristics and Material Design 3 principles. Key Principles Applied: 1. Visibility of System Status - Clear feedback everywhere 2. Match Real World - Familiar gaming tool patterns 3. User Control - Easy undo, clear exits 4. Consistency - Unified design language 5. Error Prevention - Confirmation dialogs, validation 6. Recognition > Recall - Visual icons, clear labels 7. Flexibility - Shortcuts for experts 8. Aesthetic & Minimal - Clean, focused UI 9. Error Recovery - Clear error messages 10. Help - Contextual tooltips, onboarding Material Design 3: - Elevation and depth - Consistent spacing (8dp grid) - Rounded corners (12dp, 16dp, 28dp) - Motion and transitions (150-300ms) - Color roles (primary, secondary, surface, error) """ from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget, QLabel, QPushButton, QFrame, QScrollArea, QGridLayout, QSizePolicy, QSpacerItem, QGraphicsDropShadowEffect, QProgressBar, QToolTip, QDialog, QLineEdit, QTextEdit, QCheckBox ) from PyQt6.QtCore import ( Qt, QTimer, pyqtSignal, QSize, QPropertyAnimation, QEasingCurve, QPoint, QParallelAnimationGroup ) from PyQt6.QtGui import ( QColor, QPainter, QLinearGradient, QFont, QIcon, QFontDatabase, QPalette, QCursor, QKeySequence, QShortcut ) from PyQt6.QtWidgets import QGraphicsOpacityEffect from core.eu_styles import get_all_colors from core.icon_manager import get_icon_manager # Import actual UI views try: from core.ui.dashboard_view import DashboardView DASHBOARD_VIEW_AVAILABLE = True except ImportError: DASHBOARD_VIEW_AVAILABLE = False try: from core.ui.settings_view import SettingsView SETTINGS_VIEW_AVAILABLE = True except ImportError: SETTINGS_VIEW_AVAILABLE = False try: from core.ui.plugins_view import PluginsView PLUGINS_VIEW_AVAILABLE = True except ImportError: PLUGINS_VIEW_AVAILABLE = False try: from core.ui.widgets_view import WidgetsView WIDGETS_VIEW_AVAILABLE = True except ImportError: WIDGETS_VIEW_AVAILABLE = False # ============================================================ # DESIGN TOKENS - Material Design 3 Inspired # ============================================================ class DesignTokens: """Central design tokens for consistent UI.""" # EU Color Palette EU_DARK_BLUE = "#141f23" EU_ORANGE = "#ff8c42" EU_SURFACE = "rgba(28, 35, 45, 0.95)" # Elevation (shadows) ELEVATION_0 = "0 0 0 rgba(0,0,0,0)" ELEVATION_1 = "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)" ELEVATION_2 = "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12)" ELEVATION_3 = "0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.1)" ELEVATION_4 = "0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.1)" # Spacing (8dp grid) SPACE_XS = 4 SPACE_S = 8 SPACE_M = 16 SPACE_L = 24 SPACE_XL = 32 SPACE_XXL = 48 # Border Radius RADIUS_S = 8 RADIUS_M = 12 RADIUS_L = 16 RADIUS_XL = 24 RADIUS_FULL = 9999 # Motion Duration DURATION_FAST = 150 DURATION_NORMAL = 250 DURATION_SLOW = 350 # Typography Scale FONT_FAMILY = "Inter, SF Pro Display, Segoe UI, sans-serif" @classmethod def get_color(cls, name: str) -> str: c = get_all_colors() return c.get(name, "#ffffff") # ============================================================ # COMPONENT LIBRARY # ============================================================ class Surface(QFrame): """Material Design Surface component with glassmorphism.""" def __init__(self, elevation: int = 1, radius: int = 16, parent=None): super().__init__(parent) self.elevation = elevation self.radius = radius self._setup_style() self._setup_shadow() def _setup_style(self): """Apply surface styling with glassmorphism.""" c = get_all_colors() bg_opacity = 0.90 + (self.elevation * 0.02) bg = f"rgba(20, 31, 35, {min(bg_opacity, 0.98)})" border_opacity = 0.08 + (self.elevation * 0.02) border = f"rgba(255, 140, 66, {min(border_opacity, 0.2)})" self.setStyleSheet(f""" Surface {{ background: {bg}; border: 1px solid {border}; border-radius: {self.radius}px; }} """) def _setup_shadow(self): """Apply drop shadow based on elevation.""" if self.elevation > 0: shadow = QGraphicsDropShadowEffect(self) shadow.setBlurRadius(self.elevation * 12) shadow.setXOffset(0) shadow.setYOffset(self.elevation * 3) opacity = min(self.elevation * 0.1, 0.5) shadow.setColor(QColor(0, 0, 0, int(255 * opacity))) self.setGraphicsEffect(shadow) class Button(QPushButton): """Material Design 3 button with proper states and motion.""" clicked_animation = pyqtSignal() def __init__(self, text: str, variant: str = "filled", icon: str = None, parent=None): super().__init__(text, parent) self.variant = variant self.icon_text = icon self._hovered = False self._pressed = False self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.setFixedHeight(44) self._setup_style() self._setup_animations() # Load icon if provided if icon: self._load_icon(icon) def _load_icon(self, icon_name: str): """Load SVG icon for button.""" icon_mgr = get_icon_manager() pixmap = icon_mgr.get_pixmap(icon_name, size=20) self.setIcon(QIcon(pixmap)) self.setIconSize(QSize(20, 20)) def _setup_style(self): """Apply button styling based on variant.""" c = get_all_colors() orange = DesignTokens.EU_ORANGE styles = { "filled": f""" QPushButton {{ background: {orange}; color: white; border: none; border-radius: 22px; padding: 0 24px; font-size: 14px; font-weight: 600; font-family: {DesignTokens.FONT_FAMILY}; }} QPushButton:hover {{ background: #ffa366; }} QPushButton:pressed {{ background: #e67a3a; }} QPushButton:disabled {{ background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.38); }} """, "tonal": f""" QPushButton {{ background: rgba(255, 140, 66, 0.15); color: {orange}; border: none; border-radius: 22px; padding: 0 24px; font-size: 14px; font-weight: 600; }} QPushButton:hover {{ background: rgba(255, 140, 66, 0.25); }} QPushButton:pressed {{ background: rgba(255, 140, 66, 0.35); }} """, "outlined": f""" QPushButton {{ background: transparent; color: {orange}; border: 1px solid rgba(255, 140, 66, 0.5); border-radius: 22px; padding: 0 24px; font-size: 14px; font-weight: 600; }} QPushButton:hover {{ background: rgba(255, 140, 66, 0.08); border: 1px solid {orange}; }} """, "text": f""" QPushButton {{ background: transparent; color: {orange}; border: none; border-radius: 8px; padding: 0 16px; font-size: 14px; font-weight: 600; }} QPushButton:hover {{ background: rgba(255, 140, 66, 0.08); }} """, "elevated": f""" QPushButton {{ background: rgba(45, 55, 72, 0.9); color: {c['text_primary']}; border: 1px solid rgba(255, 140, 66, 0.08); border-radius: 22px; padding: 0 24px; font-size: 14px; font-weight: 600; }} QPushButton:hover {{ background: rgba(55, 65, 82, 0.95); border: 1px solid rgba(255, 140, 66, 0.15); }} """ } self.setStyleSheet(styles.get(self.variant, styles["filled"])) def _setup_animations(self): """Setup press/hover animations.""" self._scale_anim = QPropertyAnimation(self, b"minimumWidth") self._scale_anim.setDuration(DesignTokens.DURATION_FAST) self._scale_anim.setEasingCurve(QEasingCurve.Type.OutCubic) def enterEvent(self, event): """Hover start animation.""" self._hovered = True super().enterEvent(event) def leaveEvent(self, event): """Hover end animation.""" self._hovered = False super().leaveEvent(event) def mousePressEvent(self, event): """Press animation.""" self._pressed = True self.clicked_animation.emit() super().mousePressEvent(event) class Card(Surface): """Material Design Card component with EU aesthetic.""" def __init__(self, title: str = None, subtitle: str = None, parent=None): super().__init__(elevation=2, radius=16, parent=parent) self.layout = QVBoxLayout(self) self.layout.setContentsMargins(24, 24, 24, 24) self.layout.setSpacing(16) # Header if title: self._create_header(title, subtitle) def _create_header(self, title: str, subtitle: str = None): """Create card header.""" c = get_all_colors() title_label = QLabel(title) title_label.setStyleSheet(f""" color: {c['text_primary']}; font-size: 18px; font-weight: 600; font-family: {DesignTokens.FONT_FAMILY}; """) self.layout.addWidget(title_label) if subtitle: subtitle_label = QLabel(subtitle) subtitle_label.setStyleSheet(f""" color: {c['text_secondary']}; font-size: 13px; font-family: {DesignTokens.FONT_FAMILY}; """) self.layout.addWidget(subtitle_label) # Separator separator = QFrame() separator.setFixedHeight(1) separator.setStyleSheet("background: rgba(255, 140, 66, 0.1);") self.layout.addWidget(separator) def set_content(self, widget: QWidget): """Set card content.""" self.layout.addWidget(widget, 1) class NavigationRail(QFrame): """Material Design Navigation Rail with EU styling - Expandable with labels.""" destination_changed = pyqtSignal(str) # Widths for collapsed/expanded states WIDTH_COLLAPSED = 80 WIDTH_EXPANDED = 200 def __init__(self, parent=None): super().__init__(parent) self.destinations = [] self.active_destination = None self.icon_manager = get_icon_manager() self._expanded = False self._setup_style() self._setup_layout() def _setup_style(self): """Apply navigation rail styling.""" self.setFixedWidth(self.WIDTH_COLLAPSED) self.setStyleSheet(""" NavigationRail { background: rgba(20, 31, 35, 0.98); border-right: 1px solid rgba(255, 140, 66, 0.08); } """) def _setup_layout(self): """Setup vertical layout with expand toggle.""" self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 12, 0, 12) self.main_layout.setSpacing(0) # Top toggle button - use arrow pointing right when collapsed self.toggle_btn = QPushButton() self.toggle_btn.setFixedSize(56, 32) self.toggle_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.toggle_btn.setToolTip("Expand sidebar") self.toggle_btn.setIcon(self.icon_manager.get_icon("chevron-right")) # Arrow pointing right when collapsed self.toggle_btn.setIconSize(QSize(20, 20)) self.toggle_btn.setStyleSheet(f""" QPushButton {{ background: transparent; border: none; border-radius: 8px; margin: 0 12px; }} QPushButton:hover {{ background: rgba(255, 140, 66, 0.15); }} """) self.toggle_btn.clicked.connect(self._toggle_expanded) toggle_container = QWidget() toggle_layout = QHBoxLayout(toggle_container) toggle_layout.setContentsMargins(0, 0, 0, 0) toggle_layout.addWidget(self.toggle_btn) toggle_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.main_layout.addWidget(toggle_container) # Spacer after toggle self.main_layout.addSpacing(16) # Destinations container self.destinations_widget = QWidget() self.destinations_layout = QVBoxLayout(self.destinations_widget) self.destinations_layout.setContentsMargins(12, 0, 12, 0) self.destinations_layout.setSpacing(8) self.destinations_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.main_layout.addWidget(self.destinations_widget) # Add spacer at bottom self.main_layout.addStretch() def _toggle_expanded(self): """Toggle between collapsed and expanded states.""" self._expanded = not self._expanded # Animate width change self._animate_width() # Update toggle button icon - use arrows instead of menu/close if self._expanded: self.toggle_btn.setToolTip("Collapse sidebar") self.toggle_btn.setIcon(self.icon_manager.get_icon("chevron-left")) # Arrow pointing left else: self.toggle_btn.setToolTip("Expand sidebar") self.toggle_btn.setIcon(self.icon_manager.get_icon("chevron-right")) # Arrow pointing right # Update all destinations to show/hide labels for dest in self.destinations: dest.set_expanded(self._expanded) def _animate_width(self): """Animate the width change.""" target_width = self.WIDTH_EXPANDED if self._expanded else self.WIDTH_COLLAPSED # Use QPropertyAnimation for smooth width transition self._width_anim = QPropertyAnimation(self, b"minimumWidth") self._width_anim.setDuration(DesignTokens.DURATION_NORMAL) self._width_anim.setStartValue(self.width()) self._width_anim.setEndValue(target_width) self._width_anim.setEasingCurve(QEasingCurve.Type.OutCubic) self._width_anim2 = QPropertyAnimation(self, b"maximumWidth") self._width_anim2.setDuration(DesignTokens.DURATION_NORMAL) self._width_anim2.setStartValue(self.width()) self._width_anim2.setEndValue(target_width) self._width_anim2.setEasingCurve(QEasingCurve.Type.OutCubic) # Update fixed width when done self._width_anim2.finished.connect(lambda: self.setFixedWidth(target_width)) self._width_anim.start() self._width_anim2.start() def add_destination(self, icon_name: str, label: str, destination_id: str): """Add a navigation destination with SVG icon.""" btn = NavigationDestination(icon_name, label, destination_id, self.icon_manager) btn.clicked.connect(lambda: self._on_destination_clicked(destination_id)) btn.set_expanded(self._expanded) self.destinations.append(btn) self.destinations_layout.addWidget(btn) def _on_destination_clicked(self, destination_id: str): """Handle destination selection.""" self.set_active_destination(destination_id) self.destination_changed.emit(destination_id) def set_active_destination(self, destination_id: str): """Set active destination.""" self.active_destination = destination_id for dest in self.destinations: dest.set_active(dest.destination_id == destination_id) def is_expanded(self) -> bool: """Return current expanded state.""" return self._expanded class NavigationDestination(QPushButton): """Single navigation destination with SVG icon and optional label.""" def __init__(self, icon_name: str, label: str, destination_id: str, icon_manager, parent=None): super().__init__(parent) self.destination_id = destination_id self.icon_manager = icon_manager self.icon_name = icon_name self.label_text = label self._expanded = False self._active = False self._setup_style() self._create_content() def _setup_style(self): """Apply destination styling.""" self.setMinimumHeight(48) self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self._update_style(False) def _create_content(self): """Create icon and label.""" # Horizontal layout for icon + label self.content_layout = QHBoxLayout(self) self.content_layout.setContentsMargins(12, 8, 12, 8) self.content_layout.setSpacing(12) # Icon self.icon_label = QLabel() self.icon_label.setPixmap(self.icon_manager.get_pixmap(self.icon_name, size=24)) self.content_layout.addWidget(self.icon_label) # Text label (hidden when collapsed) self.text_label = QLabel(self.label_text) self.text_label.setStyleSheet(""" QLabel { color: rgba(255, 255, 255, 0.7); font-size: 14px; font-weight: 500; } """) self.text_label.hide() self.content_layout.addWidget(self.text_label) self.content_layout.addStretch() self.setToolTip(self.label_text) def set_expanded(self, expanded: bool): """Show or hide the text label.""" self._expanded = expanded if expanded: self.text_label.show() self.setMinimumWidth(176) # Fill expanded rail width self.setMaximumWidth(176) else: self.text_label.hide() self.setFixedSize(56, 48) self._update_style(self._active) def _update_style(self, active: bool): """Update style based on active state with orange accent border.""" self._active = active orange = DesignTokens.EU_ORANGE if self._expanded: # Expanded style - full width with left border if active: self.setStyleSheet(f""" QPushButton {{ background: rgba(255, 140, 66, 0.15); border: none; border-left: 3px solid {orange}; border-radius: 0 12px 12px 0; text-align: left; padding-left: 9px; }} QLabel {{ color: #ffffff; font-weight: 600; }} """) else: self.setStyleSheet(f""" QPushButton {{ background: transparent; border: none; border-left: 3px solid transparent; border-radius: 0 12px 12px 0; text-align: left; padding-left: 9px; }} QPushButton:hover {{ background: rgba(255, 255, 255, 0.05); border-left: 3px solid rgba(255, 140, 66, 0.3); }} QLabel {{ color: rgba(255, 255, 255, 0.7); }} """) else: # Collapsed style - icon only if active: self.setStyleSheet(f""" QPushButton {{ background: rgba(255, 140, 66, 0.15); border: none; border-left: 3px solid {orange}; border-radius: 0 16px 16px 0; }} """) else: self.setStyleSheet(f""" QPushButton {{ background: transparent; border: none; border-left: 3px solid transparent; border-radius: 0 16px 16px 0; }} QPushButton:hover {{ background: rgba(255, 255, 255, 0.05); border-left: 3px solid rgba(255, 140, 66, 0.3); }} """) def set_active(self, active: bool): """Set active state.""" self._update_style(active) class StatusIndicator(QFrame): """System status indicator with SVG icons.""" STATUS_COLORS = { "active": "#4ecca3", "warning": "#ffd93d", "error": "#ff6b6b", "idle": "#6b7280", "busy": "#3b82f6" } def __init__(self, name: str, status: str = "idle", parent=None): super().__init__(parent) self.name = name self.status = status self.icon_manager = get_icon_manager() self._setup_ui() def _setup_ui(self): """Setup indicator UI.""" layout = QHBoxLayout(self) layout.setContentsMargins(12, 8, 12, 8) layout.setSpacing(10) # Status dot icon self.dot = QLabel() dot_pixmap = self.icon_manager.get_pixmap("check", size=10) self.dot.setPixmap(dot_pixmap) layout.addWidget(self.dot) # Name self.name_label = QLabel(self.name) self.name_label.setStyleSheet("font-size: 13px; font-weight: 500;") layout.addWidget(self.name_label) layout.addStretch() # Status text self.status_label = QLabel(self.status.title()) self.status_label.setStyleSheet("font-size: 12px; opacity: 0.7;") layout.addWidget(self.status_label) self._update_style() def set_status(self, status: str): """Update status with animation.""" old_status = self.status self.status = status self.status_label.setText(status.title()) self._update_style() if old_status != status: self._pulse_animation() def _update_style(self): """Update colors based on status.""" c = get_all_colors() color = self.STATUS_COLORS.get(self.status, c['text_secondary']) self.dot.setStyleSheet(f"color: {color}; font-size: 10px;") self.name_label.setStyleSheet(f"color: {c['text_primary']}; font-size: 13px; font-weight: 500;") self.status_label.setStyleSheet(f"color: {color}; font-size: 12px;") def _pulse_animation(self): """Pulse animation on status change.""" anim = QPropertyAnimation(self, b"minimumHeight") anim.setDuration(300) anim.setStartValue(self.height()) anim.setEndValue(self.height() + 4) anim.setEasingCurve(QEasingCurve.Type.OutBounce) anim.start() class IconButton(QPushButton): """Icon-only button with SVG support.""" def __init__(self, icon_name: str, tooltip: str = None, size: int = 40, parent=None): super().__init__(parent) self.icon_manager = get_icon_manager() self.setFixedSize(size, size) self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # Load icon self.setIcon(self.icon_manager.get_icon(icon_name)) self.setIconSize(QSize(size - 16, size - 16)) if tooltip: self.setToolTip(tooltip) self._setup_style() def _setup_style(self): """Apply icon button styling.""" self.setStyleSheet(""" QPushButton { background: transparent; border: none; border-radius: 8px; } QPushButton:hover { background: rgba(255, 255, 255, 0.1); } QPushButton:pressed { background: rgba(255, 255, 255, 0.05); } """) # ============================================================ # PERFECT MAIN WINDOW # ============================================================ class PerfectMainWindow(QMainWindow): """Perfect UX Main Window - emoji-free, professional UI.""" def __init__(self, plugin_manager, parent=None): super().__init__(parent) self.plugin_manager = plugin_manager self._current_view = "dashboard" self.icon_manager = get_icon_manager() # Add settings reference for SettingsView compatibility from core.settings import get_settings self.settings = get_settings() self._setup_window() self._setup_ui() self._setup_shortcuts() def _setup_window(self): """Setup window with proper sizing and positioning.""" self.setWindowTitle("EU-Utility") self.setMinimumSize(1280, 800) self.resize(1440, 900) # Center on screen screen = self.screen().geometry() self.move( (screen.width() - self.width()) // 2, (screen.height() - self.height()) // 2 ) # Apply global stylesheet self._apply_global_styles() def _apply_global_styles(self): """Apply global Material Design 3 styles.""" c = get_all_colors() self.setStyleSheet(f""" QMainWindow {{ background: {DesignTokens.EU_DARK_BLUE}; }} QToolTip {{ background: rgba(20, 31, 35, 0.98); color: {c['text_primary']}; border: 1px solid rgba(255, 140, 66, 0.2); border-radius: 8px; padding: 8px 12px; font-size: 13px; }} QScrollBar:vertical {{ background: transparent; width: 8px; margin: 0; }} QScrollBar::handle:vertical {{ background: rgba(255, 140, 66, 0.3); border-radius: 4px; min-height: 40px; }} QScrollBar::handle:vertical:hover {{ background: rgba(255, 140, 66, 0.5); }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }} """) def _setup_ui(self): """Setup the professional UI structure.""" central = QWidget() self.setCentralWidget(central) layout = QHBoxLayout(central) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Navigation Rail with SVG icons self.nav_rail = NavigationRail() self.nav_rail.add_destination("dashboard", "Dashboard", "dashboard") self.nav_rail.add_destination("plugins", "Plugins", "plugins") self.nav_rail.add_destination("widgets", "Widgets", "widgets") self.nav_rail.add_destination("settings", "Settings", "settings") self.nav_rail.destination_changed.connect(self._on_nav_changed) layout.addWidget(self.nav_rail) # Main Content Area self.content_stack = QStackedWidget() self.content_stack.setStyleSheet("background: transparent;") # Create views self.dashboard_view = self._create_dashboard_view() self.plugins_view = self._create_plugins_view() self.widgets_view = self._create_widgets_view() self.settings_view = self._create_settings_view() self.content_stack.addWidget(self.dashboard_view) self.content_stack.addWidget(self.plugins_view) self.content_stack.addWidget(self.widgets_view) self.content_stack.addWidget(self.settings_view) layout.addWidget(self.content_stack, 1) # Status Bar self._create_status_bar() def _create_dashboard_view(self) -> QWidget: """Create the Dashboard view.""" # Try to use the actual DashboardView if available if DASHBOARD_VIEW_AVAILABLE: try: return DashboardView(self) except Exception as e: print(f"[PerfectUX] Failed to create DashboardView: {e}") # Fallback to built-in dashboard scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll.setStyleSheet("background: transparent; border: none;") container = QWidget() container.setStyleSheet("background: transparent;") layout = QVBoxLayout(container) layout.setContentsMargins(32, 32, 32, 32) layout.setSpacing(24) c = get_all_colors() # Header header = QLabel("Dashboard") header.setStyleSheet(f""" color: {c['text_primary']}; font-size: 36px; font-weight: 700; font-family: {DesignTokens.FONT_FAMILY}; """) layout.addWidget(header) subtitle = QLabel("Monitor your game, track progress, and access tools quickly.") subtitle.setStyleSheet(f"color: {c['text_secondary']}; font-size: 14px;") subtitle.setWordWrap(True) layout.addWidget(subtitle) # System Status Card status_card = Card("System Status", "Live service monitoring") status_widget = QWidget() status_layout = QVBoxLayout(status_widget) status_layout.setSpacing(8) self.status_indicators = { "log_reader": StatusIndicator("Log Reader", "active"), "ocr": StatusIndicator("OCR Service", "idle"), "event_bus": StatusIndicator("Event Bus", "active"), "nexus_api": StatusIndicator("Nexus API", "idle"), } for indicator in self.status_indicators.values(): status_layout.addWidget(indicator) status_card.set_content(status_widget) layout.addWidget(status_card) # Quick Actions Grid actions_card = Card("Quick Actions", "Frequently used tools") actions_widget = QWidget() actions_layout = QGridLayout(actions_widget) actions_layout.setSpacing(12) # Actions with SVG icons actions = [ ("camera", "Scan Skills", "Use OCR to scan skill levels"), ("package", "Check Loot", "Review recent loot data"), ("globe", "Search Nexus", "Find items and prices"), ("bar-chart", "View Stats", "See your hunting analytics"), ] for i, (icon, title, tooltip) in enumerate(actions): btn = Button(title, variant="elevated", icon=icon) btn.setToolTip(tooltip) btn.setFixedHeight(56) actions_layout.addWidget(btn, i // 2, i % 2) actions_card.set_content(actions_widget) layout.addWidget(actions_card) # Recent Activity activity_card = Card("Recent Activity", "Latest events and updates") activity_widget = QWidget() activity_layout = QVBoxLayout(activity_widget) activity_layout.setSpacing(12) activities = [ ("Plugin updated", "Clock Widget v1.0.1 installed", "2m ago", "check"), ("Scan completed", "Found 12 skills on page 1", "15m ago", "info"), ("Settings changed", "Theme set to Dark", "1h ago", "more"), ] for title, detail, time, icon_name in activities: item = self._create_activity_item(title, detail, time, icon_name) activity_layout.addWidget(item) activity_card.set_content(activity_widget) layout.addWidget(activity_card) layout.addStretch() scroll.setWidget(container) return scroll def _create_activity_item(self, title: str, detail: str, time: str, icon_name: str) -> QFrame: """Create a single activity item with SVG icon.""" c = get_all_colors() item = QFrame() item.setStyleSheet(""" QFrame { background: rgba(255, 255, 255, 0.03); border-radius: 12px; } QFrame:hover { background: rgba(255, 255, 255, 0.06); } """) layout = QHBoxLayout(item) layout.setContentsMargins(16, 12, 16, 12) # Icon icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=16) icon_label.setPixmap(icon_pixmap) layout.addWidget(icon_label) # Content content_layout = QVBoxLayout() content_layout.setSpacing(2) title_label = QLabel(title) title_label.setStyleSheet(f"color: {c['text_primary']}; font-size: 14px; font-weight: 500;") content_layout.addWidget(title_label) detail_label = QLabel(detail) detail_label.setStyleSheet(f"color: {c['text_secondary']}; font-size: 12px;") content_layout.addWidget(detail_label) layout.addLayout(content_layout, 1) # Time time_label = QLabel(time) time_label.setStyleSheet(f"color: {c['text_muted']}; font-size: 11px;") layout.addWidget(time_label) return item def _create_plugins_view(self) -> QWidget: """Create the Plugins view with Installed and Store tabs.""" from PyQt6.QtWidgets import QTabWidget, QCheckBox, QGroupBox, QScrollArea container = QWidget() layout = QVBoxLayout(container) layout.setContentsMargins(32, 32, 32, 32) layout.setSpacing(16) c = get_all_colors() # Header header = QLabel("Plugins") header.setStyleSheet(f"font-size: 36px; font-weight: 700; color: {c['text_primary']};") layout.addWidget(header) subtitle = QLabel("Manage installed plugins and browse the store.") subtitle.setStyleSheet(f"color: {c['text_secondary']}; font-size: 14px;") layout.addWidget(subtitle) # Tabs tabs = QTabWidget() tabs.setStyleSheet(f""" QTabBar::tab {{ background-color: rgba(20, 31, 35, 0.95); color: rgba(255,255,255,150); padding: 10px 20px; border-top-left-radius: 6px; border-top-right-radius: 6px; }} QTabBar::tab:selected {{ background-color: {DesignTokens.EU_ORANGE}; color: white; font-weight: bold; }} """) # Installed Tab installed_tab = QWidget() installed_layout = QVBoxLayout(installed_tab) installed_layout.setSpacing(12) info = QLabel("Enable or disable installed plugins. Changes take effect immediately.") info.setStyleSheet(f"color: {c['text_secondary']}; font-size: 13px;") info.setWordWrap(True) installed_layout.addWidget(info) # Plugin list scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet("background: transparent; border: none;") scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) plugins_container = QWidget() plugins_layout = QVBoxLayout(plugins_container) plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop) plugins_layout.setSpacing(8) # Get plugins from plugin_manager if self.plugin_manager: try: all_plugins = self.plugin_manager.get_all_discovered_plugins() if all_plugins: for plugin_id, plugin_class in sorted(all_plugins.items(), key=lambda x: x[1].name if hasattr(x[1], 'name') else str(x[0])): row = QHBoxLayout() row.setSpacing(12) # Checkbox cb = QCheckBox() try: is_enabled = self.plugin_manager.is_plugin_enabled(plugin_id) except: is_enabled = True cb.setChecked(is_enabled) cb.stateChanged.connect(lambda state, pid=plugin_id: self._toggle_plugin(pid, state)) row.addWidget(cb) # Plugin info info_layout = QVBoxLayout() info_layout.setSpacing(2) name_layout = QHBoxLayout() name = QLabel(getattr(plugin_class, 'name', str(plugin_id))) name.setStyleSheet(f"color: {c['text_primary']}; font-size: 14px; font-weight: 500;") name_layout.addWidget(name) version = getattr(plugin_class, 'version', '?.?.?') version_label = QLabel(f"v{version}") version_label.setStyleSheet(f"color: {c['text_muted']}; font-size: 11px;") name_layout.addWidget(version_label) name_layout.addStretch() info_layout.addLayout(name_layout) desc = getattr(plugin_class, 'description', 'No description') desc_label = QLabel(desc) desc_label.setStyleSheet(f"color: {c['text_secondary']}; font-size: 12px;") desc_label.setWordWrap(True) info_layout.addWidget(desc_label) row.addLayout(info_layout, 1) plugins_layout.addLayout(row) # Separator sep = QFrame() sep.setFrameShape(QFrame.Shape.HLine) sep.setStyleSheet(f"background-color: {DesignTokens.EU_ORANGE}33;") sep.setFixedHeight(1) plugins_layout.addWidget(sep) else: no_plugins = QLabel("No plugins installed.\n\nVisit the Store tab to install plugins.") no_plugins.setStyleSheet(f"color: {c['text_muted']}; padding: 40px;") no_plugins.setAlignment(Qt.AlignmentFlag.AlignCenter) plugins_layout.addWidget(no_plugins) except Exception as e: error = QLabel(f"Error loading plugins: {e}") error.setStyleSheet("color: #ff4757;") plugins_layout.addWidget(error) else: no_pm = QLabel("Plugin Manager not available") no_pm.setStyleSheet(f"color: {c['text_muted']}; padding: 40px;") no_pm.setAlignment(Qt.AlignmentFlag.AlignCenter) plugins_layout.addWidget(no_pm) plugins_layout.addStretch() scroll.setWidget(plugins_container) installed_layout.addWidget(scroll) tabs.addTab(installed_tab, "Installed") # Store Tab store_tab = QWidget() store_layout = QVBoxLayout(store_tab) store_info = QLabel("Browse and install plugins from the community repository.") store_info.setStyleSheet(f"color: {c['text_secondary']}; font-size: 13px;") store_info.setWordWrap(True) store_layout.addWidget(store_info) # Try to add PluginStoreUI try: from core.plugin_store import PluginStoreUI if self.plugin_manager: store_ui = PluginStoreUI(self.plugin_manager) store_layout.addWidget(store_ui) else: raise Exception("PluginManager not available") except Exception as e: placeholder = Card("Plugin Store", "Browse community plugins") ph_layout = QVBoxLayout() ph_text = QLabel("Plugin Store interface loading...") ph_text.setStyleSheet(f"color: {c['text_secondary']}; padding: 40px;") ph_text.setAlignment(Qt.AlignmentFlag.AlignCenter) ph_layout.addWidget(ph_text) placeholder.set_content(ph_layout) store_layout.addWidget(placeholder) store_layout.addStretch() tabs.addTab(store_tab, "Store") layout.addWidget(tabs) return container def _toggle_plugin(self, plugin_id: str, state: int): """Enable or disable a plugin.""" if not self.plugin_manager: return try: if state == Qt.CheckState.Checked.value: self.plugin_manager.enable_plugin(plugin_id) print(f"[Plugins] Enabled: {plugin_id}") else: self.plugin_manager.disable_plugin(plugin_id) print(f"[Plugins] Disabled: {plugin_id}") except Exception as e: print(f"[Plugins] Error toggling {plugin_id}: {e}") def _create_widgets_view(self) -> QWidget: """Create the Widgets view with registered widgets.""" from PyQt6.QtWidgets import QScrollArea container = QWidget() layout = QVBoxLayout(container) layout.setContentsMargins(32, 32, 32, 32) layout.setSpacing(16) c = get_all_colors() # Header header = QLabel("Widgets") header.setStyleSheet(f"font-size: 36px; font-weight: 700; color: {c['text_primary']};") layout.addWidget(header) subtitle = QLabel("Manage overlay widgets for the activity bar.") subtitle.setStyleSheet(f"color: {c['text_secondary']}; font-size: 14px;") layout.addWidget(subtitle) # Scroll area for widgets scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet("background: transparent; border: none;") scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) widgets_container = QWidget() widgets_layout = QVBoxLayout(widgets_container) widgets_layout.setAlignment(Qt.AlignmentFlag.AlignTop) widgets_layout.setSpacing(12) # Built-in widgets list builtin_widgets = [ {'id': 'clock', 'name': 'Clock Widget', 'description': 'Digital clock with date for activity bar', 'plugin': 'Built-in', 'enabled': True}, {'id': 'system_monitor', 'name': 'System Monitor', 'description': 'CPU and RAM usage display', 'plugin': 'System Tools', 'enabled': False}, {'id': 'skill_tracker', 'name': 'Skill Tracker Mini', 'description': 'Quick view of skill gains', 'plugin': 'Skill Scanner', 'enabled': False}, ] for widget_info in builtin_widgets: widget_card = self._create_widget_card(widget_info) widgets_layout.addWidget(widget_card) widgets_layout.addStretch() scroll.setWidget(widgets_container) layout.addWidget(scroll) return container def _create_widget_card(self, widget_info) -> QFrame: """Create a widget card.""" c = get_all_colors() card = QFrame() card.setStyleSheet(f""" QFrame {{ background: rgba(255, 255, 255, 0.03); border-radius: 12px; }} QFrame:hover {{ background: rgba(255, 255, 255, 0.05); }} """) layout = QHBoxLayout(card) layout.setContentsMargins(16, 12, 16, 12) layout.setSpacing(12) # Enable checkbox enabled = widget_info.get('enabled', False) cb = QCheckBox() cb.setChecked(enabled) layout.addWidget(cb) # Widget info info_layout = QVBoxLayout() info_layout.setSpacing(4) # Name and plugin name_row = QHBoxLayout() name = QLabel(widget_info.get('name', 'Unknown')) name.setStyleSheet(f"color: {c['text_primary']}; font-size: 14px; font-weight: 500;") name_row.addWidget(name) plugin = widget_info.get('plugin', '') if plugin: plugin_label = QLabel(f"via {plugin}") plugin_label.setStyleSheet(f"color: {DesignTokens.EU_ORANGE}CC; font-size: 11px;") name_row.addWidget(plugin_label) name_row.addStretch() info_layout.addLayout(name_row) # Description desc = widget_info.get('description', '') desc_label = QLabel(desc) desc_label.setStyleSheet(f"color: {c['text_secondary']}; font-size: 12px;") desc_label.setWordWrap(True) info_layout.addWidget(desc_label) layout.addLayout(info_layout, 1) # Preview button preview_btn = QPushButton("Preview") preview_btn.setStyleSheet(f""" QPushButton {{ background: {DesignTokens.EU_ORANGE}33; color: {DesignTokens.EU_ORANGE}; border: 1px solid {DesignTokens.EU_ORANGE}4D; border-radius: 6px; padding: 6px 12px; font-size: 11px; }} QPushButton:hover {{ background: {DesignTokens.EU_ORANGE}4D; }} """) layout.addWidget(preview_btn) return card def _create_settings_view(self) -> QWidget: """Create the Settings view.""" # Try to use the actual SettingsView if available if SETTINGS_VIEW_AVAILABLE: try: return SettingsView(self) except Exception as e: print(f"[PerfectUX] Failed to create SettingsView: {e}") # Fallback to placeholder container = QWidget() layout = QVBoxLayout(container) layout.setContentsMargins(32, 32, 32, 32) c = get_all_colors() header = QLabel("Settings") header.setStyleSheet(f"font-size: 36px; font-weight: 700; color: {c['text_primary']};") layout.addWidget(header) subtitle = QLabel("Configure application preferences and options.") subtitle.setStyleSheet(f"color: {c['text_secondary']}; font-size: 14px;") layout.addWidget(subtitle) layout.addStretch() return container def _create_status_bar(self): """Create bottom status bar with useful info.""" self.status_bar = QFrame() self.status_bar.setFixedHeight(32) self.status_bar.setStyleSheet(""" QFrame { background: rgba(15, 20, 25, 0.98); border-top: 1px solid rgba(255, 140, 66, 0.06); } """) layout = QHBoxLayout(self.status_bar) layout.setContentsMargins(16, 0, 16, 0) # Left side - Plugin count try: plugin_count = len(self.plugin_manager.get_all_plugins()) if self.plugin_manager else 0 except: plugin_count = 0 self.plugin_count_label = QLabel(f"{plugin_count} Plugins") self.plugin_count_label.setStyleSheet("color: rgba(255, 255, 255, 0.5); font-size: 12px;") layout.addWidget(self.plugin_count_label) layout.addSpacing(20) # Middle - EU Status self.eu_status_label = QLabel("EU: Not Running") self.eu_status_label.setStyleSheet("color: rgba(255, 255, 255, 0.4); font-size: 12px;") layout.addWidget(self.eu_status_label) layout.addStretch() # Right side - Version self.version_text = QLabel("EU-Utility v2.2.0") self.version_text.setStyleSheet("color: rgba(255, 255, 255, 0.3); font-size: 11px;") layout.addWidget(self.version_text) self.centralWidget().layout().addWidget(self.status_bar) # Start EU status timer self._start_eu_status_timer() def _start_eu_status_timer(self): """Start timer to update EU status in status bar.""" from PyQt6.QtCore import QTimer self._status_timer = QTimer(self) self._status_timer.timeout.connect(self._update_eu_status) self._status_timer.start(2000) # Update every 2 seconds def _update_eu_status(self): """Update EU status in status bar.""" try: from core.window_manager import get_window_manager wm = get_window_manager() if wm and wm.is_available(): eu_window = wm.find_eu_window() if eu_window: if eu_window.is_focused: self.eu_status_label.setText("EU: Focused") self.eu_status_label.setStyleSheet("color: #4ecca3; font-size: 12px;") # Green else: self.eu_status_label.setText("EU: Running") self.eu_status_label.setStyleSheet("color: #ffd93d; font-size: 12px;") # Yellow else: self.eu_status_label.setText("EU: Not Running") self.eu_status_label.setStyleSheet("color: rgba(255, 255, 255, 0.4); font-size: 12px;") except: pass def _setup_shortcuts(self): """Setup keyboard shortcuts.""" for i, view in enumerate(["dashboard", "plugins", "widgets", "settings"]): shortcut = QShortcut(QKeySequence(f"Ctrl+{i+1}"), self) shortcut.activated.connect(lambda v=view: self._navigate_to(v)) def _on_nav_changed(self, destination_id: str): """Handle navigation change with animation.""" self._current_view = destination_id view_map = { "dashboard": 0, "plugins": 1, "widgets": 2, "settings": 3 } if destination_id in view_map: self._animate_transition(view_map[destination_id]) # Update status bar view indicator if hasattr(self, 'plugin_count_label'): view_name = destination_id.replace('_', ' ').title() # Don't overwrite the plugin count, just update the view context # The status bar now shows plugins, EU status, and version def _animate_transition(self, index: int): """Animate view transition.""" self.content_stack.setCurrentIndex(index) def _navigate_to(self, view: str): """Navigate to specific view.""" self.nav_rail.set_active_destination(view) self._on_nav_changed(view) def show_plugin(self, plugin_id: str): """Show specific plugin view.""" self._navigate_to("plugins") def create_perfect_window(plugin_manager) -> PerfectMainWindow: """Factory function for creating the perfect main window.""" return PerfectMainWindow(plugin_manager)