EU-Utility/core/perfect_ux.py

1209 lines
42 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."""
# Try to use the actual PluginsView if available
if PLUGINS_VIEW_AVAILABLE and self.plugin_manager:
try:
return PluginsView(self)
except Exception as e:
print(f"[PerfectUX] Failed to create PluginsView: {e}")
# Fallback to placeholder
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(32, 32, 32, 32)
c = get_all_colors()
header = QLabel("Plugins")
header.setStyleSheet(f"font-size: 36px; font-weight: 700; color: {c['text_primary']};")
layout.addWidget(header)
subtitle = QLabel("Manage and configure plugins to extend functionality.")
subtitle.setStyleSheet(f"color: {c['text_secondary']}; font-size: 14px;")
layout.addWidget(subtitle)
# Placeholder for plugin grid
placeholder = Card("Plugin Manager", "Browse, install, and configure plugins")
placeholder_layout = QVBoxLayout()
placeholder_text = QLabel("Plugin grid and management interface will be displayed here.")
placeholder_text.setStyleSheet(f"color: {c['text_secondary']}; padding: 40px;")
placeholder_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
placeholder_layout.addWidget(placeholder_text)
layout.addWidget(placeholder)
layout.addStretch()
return container
def _create_widgets_view(self) -> QWidget:
"""Create the Widgets view."""
# Try to use the actual WidgetsView if available
if WIDGETS_VIEW_AVAILABLE and self.plugin_manager:
try:
return WidgetsView(self)
except Exception as e:
print(f"[PerfectUX] Failed to create WidgetsView: {e}")
# Fallback to placeholder
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(32, 32, 32, 32)
c = get_all_colors()
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 in-game use.")
subtitle.setStyleSheet(f"color: {c['text_secondary']}; font-size: 14px;")
layout.addWidget(subtitle)
layout.addStretch()
return container
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)