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