544 lines
18 KiB
Python
544 lines
18 KiB
Python
"""
|
||
EU-Utility - Overlay Window
|
||
|
||
Clean, game-like overlay with icon view and list view options.
|
||
No emojis - only actual icons.
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
try:
|
||
from PyQt6.QtWidgets import (
|
||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||
QLabel, QPushButton, QStackedWidget, QSystemTrayIcon,
|
||
QMenu, QApplication, QFrame, QGraphicsDropShadowEffect,
|
||
QListWidget, QListWidgetItem, QButtonGroup
|
||
)
|
||
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSize
|
||
from PyQt6.QtGui import QAction, QIcon, QColor, QFont
|
||
PYQT6_AVAILABLE = True
|
||
except ImportError:
|
||
PYQT6_AVAILABLE = False
|
||
print("PyQt6 not available. Install with: pip install PyQt6")
|
||
|
||
from core.eu_styles import EU_COLORS, EU_STYLES
|
||
from core.icon_manager import get_icon_manager, get_plugin_icon_name
|
||
|
||
|
||
class OverlayWindow(QMainWindow):
|
||
"""Clean EU-styled overlay window with view toggle."""
|
||
|
||
visibility_changed = pyqtSignal(bool)
|
||
|
||
def __init__(self, plugin_manager=None):
|
||
super().__init__()
|
||
|
||
if not PYQT6_AVAILABLE:
|
||
raise ImportError("PyQt6 is required")
|
||
|
||
self.plugin_manager = plugin_manager
|
||
self.is_visible = False
|
||
self.plugin_buttons = []
|
||
self.plugin_list_items = []
|
||
self.icon_manager = get_icon_manager()
|
||
self.view_mode = "icons" # "icons" or "list"
|
||
|
||
self._setup_window()
|
||
self._setup_ui()
|
||
self._setup_tray()
|
||
|
||
self.hide_overlay()
|
||
|
||
def _setup_window(self):
|
||
"""Configure window with EU styling - shows in taskbar."""
|
||
self.setWindowTitle("EU-Utility")
|
||
|
||
# Frameless, but NOT Tool (so it shows in taskbar)
|
||
# WindowStaysOnTopHint makes it stay on top without hiding from taskbar
|
||
self.setWindowFlags(
|
||
Qt.WindowType.FramelessWindowHint |
|
||
Qt.WindowType.WindowStaysOnTopHint
|
||
)
|
||
|
||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||
|
||
# Clean, game-like size
|
||
self.resize(850, 600)
|
||
self._center_window()
|
||
|
||
def _center_window(self):
|
||
"""Center window on screen."""
|
||
screen = QApplication.primaryScreen().geometry()
|
||
x = (screen.width() - self.width()) // 2
|
||
y = (screen.height() - self.height()) // 3
|
||
self.move(x, y)
|
||
|
||
def _setup_ui(self):
|
||
"""Setup clean EU-styled UI."""
|
||
central = QWidget()
|
||
self.setCentralWidget(central)
|
||
|
||
layout = QVBoxLayout(central)
|
||
layout.setContentsMargins(20, 20, 20, 20)
|
||
layout.setSpacing(0)
|
||
|
||
# Main container with orange window border like Skills window
|
||
self.container = QFrame()
|
||
self.container.setObjectName("euContainer")
|
||
self.container.setStyleSheet(f"""
|
||
#euContainer {{
|
||
background-color: {EU_COLORS['bg_dark']};
|
||
border: 1px solid {EU_COLORS['border_window']};
|
||
border-radius: 8px;
|
||
}}
|
||
""")
|
||
|
||
shadow = QGraphicsDropShadowEffect()
|
||
shadow.setBlurRadius(30)
|
||
shadow.setColor(QColor(0, 0, 0, 150))
|
||
shadow.setOffset(0, 8)
|
||
self.container.setGraphicsEffect(shadow)
|
||
|
||
container_layout = QVBoxLayout(self.container)
|
||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||
container_layout.setSpacing(0)
|
||
|
||
# Header with centered title - CLEAN (no weird lines)
|
||
header = QWidget()
|
||
header.setObjectName("header")
|
||
header.setStyleSheet(f"""
|
||
QWidget {{
|
||
background-color: {EU_COLORS['bg_header']};
|
||
border-top-left-radius: 8px;
|
||
border-top-right-radius: 8px;
|
||
border-bottom: 1px solid {EU_COLORS['border_medium']};
|
||
}}
|
||
""")
|
||
header_layout = QHBoxLayout(header)
|
||
header_layout.setContentsMargins(15, 12, 15, 12)
|
||
header_layout.setSpacing(10)
|
||
|
||
# App icon
|
||
app_icon = QLabel()
|
||
app_icon_pixmap = self.icon_manager.get_pixmap("target", size=20)
|
||
app_icon.setPixmap(app_icon_pixmap)
|
||
app_icon.setFixedSize(20, 20)
|
||
header_layout.addWidget(app_icon)
|
||
|
||
# Centered title
|
||
title = QLabel("EU-UTILITY")
|
||
title.setStyleSheet(f"""
|
||
color: {EU_COLORS['text_primary']};
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
letter-spacing: 2px;
|
||
""")
|
||
header_layout.addWidget(title)
|
||
|
||
header_layout.addStretch()
|
||
|
||
# View toggle buttons
|
||
view_group = QButtonGroup(self)
|
||
view_group.setExclusive(True)
|
||
|
||
self.icon_view_btn = QPushButton("Grid")
|
||
self.icon_view_btn.setCheckable(True)
|
||
self.icon_view_btn.setChecked(True)
|
||
self.icon_view_btn.setFixedSize(50, 24)
|
||
self.icon_view_btn.setStyleSheet(self._view_toggle_style())
|
||
self.icon_view_btn.clicked.connect(lambda: self._set_view_mode("icons"))
|
||
header_layout.addWidget(self.icon_view_btn)
|
||
|
||
self.list_view_btn = QPushButton("List")
|
||
self.list_view_btn.setCheckable(True)
|
||
self.list_view_btn.setFixedSize(50, 24)
|
||
self.list_view_btn.setStyleSheet(self._view_toggle_style())
|
||
self.list_view_btn.clicked.connect(lambda: self._set_view_mode("list"))
|
||
header_layout.addWidget(self.list_view_btn)
|
||
|
||
view_group.addButton(self.icon_view_btn)
|
||
view_group.addButton(self.list_view_btn)
|
||
|
||
# Close button
|
||
close_btn = QPushButton("×")
|
||
close_btn.setFixedSize(24, 24)
|
||
close_btn.setStyleSheet(f"""
|
||
QPushButton {{
|
||
background-color: transparent;
|
||
color: {EU_COLORS['text_muted']};
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
border: none;
|
||
border-radius: 4px;
|
||
}}
|
||
QPushButton:hover {{
|
||
background-color: rgba(244, 67, 54, 150);
|
||
color: white;
|
||
}}
|
||
""")
|
||
close_btn.clicked.connect(self.hide_overlay)
|
||
header_layout.addWidget(close_btn)
|
||
|
||
# Make header draggable
|
||
header.mousePressEvent = self._header_mouse_press
|
||
header.mouseMoveEvent = self._header_mouse_move
|
||
|
||
container_layout.addWidget(header)
|
||
|
||
# Content area with sidebar and main content
|
||
content_split = QHBoxLayout()
|
||
content_split.setSpacing(0)
|
||
content_split.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# Sidebar (plugin selector)
|
||
self.sidebar = QWidget()
|
||
self.sidebar.setFixedWidth(200)
|
||
self.sidebar.setStyleSheet(f"""
|
||
QWidget {{
|
||
background-color: rgba(20, 25, 35, 150);
|
||
border-right: 1px solid {EU_COLORS['border_medium']};
|
||
}}
|
||
""")
|
||
self._setup_sidebar()
|
||
content_split.addWidget(self.sidebar)
|
||
|
||
# Main content
|
||
self.content_area = QWidget()
|
||
self.content_area.setStyleSheet("background: transparent;")
|
||
content_layout = QVBoxLayout(self.content_area)
|
||
content_layout.setContentsMargins(20, 20, 20, 20)
|
||
content_layout.setSpacing(15)
|
||
|
||
# Plugin stack
|
||
self.plugin_stack = QStackedWidget()
|
||
self.plugin_stack.setStyleSheet("background: transparent;")
|
||
content_layout.addWidget(self.plugin_stack, 1)
|
||
|
||
content_split.addWidget(self.content_area, 1)
|
||
container_layout.addLayout(content_split, 1)
|
||
|
||
layout.addWidget(self.container)
|
||
|
||
# Load plugins
|
||
if self.plugin_manager:
|
||
self._load_plugins()
|
||
|
||
def _setup_sidebar(self):
|
||
"""Setup sidebar with scroll area to prevent expanding out of screen."""
|
||
from PyQt6.QtWidgets import QScrollArea
|
||
|
||
# Create scroll area
|
||
scroll = QScrollArea()
|
||
scroll.setWidgetResizable(True)
|
||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
scroll.setStyleSheet(f"""
|
||
QScrollArea {{
|
||
background: transparent;
|
||
border: none;
|
||
}}
|
||
QScrollBar:vertical {{
|
||
background: rgba(0, 0, 0, 50);
|
||
width: 8px;
|
||
border-radius: 4px;
|
||
}}
|
||
QScrollBar::handle:vertical {{
|
||
background: rgba(255, 255, 255, 30);
|
||
border-radius: 4px;
|
||
min-height: 30px;
|
||
}}
|
||
""")
|
||
|
||
# Create container widget for scroll area
|
||
container = QWidget()
|
||
sidebar_layout = QVBoxLayout(container)
|
||
sidebar_layout.setContentsMargins(0, 0, 0, 0)
|
||
sidebar_layout.setSpacing(0)
|
||
|
||
# Category label
|
||
plugins_label = QLabel("PLUGINS")
|
||
plugins_label.setStyleSheet(f"""
|
||
color: {EU_COLORS['text_muted']};
|
||
font-size: 10px;
|
||
font-weight: bold;
|
||
padding: 15px;
|
||
border-bottom: 1px solid {EU_COLORS['border_subtle']};
|
||
""")
|
||
sidebar_layout.addWidget(plugins_label)
|
||
|
||
# Plugin list (for list view)
|
||
self.plugin_list = QListWidget()
|
||
self.plugin_list.setFrameShape(QFrame.Shape.NoFrame)
|
||
self.plugin_list.setStyleSheet(f"""
|
||
QListWidget {{
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
}}
|
||
QListWidget::item {{
|
||
color: {EU_COLORS['text_secondary']};
|
||
padding: 12px 15px;
|
||
border-left: 3px solid transparent;
|
||
border-bottom: 1px solid {EU_COLORS['border_subtle']};
|
||
}}
|
||
QListWidget::item:hover {{
|
||
background-color: {EU_COLORS['bg_hover']};
|
||
color: {EU_COLORS['text_primary']};
|
||
}}
|
||
QListWidget::item:selected {{
|
||
background-color: {EU_COLORS['bg_selected']};
|
||
color: white;
|
||
border-left: 3px solid {EU_COLORS['accent_orange']};
|
||
}}
|
||
""")
|
||
self.plugin_list.itemClicked.connect(self._on_list_item_clicked)
|
||
sidebar_layout.addWidget(self.plugin_list)
|
||
|
||
# Icon grid (for icon view)
|
||
self.icon_grid = QWidget()
|
||
self.icon_grid.setStyleSheet("background: transparent;")
|
||
self.icon_grid_layout = QVBoxLayout(self.icon_grid)
|
||
self.icon_grid_layout.setSpacing(0)
|
||
self.icon_grid_layout.setContentsMargins(0, 0, 0, 0)
|
||
sidebar_layout.addWidget(self.icon_grid)
|
||
|
||
# Set container as scroll widget
|
||
scroll.setWidget(container)
|
||
|
||
# Add scroll to sidebar layout
|
||
main_layout = QVBoxLayout(self.sidebar)
|
||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||
main_layout.addWidget(scroll)
|
||
|
||
# Initially show icon grid
|
||
self.plugin_list.hide()
|
||
|
||
def _view_toggle_style(self):
|
||
"""Get style for view toggle buttons."""
|
||
return f"""
|
||
QPushButton {{
|
||
background-color: rgba(60, 70, 90, 100);
|
||
color: {EU_COLORS['text_muted']};
|
||
border: 1px solid {EU_COLORS['border_subtle']};
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
}}
|
||
QPushButton:hover {{
|
||
background-color: rgba(80, 90, 110, 150);
|
||
}}
|
||
QPushButton:checked {{
|
||
background-color: {EU_COLORS['accent_orange']};
|
||
color: white;
|
||
border-color: {EU_COLORS['accent_orange']};
|
||
}}
|
||
"""
|
||
|
||
def _load_plugins(self):
|
||
"""Load plugins into sidebar and stack."""
|
||
for idx, (plugin_id, plugin) in enumerate(self.plugin_manager.get_all_plugins().items()):
|
||
# Get icon name
|
||
icon_name = get_plugin_icon_name(plugin.name)
|
||
|
||
# Add to list view
|
||
list_item = QListWidgetItem(plugin.name)
|
||
list_item.setData(Qt.ItemDataRole.UserRole, idx)
|
||
self.plugin_list.addItem(list_item)
|
||
|
||
# Add to icon grid
|
||
icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
|
||
self.icon_grid_layout.addWidget(icon_btn)
|
||
|
||
# Add plugin UI to stack
|
||
try:
|
||
plugin_ui = plugin.get_ui()
|
||
if plugin_ui:
|
||
plugin_ui.setStyleSheet("background: transparent;")
|
||
self.plugin_stack.addWidget(plugin_ui)
|
||
except Exception as e:
|
||
print(f"[Overlay] Error loading UI for {plugin.name}: {e}")
|
||
|
||
# Add stretch to icon grid
|
||
self.icon_grid_layout.addStretch()
|
||
|
||
# Select first plugin
|
||
if self.plugin_list.count() > 0:
|
||
self.plugin_list.setCurrentRow(0)
|
||
self.plugin_stack.setCurrentIndex(0)
|
||
|
||
def _create_icon_button(self, name, icon_name, index):
|
||
"""Create an icon button for the sidebar."""
|
||
btn = QPushButton()
|
||
btn.setFixedSize(180, 50)
|
||
btn.setCheckable(True)
|
||
btn.setStyleSheet(f"""
|
||
QPushButton {{
|
||
background-color: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
text-align: left;
|
||
padding: 5px 10px;
|
||
}}
|
||
QPushButton:hover {{
|
||
background-color: rgba(255, 255, 255, 10);
|
||
border-color: {EU_COLORS['border_subtle']};
|
||
}}
|
||
QPushButton:checked {{
|
||
background-color: rgba(255, 140, 66, 30);
|
||
border-color: {EU_COLORS['accent_orange']};
|
||
}}
|
||
""")
|
||
|
||
# Button layout
|
||
btn_layout = QHBoxLayout(btn)
|
||
btn_layout.setContentsMargins(10, 5, 10, 5)
|
||
btn_layout.setSpacing(10)
|
||
|
||
# Icon
|
||
icon_label = QLabel()
|
||
icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=20)
|
||
icon_label.setPixmap(icon_pixmap)
|
||
icon_label.setFixedSize(20, 20)
|
||
btn_layout.addWidget(icon_label)
|
||
|
||
# Text
|
||
text_label = QLabel(name)
|
||
text_label.setStyleSheet(f"color: {EU_COLORS['text_primary']}; font-size: 12px;")
|
||
btn_layout.addWidget(text_label)
|
||
btn_layout.addStretch()
|
||
|
||
btn.clicked.connect(lambda: self._on_icon_button_clicked(index, btn))
|
||
|
||
if index == 0:
|
||
btn.setChecked(True)
|
||
|
||
return btn
|
||
|
||
def _on_list_item_clicked(self, item):
|
||
"""Handle list item click."""
|
||
idx = item.data(Qt.ItemDataRole.UserRole)
|
||
self.plugin_stack.setCurrentIndex(idx)
|
||
|
||
def _on_icon_button_clicked(self, index, btn):
|
||
"""Handle icon button click."""
|
||
# Uncheck all other buttons
|
||
for i in range(self.icon_grid_layout.count() - 1): # Exclude stretch
|
||
widget = self.icon_grid_layout.itemAt(i).widget()
|
||
if widget and isinstance(widget, QPushButton) and widget != btn:
|
||
widget.setChecked(False)
|
||
|
||
btn.setChecked(True)
|
||
self.plugin_stack.setCurrentIndex(index)
|
||
|
||
def _set_view_mode(self, mode):
|
||
"""Switch between icon and list view."""
|
||
self.view_mode = mode
|
||
|
||
if mode == "icons":
|
||
self.plugin_list.hide()
|
||
self.icon_grid.show()
|
||
else:
|
||
self.icon_grid.hide()
|
||
self.plugin_list.show()
|
||
|
||
def _setup_tray(self):
|
||
"""Setup system tray."""
|
||
self.tray_icon = QSystemTrayIcon(self)
|
||
|
||
icon_path = Path("assets/icon.ico")
|
||
if icon_path.exists():
|
||
self.tray_icon.setIcon(QIcon(str(icon_path)))
|
||
|
||
tray_menu = QMenu()
|
||
tray_menu.setStyleSheet(f"""
|
||
QMenu {{
|
||
background-color: {EU_COLORS['bg_dark']};
|
||
color: white;
|
||
border: 1px solid {EU_COLORS['border_medium']};
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
}}
|
||
QMenu::item {{
|
||
padding: 8px 20px;
|
||
border-radius: 4px;
|
||
}}
|
||
QMenu::item:selected {{
|
||
background-color: {EU_COLORS['accent_orange']};
|
||
}}
|
||
""")
|
||
|
||
show_action = QAction("Show EU-Utility", self)
|
||
show_action.triggered.connect(self.show_overlay)
|
||
tray_menu.addAction(show_action)
|
||
|
||
tray_menu.addSeparator()
|
||
|
||
quit_action = QAction("Quit", self)
|
||
quit_action.triggered.connect(self.quit_app)
|
||
tray_menu.addAction(quit_action)
|
||
|
||
self.tray_icon.setContextMenu(tray_menu)
|
||
self.tray_icon.activated.connect(self._tray_activated)
|
||
self.tray_icon.show()
|
||
|
||
def _tray_activated(self, reason):
|
||
"""Handle tray activation."""
|
||
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
||
self.toggle_overlay()
|
||
|
||
# Drag functionality
|
||
def _header_mouse_press(self, event):
|
||
"""Start dragging from header."""
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self._dragging = True
|
||
self._drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||
event.accept()
|
||
|
||
def _header_mouse_move(self, event):
|
||
"""Drag window."""
|
||
if hasattr(self, '_dragging') and self._dragging:
|
||
self.move(event.globalPosition().toPoint() - self._drag_position)
|
||
event.accept()
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
"""Stop dragging."""
|
||
self._dragging = False
|
||
super().mouseReleaseEvent(event)
|
||
|
||
def show_overlay(self):
|
||
"""Show overlay."""
|
||
self.show()
|
||
self.raise_()
|
||
self.activateWindow()
|
||
self.is_visible = True
|
||
self.visibility_changed.emit(True)
|
||
|
||
def hide_overlay(self):
|
||
"""Hide overlay."""
|
||
self.hide()
|
||
self.is_visible = False
|
||
self.visibility_changed.emit(False)
|
||
|
||
def toggle_overlay(self):
|
||
"""Toggle overlay."""
|
||
if self.is_visible:
|
||
self.hide_overlay()
|
||
else:
|
||
self.show_overlay()
|
||
|
||
def quit_app(self):
|
||
"""Quit application."""
|
||
if self.plugin_manager:
|
||
self.plugin_manager.shutdown_all()
|
||
self.tray_icon.hide()
|
||
QApplication.quit()
|
||
|
||
def keyPressEvent(self, event):
|
||
"""Handle ESC key."""
|
||
if event.key() == Qt.Key.Key_Escape:
|
||
self.hide_overlay()
|
||
else:
|
||
super().keyPressEvent(event)
|