EU-Utility/projects/EU-Utility/core/overlay_window.py

544 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)