EU-Utility/core/floating_icon.py

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