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