1438 lines
52 KiB
Python
1438 lines
52 KiB
Python
"""
|
|
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
|
|
)
|
|
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)
|