EU-Utility/core/overlay_widgets.py

511 lines
16 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 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)