340 lines
11 KiB
Python
340 lines
11 KiB
Python
"""
|
|
EU-Utility - Enhanced Floating Icon
|
|
|
|
Features:
|
|
- EU game aesthetic matching
|
|
- Smooth animations
|
|
- Accessibility support
|
|
- Customizable appearance
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QLabel, QApplication,
|
|
QGraphicsDropShadowEffect, QGraphicsOpacityEffect
|
|
)
|
|
from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QPropertyAnimation, QEasingCurve, QTimer
|
|
from PyQt6.QtGui import QMouseEvent, QEnterEvent, QColor, QPixmap, QPainter, QCursor
|
|
from PyQt6.QtSvg import QSvgRenderer
|
|
|
|
from core.eu_styles import (
|
|
get_color, get_all_colors, EU_SIZES, AnimationHelper, EUTheme
|
|
)
|
|
|
|
|
|
class FloatingIcon(QWidget):
|
|
"""
|
|
Draggable floating icon with EU game styling.
|
|
Features smooth animations and accessibility support.
|
|
"""
|
|
|
|
clicked = pyqtSignal()
|
|
double_clicked = pyqtSignal()
|
|
|
|
ICONS_DIR = Path(__file__).parent.parent / "assets" / "icons"
|
|
|
|
# Size presets
|
|
SIZE_SMALL = 32
|
|
SIZE_MEDIUM = 40
|
|
SIZE_LARGE = 48
|
|
|
|
def __init__(self, parent=None, size: int = SIZE_MEDIUM, icon_name: str = "target"):
|
|
super().__init__(parent)
|
|
|
|
self.size = size
|
|
self.icon_name = icon_name
|
|
self._is_dragging = False
|
|
self._drag_position = QPoint()
|
|
self._click_threshold = 5
|
|
self._click_start_pos = QPoint()
|
|
self._hovered = False
|
|
|
|
# Animation properties
|
|
self._pulse_animation = None
|
|
self._scale_animation = None
|
|
self._glow_animation = None
|
|
|
|
self._setup_window()
|
|
self._setup_ui()
|
|
self._setup_animations()
|
|
self._setup_accessibility()
|
|
|
|
# Position near top-left game icons by default
|
|
self._set_default_position()
|
|
|
|
def _setup_window(self):
|
|
"""Configure window properties."""
|
|
self.setWindowFlags(
|
|
Qt.WindowType.FramelessWindowHint |
|
|
Qt.WindowType.WindowStaysOnTopHint |
|
|
Qt.WindowType.Tool
|
|
)
|
|
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
self.setFixedSize(self.size, self.size)
|
|
|
|
# Enable mouse tracking for hover effects
|
|
self.setMouseTracking(True)
|
|
|
|
def _setup_ui(self):
|
|
"""Setup the icon UI."""
|
|
c = get_all_colors()
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
self.icon_label = QLabel()
|
|
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.icon_label.setFixedSize(self.size, self.size)
|
|
|
|
# Load icon
|
|
self._load_icon()
|
|
|
|
# Apply EU styling with glass morphism effect
|
|
self._update_style()
|
|
|
|
# Add glow effect
|
|
self._setup_glow_effect()
|
|
|
|
layout.addWidget(self.icon_label)
|
|
|
|
def _load_icon(self):
|
|
"""Load the icon image."""
|
|
svg_path = self.ICONS_DIR / f"{self.icon_name}.svg"
|
|
|
|
if svg_path.exists():
|
|
renderer = QSvgRenderer(str(svg_path))
|
|
icon_size = int(self.size * 0.55)
|
|
pixmap = QPixmap(icon_size, icon_size)
|
|
pixmap.fill(Qt.GlobalColor.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
renderer.render(painter)
|
|
painter.end()
|
|
|
|
self.icon_label.setPixmap(pixmap)
|
|
else:
|
|
# Fallback to text icon
|
|
self.icon_label.setText("◆")
|
|
self.icon_label.setStyleSheet(f"""
|
|
color: {get_color('accent_orange')};
|
|
font-size: {self.size // 2}px;
|
|
font-weight: bold;
|
|
""")
|
|
|
|
def _update_style(self):
|
|
"""Update the icon styling."""
|
|
c = get_all_colors()
|
|
|
|
if self._hovered:
|
|
# Hover state - brighter with accent border
|
|
self.icon_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {c['bg_secondary']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
border: 2px solid {c['accent_orange']};
|
|
}}
|
|
""")
|
|
else:
|
|
# Normal state - subtle glass effect
|
|
self.icon_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {c['bg_secondary']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
border: 1px solid {c['border_default']};
|
|
}}
|
|
""")
|
|
|
|
def _setup_glow_effect(self):
|
|
"""Setup the glow shadow effect."""
|
|
c = get_all_colors()
|
|
|
|
self.shadow_effect = QGraphicsDropShadowEffect()
|
|
self.shadow_effect.setBlurRadius(20)
|
|
self.shadow_effect.setColor(QColor(c['accent_orange'].replace('#', '')))
|
|
self.shadow_effect.setColor(QColor(255, 140, 66, 60))
|
|
self.shadow_effect.setOffset(0, 4)
|
|
|
|
self.icon_label.setGraphicsEffect(self.shadow_effect)
|
|
|
|
def _setup_animations(self):
|
|
"""Setup hover and interaction animations."""
|
|
# Scale animation for hover
|
|
self._scale_anim = QPropertyAnimation(self, b"geometry")
|
|
self._scale_anim.setDuration(150)
|
|
self._scale_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
|
|
|
# Opacity animation for click feedback
|
|
self._opacity_effect = QGraphicsOpacityEffect(self)
|
|
self.setGraphicsEffect(self._opacity_effect)
|
|
|
|
self._opacity_anim = QPropertyAnimation(self._opacity_effect, b"opacity")
|
|
self._opacity_anim.setDuration(100)
|
|
|
|
def _setup_accessibility(self):
|
|
"""Setup accessibility features."""
|
|
self.setAccessibleName("EU-Utility Floating Icon")
|
|
self.setAccessibleDescription(
|
|
"Click to open the EU-Utility overlay. "
|
|
"Drag to reposition. Double-click for quick actions."
|
|
)
|
|
self.setToolTip("EU-Utility (Click to open, drag to move)")
|
|
|
|
def _set_default_position(self):
|
|
"""Set default position on screen."""
|
|
screen = QApplication.primaryScreen().geometry()
|
|
# Position near top-left but not overlapping system icons
|
|
x = min(250, screen.width() - self.size - 20)
|
|
y = 20
|
|
self.move(x, y)
|
|
|
|
def _animate_click(self):
|
|
"""Animate click feedback."""
|
|
self._opacity_anim.setStartValue(1.0)
|
|
self._opacity_anim.setEndValue(0.7)
|
|
self._opacity_anim.finished.connect(self._animate_click_restore)
|
|
self._opacity_anim.start()
|
|
|
|
def _animate_click_restore(self):
|
|
"""Restore opacity after click."""
|
|
self._opacity_anim.setStartValue(0.7)
|
|
self._opacity_anim.setEndValue(1.0)
|
|
self._opacity_anim.start()
|
|
|
|
def _start_pulse(self):
|
|
"""Start pulsing animation for attention."""
|
|
self._pulse_animation = AnimationHelper.pulse(self, 1500)
|
|
self._pulse_animation.start()
|
|
|
|
def _stop_pulse(self):
|
|
"""Stop pulsing animation."""
|
|
if self._pulse_animation:
|
|
self._pulse_animation.stop()
|
|
self._opacity_effect.setOpacity(1.0)
|
|
|
|
def mousePressEvent(self, event: QMouseEvent):
|
|
"""Handle mouse press."""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
self._is_dragging = True
|
|
self._click_start_pos = event.globalPosition().toPoint()
|
|
self._drag_position = self._click_start_pos - self.frameGeometry().topLeft()
|
|
event.accept()
|
|
|
|
def mouseMoveEvent(self, event: QMouseEvent):
|
|
"""Handle mouse drag."""
|
|
if self._is_dragging:
|
|
new_pos = event.globalPosition().toPoint() - self._drag_position
|
|
|
|
# Keep within screen bounds
|
|
screen = QApplication.primaryScreen().geometry()
|
|
new_pos.setX(max(0, min(new_pos.x(), screen.width() - self.size)))
|
|
new_pos.setY(max(0, min(new_pos.y(), screen.height() - self.size)))
|
|
|
|
self.move(new_pos)
|
|
event.accept()
|
|
|
|
def mouseReleaseEvent(self, event: QMouseEvent):
|
|
"""Handle mouse release."""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
release_pos = event.globalPosition().toPoint()
|
|
distance = (release_pos - self._click_start_pos).manhattanLength()
|
|
|
|
self._is_dragging = False
|
|
|
|
if distance < self._click_threshold:
|
|
self._animate_click()
|
|
self.clicked.emit()
|
|
|
|
event.accept()
|
|
|
|
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
|
"""Handle double click."""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
self.double_clicked.emit()
|
|
event.accept()
|
|
|
|
def enterEvent(self, event: QEnterEvent):
|
|
"""Handle mouse enter."""
|
|
self._hovered = True
|
|
self._update_style()
|
|
|
|
# Enhance glow on hover
|
|
self.shadow_effect.setBlurRadius(30)
|
|
self.shadow_effect.setColor(QColor(255, 140, 66, 100))
|
|
|
|
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
super().enterEvent(event)
|
|
|
|
def leaveEvent(self, event):
|
|
"""Handle mouse leave."""
|
|
self._hovered = False
|
|
self._update_style()
|
|
|
|
# Reset glow
|
|
self.shadow_effect.setBlurRadius(20)
|
|
self.shadow_effect.setColor(QColor(255, 140, 66, 60))
|
|
|
|
self.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
|
|
super().leaveEvent(event)
|
|
|
|
def set_icon(self, icon_name: str):
|
|
"""Change the icon."""
|
|
self.icon_name = icon_name
|
|
self._load_icon()
|
|
|
|
def set_size(self, size: int):
|
|
"""Change the icon size."""
|
|
self.size = size
|
|
self.setFixedSize(size, size)
|
|
self.icon_label.setFixedSize(size, size)
|
|
self._load_icon()
|
|
|
|
def show_notification_badge(self, count: int = 1):
|
|
"""Show a notification badge on the icon."""
|
|
# This could be implemented with a small overlay label
|
|
pass
|
|
|
|
def hide_notification_badge(self):
|
|
"""Hide the notification badge."""
|
|
pass
|
|
|
|
def refresh_theme(self):
|
|
"""Refresh appearance when theme changes."""
|
|
self._update_style()
|
|
self._setup_glow_effect()
|
|
|
|
|
|
class FloatingIconManager:
|
|
"""Manager for multiple floating icons."""
|
|
|
|
def __init__(self):
|
|
self.icons: list[FloatingIcon] = []
|
|
|
|
def create_icon(self, icon_name: str = "target", size: int = FloatingIcon.SIZE_MEDIUM) -> FloatingIcon:
|
|
"""Create a new floating icon."""
|
|
icon = FloatingIcon(size=size, icon_name=icon_name)
|
|
self.icons.append(icon)
|
|
return icon
|
|
|
|
def show_all(self):
|
|
"""Show all floating icons."""
|
|
for icon in self.icons:
|
|
icon.show()
|
|
|
|
def hide_all(self):
|
|
"""Hide all floating icons."""
|
|
for icon in self.icons:
|
|
icon.hide()
|
|
|
|
def refresh_all_themes(self):
|
|
"""Refresh theme for all icons."""
|
|
for icon in self.icons:
|
|
icon.refresh_theme()
|
|
|
|
def clear(self):
|
|
"""Remove all icons."""
|
|
for icon in self.icons:
|
|
icon.deleteLater()
|
|
self.icons.clear()
|