""" EU-Utility - Overlay Widget System Draggable, hideable overlay elements that appear in-game. """ from pathlib import Path import json import platform import subprocess from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QGraphicsDropShadowEffect, QApplication ) from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QTimer from PyQt6.QtGui import QColor, QMouseEvent from core.icon_manager import get_icon_manager class OverlayWidget(QFrame): """Base class for in-game overlay widgets.""" # Position persistence position_changed = pyqtSignal(QPoint) visibility_changed = pyqtSignal(bool) closed = pyqtSignal() def __init__(self, title="Overlay", icon_name="target", parent=None): super().__init__(parent) self.title = title self.icon_name = icon_name self.dragging = False self.drag_position = QPoint() self._visible = True self.icon_manager = get_icon_manager() # Frameless, always on top self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool | Qt.WindowType.WindowTransparentForInput ) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # EU game-style styling self.setStyleSheet(""" OverlayWidget { background-color: rgba(20, 25, 35, 220); border: 1px solid rgba(100, 150, 200, 60); border-radius: 8px; } """) # Shadow shadow = QGraphicsDropShadowEffect() shadow.setBlurRadius(15) shadow.setColor(QColor(0, 0, 0, 100)) shadow.setOffset(0, 2) self.setGraphicsEffect(shadow) self._setup_ui() def _setup_ui(self): """Setup widget UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Header bar (draggable) self.header = QWidget() self.header.setStyleSheet(""" QWidget { background-color: rgba(30, 40, 55, 200); border-top-left-radius: 8px; border-top-right-radius: 8px; border-bottom: 1px solid rgba(100, 150, 200, 40); } """) header_layout = QHBoxLayout(self.header) header_layout.setContentsMargins(8, 6, 8, 6) header_layout.setSpacing(6) # Icon icon_label = QLabel() icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=14) icon_label.setPixmap(icon_pixmap) icon_label.setFixedSize(14, 14) icon_label.setStyleSheet("background: transparent;") header_layout.addWidget(icon_label) # Title title_label = QLabel(self.title) title_label.setStyleSheet(""" color: rgba(255, 255, 255, 200); font-size: 11px; font-weight: bold; background: transparent; """) header_layout.addWidget(title_label) header_layout.addStretch() # Hide button hide_btn = QPushButton("−") hide_btn.setFixedSize(18, 18) hide_btn.setStyleSheet(""" QPushButton { background-color: transparent; color: rgba(255, 255, 255, 150); border: none; font-size: 14px; font-weight: bold; } QPushButton:hover { color: white; background-color: rgba(255, 255, 255, 20); border-radius: 3px; } """) hide_btn.clicked.connect(self.toggle_visibility) header_layout.addWidget(hide_btn) # Close button close_btn = QPushButton("×") close_btn.setFixedSize(18, 18) close_btn.setStyleSheet(""" QPushButton { background-color: transparent; color: rgba(255, 255, 255, 150); border: none; font-size: 14px; font-weight: bold; } QPushButton:hover { color: #f44336; background-color: rgba(255, 255, 255, 20); border-radius: 3px; } """) close_btn.clicked.connect(self.close_widget) header_layout.addWidget(close_btn) layout.addWidget(self.header) # Content area self.content = QWidget() self.content.setStyleSheet("background: transparent;") self.content_layout = QVBoxLayout(self.content) self.content_layout.setContentsMargins(10, 10, 10, 10) layout.addWidget(self.content) def mousePressEvent(self, event: QMouseEvent): """Start dragging from header.""" if event.button() == Qt.MouseButton.LeftButton: # Only drag from header if self.header.geometry().contains(event.pos()): self.dragging = True self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event: QMouseEvent): """Drag widget.""" if self.dragging: new_pos = event.globalPosition().toPoint() - self.drag_position self.move(new_pos) self.position_changed.emit(new_pos) event.accept() def mouseReleaseEvent(self, event: QMouseEvent): """Stop dragging.""" if event.button() == Qt.MouseButton.LeftButton: self.dragging = False event.accept() def toggle_visibility(self): """Toggle content visibility.""" self._visible = not self._visible self.content.setVisible(self._visible) self.visibility_changed.emit(self._visible) # Collapse widget when hidden if self._visible: self.resize(self.sizeHint().width(), self.sizeHint().height()) else: self.resize(self.width(), self.header.height() + 4) def close_widget(self): """Close and remove widget.""" self.closed.emit() self.close() def showEvent(self, event): """Widget shown.""" super().showEvent(event) self.visibility_changed.emit(True) def hideEvent(self, event): """Widget hidden.""" super().hideEvent(event) self.visibility_changed.emit(False) class SpotifyOverlayWidget(OverlayWidget): """Spotify player as in-game overlay.""" def __init__(self, parent=None): super().__init__(title="Now Playing", icon_name="music", parent=parent) self.setMinimumWidth(200) self._setup_content() def _setup_content(self): """Setup Spotify content.""" # Track name self.track_label = QLabel("Not Playing") self.track_label.setStyleSheet(""" color: #1db954; font-size: 12px; font-weight: bold; background: transparent; """) self.track_label.setWordWrap(True) self.content_layout.addWidget(self.track_label) # Artist self.artist_label = QLabel("-") self.artist_label.setStyleSheet(""" color: rgba(255, 255, 255, 150); font-size: 10px; background: transparent; """) self.content_layout.addWidget(self.artist_label) # Progress self.progress = QLabel("▬▬▬▬▬▬▬▬▬▬") self.progress.setStyleSheet("color: #1db954; font-size: 8px; background: transparent;") self.content_layout.addWidget(self.progress) # Controls controls = QHBoxLayout() controls.setSpacing(8) prev_btn = QPushButton("⏮") play_btn = QPushButton("▶") next_btn = QPushButton("⏭") for btn in [prev_btn, play_btn, next_btn]: btn.setFixedSize(28, 28) btn.setStyleSheet(""" QPushButton { background-color: rgba(255, 255, 255, 15); border: none; border-radius: 14px; color: white; font-size: 12px; } QPushButton:hover { background-color: rgba(255, 255, 255, 30); } """) controls.addWidget(btn) controls.addStretch() self.content_layout.addLayout(controls) def update_track(self, track_data): """Update track info.""" if track_data: self.track_label.setText(track_data.get('name', 'Unknown')) artists = track_data.get('artists', []) self.artist_label.setText(', '.join(artists) if artists else '-') class MissionTrackerWidget(OverlayWidget): """Mission progress tracker overlay.""" def __init__(self, parent=None): super().__init__(title="Mission Tracker", icon_name="map", parent=parent) self.setMinimumWidth(180) self._setup_content() def _setup_content(self): """Setup mission content.""" # Mission name self.mission_label = QLabel("No Active Mission") self.mission_label.setStyleSheet(""" color: #ffc107; font-size: 11px; font-weight: bold; background: transparent; """) self.mission_label.setWordWrap(True) self.content_layout.addWidget(self.mission_label) # Progress self.progress_label = QLabel("0 / 0") self.progress_label.setStyleSheet(""" color: rgba(255, 255, 255, 150); font-size: 10px; background: transparent; """) self.content_layout.addWidget(self.progress_label) # Progress bar self.progress_bar = QLabel("░░░░░░░░░░") self.progress_bar.setStyleSheet("color: #4caf50; font-size: 10px; background: transparent;") self.content_layout.addWidget(self.progress_bar) self.content_layout.addStretch() def update_mission(self, mission_data): """Update mission info.""" if mission_data: self.mission_label.setText(mission_data.get('name', 'Unknown')) current = mission_data.get('current', 0) total = mission_data.get('total', 0) self.progress_label.setText(f"{current} / {total}") # Simple progress bar pct = current / total if total > 0 else 0 filled = int(pct * 10) bar = "█" * filled + "░" * (10 - filled) self.progress_bar.setText(bar) class SkillGainWidget(OverlayWidget): """Recent skill gains overlay.""" def __init__(self, parent=None): super().__init__(title="Skill Gains", icon_name="trending-up", parent=parent) self.setMinimumWidth(150) self._setup_content() def _setup_content(self): """Setup skill gains content.""" self.gains_layout = QVBoxLayout() self.gains_layout.setSpacing(4) # Sample gain gain = QLabel("+5.2 Aim") gain.setStyleSheet(""" color: #4caf50; font-size: 11px; font-weight: bold; background: transparent; """) self.gains_layout.addWidget(gain) self.content_layout.addLayout(self.gains_layout) self.content_layout.addStretch() def add_gain(self, skill, points): """Add a skill gain notification.""" gain = QLabel(f"+{points} {skill}") gain.setStyleSheet(""" color: #4caf50; font-size: 11px; font-weight: bold; background: transparent; """) self.gains_layout.addWidget(gain) # Keep only last 5 while self.gains_layout.count() > 5: item = self.gains_layout.takeAt(0) if item.widget(): item.widget().deleteLater() class DPPTrackerWidget(OverlayWidget): """DPP (Damage Per PEC) tracker overlay.""" def __init__(self, parent=None): super().__init__(title="DPP Tracker", icon_name="target", parent=parent) self.setMinimumWidth(160) self._setup_content() def _setup_content(self): """Setup DPP content.""" # Current DPP self.dpp_label = QLabel("0.00") self.dpp_label.setStyleSheet(""" color: #00bcd4; font-size: 20px; font-weight: bold; background: transparent; """) self.content_layout.addWidget(self.dpp_label) # Label label = QLabel("DPP") label.setStyleSheet(""" color: rgba(255, 255, 255, 150); font-size: 9px; background: transparent; """) self.content_layout.addWidget(label) # Session stats self.session_label = QLabel("Session: 0.00") self.session_label.setStyleSheet(""" color: rgba(255, 255, 255, 120); font-size: 10px; background: transparent; """) self.content_layout.addWidget(self.session_label) self.content_layout.addStretch() def update_dpp(self, current, session_avg): """Update DPP values.""" self.dpp_label.setText(f"{current:.2f}") self.session_label.setText(f"Session: {session_avg:.2f}") class OverlayManager: """Manage all overlay widgets.""" def __init__(self, app=None): self.app = app self.widgets = {} self.positions_file = Path("data/overlay_positions.json") self._load_positions() def create_widget(self, widget_type, name, **kwargs): """Create and show an overlay widget.""" widget_classes = { 'spotify': SpotifyOverlayWidget, 'mission': MissionTrackerWidget, 'skillgain': SkillGainWidget, 'dpp': DPPTrackerWidget, } widget_class = widget_classes.get(widget_type) if not widget_class: return None widget = widget_class(**kwargs) # Restore position if name in self.positions: pos = self.positions[name] widget.move(pos['x'], pos['y']) else: # Default position - right side of screen screen = QApplication.primaryScreen().geometry() y_offset = len(self.widgets) * 100 + 100 widget.move(screen.width() - 250, y_offset) # Connect signals widget.position_changed.connect(lambda p: self._save_position(name, p)) widget.closed.connect(lambda: self._remove_widget(name)) widget.show() self.widgets[name] = widget return widget def _save_position(self, name, pos): """Save widget position.""" self.positions[name] = {'x': pos.x(), 'y': pos.y()} self._save_positions() def _remove_widget(self, name): """Remove widget from tracking.""" if name in self.widgets: del self.widgets[name] if name in self.positions: del self.positions[name] self._save_positions() def toggle_widget(self, name): """Toggle widget visibility.""" if name in self.widgets: widget = self.widgets[name] if widget.isVisible(): widget.hide() else: widget.show() def hide_all(self): """Hide all overlay widgets.""" for widget in self.widgets.values(): widget.hide() def show_all(self): """Show all overlay widgets.""" for widget in self.widgets.values(): widget.show() def _load_positions(self): """Load saved positions.""" self.positions = {} if self.positions_file.exists(): import json try: with open(self.positions_file, 'r') as f: self.positions = json.load(f) except: pass def _save_positions(self): """Save positions to file.""" import json self.positions_file.parent.mkdir(parents=True, exist_ok=True) with open(self.positions_file, 'w') as f: json.dump(self.positions, f)