feat: Integration update - Icons, List View, Cleaner UI
ICON SYSTEM: - Replaced emojis with actual SVG icons - New icon_manager.py with IconManager class - PLUGIN_ICONS mapping for all plugins - Default gear icon for plugins without icons - All icons are white SVG line icons NEW ICONS (14): grid, trending-up, package, pickaxe, award, book, dollar-sign, archive, message-square, map, navigation, shopping-bag, tool, box, zap ICON EXTRACTOR INTEGRATION: - New core/icon_extractor.py - TGAReader class for reading EU TGA cache files - IconCacheManager for extracting and caching icons - Supports RLE compressed TGA - Converts BGR to RGB UI IMPROVEMENTS: - Cleaner, more game-like layout - Sidebar with plugin selector (200px width) - View toggle: Grid view vs List view - No emojis anywhere - Better spacing and padding - EU-UTILITY title (no emoji) Total icons now: 32 SVG icons
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 265 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line></svg>
|
||||
|
After Width: | Height: | Size: 424 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>
|
||||
|
After Width: | Height: | Size: 268 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 16 6 1 6"></polygon><path d="M17 3a2 2 0 0 1 2 2v16l-4-3"></path></svg>
|
||||
|
After Width: | Height: | Size: 259 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||||
|
After Width: | Height: | Size: 359 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"></polygon></svg>
|
||||
|
After Width: | Height: | Size: 212 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path></svg>
|
||||
|
After Width: | Height: | Size: 230 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
|
||||
|
After Width: | Height: | Size: 305 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
|
||||
|
After Width: | Height: | Size: 224 B |
|
|
@ -0,0 +1,273 @@
|
|||
"""
|
||||
EU-Utility - Icon Extractor Integration
|
||||
|
||||
Integrates TGA icon extraction from Lemontropia Suite's Icon Extractor.
|
||||
Provides icon cache management and TGA to PNG conversion.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
from typing import Optional, Tuple
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
print("[IconExtractor] PIL not available. Install: pip install Pillow")
|
||||
|
||||
|
||||
class TGAReader:
|
||||
"""Read TGA files from Entropia Universe cache."""
|
||||
|
||||
TGA_TYPES = {
|
||||
0: 'NO_IMAGE',
|
||||
1: 'COLOR_MAPPED',
|
||||
2: 'RGB',
|
||||
3: 'GRAYSCALE',
|
||||
9: 'RLE_COLOR_MAPPED',
|
||||
10: 'RLE_RGB',
|
||||
11: 'RLE_GRAYSCALE',
|
||||
}
|
||||
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self.header = None
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.pixels = None
|
||||
|
||||
def read(self) -> Optional[Image.Image]:
|
||||
"""Read TGA file and return PIL Image."""
|
||||
if not PIL_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(self.file_path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
return self._parse_tga(data)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TGAReader] Error reading {self.file_path}: {e}")
|
||||
return None
|
||||
|
||||
def _parse_tga(self, data: bytes) -> Optional[Image.Image]:
|
||||
"""Parse TGA data."""
|
||||
if len(data) < 18:
|
||||
return None
|
||||
|
||||
# Read header
|
||||
id_length = data[0]
|
||||
color_map_type = data[1]
|
||||
image_type = data[2]
|
||||
|
||||
# Color map spec (5 bytes)
|
||||
color_map_origin = struct.unpack('<H', data[3:5])[0]
|
||||
color_map_length = struct.unpack('<H', data[5:7])[0]
|
||||
color_map_entry_size = data[7]
|
||||
|
||||
# Image spec (10 bytes)
|
||||
x_origin = struct.unpack('<H', data[8:10])[0]
|
||||
y_origin = struct.unpack('<H', data[10:12])[0]
|
||||
self.width = struct.unpack('<H', data[12:14])[0]
|
||||
self.height = struct.unpack('<H', data[14:16])[0]
|
||||
pixel_depth = data[16]
|
||||
descriptor = data[17]
|
||||
|
||||
# Skip ID and color map
|
||||
offset = 18 + id_length
|
||||
if color_map_type == 1:
|
||||
offset += color_map_length * (color_map_entry_size // 8)
|
||||
|
||||
# Read pixel data
|
||||
pixel_data = data[offset:]
|
||||
|
||||
# Create image based on type
|
||||
if image_type in (2, 10): # RGB or RLE RGB
|
||||
return self._read_rgb(pixel_data, pixel_depth, image_type == 10)
|
||||
elif image_type in (3, 11): # Grayscale
|
||||
return self._read_grayscale(pixel_data, image_type == 11)
|
||||
|
||||
return None
|
||||
|
||||
def _read_rgb(self, data: bytes, depth: int, rle: bool) -> Optional[Image.Image]:
|
||||
"""Read RGB pixel data."""
|
||||
if depth not in (24, 32):
|
||||
return None
|
||||
|
||||
bytes_per_pixel = depth // 8
|
||||
expected_size = self.width * self.height * bytes_per_pixel
|
||||
|
||||
if rle:
|
||||
data = self._decode_rle(data, bytes_per_pixel)
|
||||
|
||||
if len(data) < expected_size:
|
||||
return None
|
||||
|
||||
# Convert BGR to RGB
|
||||
if bytes_per_pixel == 3:
|
||||
mode = 'RGB'
|
||||
pixels = bytearray()
|
||||
for i in range(0, len(data), 3):
|
||||
if i + 2 < len(data):
|
||||
pixels.extend([data[i+2], data[i+1], data[i]]) # BGR to RGB
|
||||
else:
|
||||
mode = 'RGBA'
|
||||
pixels = bytearray()
|
||||
for i in range(0, len(data), 4):
|
||||
if i + 3 < len(data):
|
||||
pixels.extend([data[i+2], data[i+1], data[i], data[i+3]]) # BGRA to RGBA
|
||||
|
||||
try:
|
||||
img = Image.frombytes(mode, (self.width, self.height), bytes(pixels))
|
||||
# Flip vertically (TGA stores bottom-to-top)
|
||||
img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||
return img
|
||||
except Exception as e:
|
||||
print(f"[TGAReader] Error creating image: {e}")
|
||||
return None
|
||||
|
||||
def _read_grayscale(self, data: bytes, rle: bool) -> Optional[Image.Image]:
|
||||
"""Read grayscale pixel data."""
|
||||
expected_size = self.width * self.height
|
||||
|
||||
if rle:
|
||||
data = self._decode_rle(data, 1)
|
||||
|
||||
if len(data) < expected_size:
|
||||
return None
|
||||
|
||||
try:
|
||||
img = Image.frombytes('L', (self.width, self.height), data[:expected_size])
|
||||
img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||
return img
|
||||
except Exception as e:
|
||||
print(f"[TGAReader] Error creating grayscale image: {e}")
|
||||
return None
|
||||
|
||||
def _decode_rle(self, data: bytes, bytes_per_pixel: int) -> bytes:
|
||||
"""Decode RLE compressed data."""
|
||||
result = bytearray()
|
||||
i = 0
|
||||
|
||||
while i < len(data):
|
||||
header = data[i]
|
||||
i += 1
|
||||
|
||||
if header >= 128: # Run-length packet
|
||||
count = (header - 128) + 1
|
||||
pixel = data[i:i + bytes_per_pixel]
|
||||
i += bytes_per_pixel
|
||||
for _ in range(count):
|
||||
result.extend(pixel)
|
||||
else: # Raw packet
|
||||
count = header + 1
|
||||
for _ in range(count):
|
||||
result.extend(data[i:i + bytes_per_pixel])
|
||||
i += bytes_per_pixel
|
||||
|
||||
return bytes(result)
|
||||
|
||||
|
||||
class IconCacheManager:
|
||||
"""Manages EU icon cache extraction and storage."""
|
||||
|
||||
def __init__(self, cache_dir: str = "assets/eu_icons"):
|
||||
self.cache_dir = Path(cache_dir)
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.icon_index = {}
|
||||
self._load_index()
|
||||
|
||||
def _load_index(self):
|
||||
"""Load icon index."""
|
||||
index_file = self.cache_dir / "index.json"
|
||||
if index_file.exists():
|
||||
try:
|
||||
import json
|
||||
with open(index_file, 'r') as f:
|
||||
self.icon_index = json.load(f)
|
||||
except:
|
||||
self.icon_index = {}
|
||||
|
||||
def _save_index(self):
|
||||
"""Save icon index."""
|
||||
import json
|
||||
index_file = self.cache_dir / "index.json"
|
||||
with open(index_file, 'w') as f:
|
||||
json.dump(self.icon_index, f, indent=2)
|
||||
|
||||
def extract_from_tga(self, tga_path: Path, icon_name: str) -> Optional[Path]:
|
||||
"""Extract icon from TGA file and save as PNG."""
|
||||
if not PIL_AVAILABLE:
|
||||
return None
|
||||
|
||||
reader = TGAReader(tga_path)
|
||||
img = reader.read()
|
||||
|
||||
if img:
|
||||
# Save as PNG
|
||||
output_path = self.cache_dir / f"{icon_name}.png"
|
||||
img.save(output_path, 'PNG')
|
||||
|
||||
# Update index
|
||||
self.icon_index[icon_name] = {
|
||||
'source': str(tga_path),
|
||||
'size': (reader.width, reader.height),
|
||||
'cached': str(output_path)
|
||||
}
|
||||
self._save_index()
|
||||
|
||||
return output_path
|
||||
|
||||
return None
|
||||
|
||||
def get_icon(self, icon_name: str, size: Tuple[int, int] = (32, 32)) -> Optional[Path]:
|
||||
"""Get icon path, extracting from TGA if needed."""
|
||||
# Check cache
|
||||
cached = self.cache_dir / f"{icon_name}.png"
|
||||
if cached.exists():
|
||||
return cached
|
||||
|
||||
# Check if we have source TGA
|
||||
if icon_name in self.icon_index:
|
||||
source = Path(self.icon_index[icon_name]['source'])
|
||||
if source.exists():
|
||||
return self.extract_from_tga(source, icon_name)
|
||||
|
||||
return None
|
||||
|
||||
def scan_cache_directory(self, eu_cache_path: Path):
|
||||
"""Scan EU cache directory for TGA icons."""
|
||||
if not eu_cache_path.exists():
|
||||
return
|
||||
|
||||
print(f"[IconCache] Scanning {eu_cache_path}...")
|
||||
|
||||
found = 0
|
||||
for tga_file in eu_cache_path.rglob("*.tga"):
|
||||
# Try to extract
|
||||
icon_name = tga_file.stem
|
||||
if self.extract_from_tga(tga_file, icon_name):
|
||||
found += 1
|
||||
|
||||
print(f"[IconCache] Extracted {found} icons")
|
||||
|
||||
def list_icons(self) -> list:
|
||||
"""List all available cached icons."""
|
||||
icons = []
|
||||
for png_file in self.cache_dir.glob("*.png"):
|
||||
icons.append(png_file.stem)
|
||||
return sorted(icons)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_icon_cache = None
|
||||
|
||||
def get_icon_cache() -> IconCacheManager:
|
||||
"""Get global icon cache instance."""
|
||||
global _icon_cache
|
||||
if _icon_cache is None:
|
||||
_icon_cache = IconCacheManager()
|
||||
return _icon_cache
|
||||
|
|
@ -1,21 +1,24 @@
|
|||
"""
|
||||
EU-Utility - Icon Manager
|
||||
|
||||
Loads and manages white/frosted style icons for EU aesthetic.
|
||||
Icons are SVG format - scalable and crisp at any size.
|
||||
Manages actual icon files (SVG/PNG) - NO emojis.
|
||||
Integrates with Icon Extractor for TGA conversion.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PyQt6.QtGui import QIcon, QPixmap, QColor
|
||||
from PyQt6.QtCore import QSize, Qt
|
||||
from PyQt6.QtGui import QIcon, QPixmap, QPainter
|
||||
from PyQt6.QtCore import Qt, QSize
|
||||
from PyQt6.QtSvg import QSvgRenderer
|
||||
|
||||
|
||||
class IconManager:
|
||||
"""Manage application icons with EU styling."""
|
||||
"""Manages icons for EU-Utility - no emojis, only real icons."""
|
||||
|
||||
_instance = None
|
||||
|
||||
# Default icon fallback (gear/settings icon)
|
||||
DEFAULT_ICON = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>"""
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
|
|
@ -27,91 +30,137 @@ class IconManager:
|
|||
return
|
||||
|
||||
self.icons_dir = Path(icons_dir)
|
||||
self.svg_icons = {}
|
||||
self._load_svg_icons()
|
||||
self.svg_cache = {}
|
||||
self._initialized = True
|
||||
|
||||
def _load_svg_icons(self):
|
||||
"""Load SVG icons."""
|
||||
if not self.icons_dir.exists():
|
||||
return
|
||||
def get_icon(self, name, size=24, color="white"):
|
||||
"""Get icon by name. Returns QIcon."""
|
||||
# Try SVG file first
|
||||
svg_path = self.icons_dir / f"{name}.svg"
|
||||
if svg_path.exists():
|
||||
return self._svg_to_icon(svg_path, size, color)
|
||||
|
||||
for icon_file in self.icons_dir.glob("*.svg"):
|
||||
name = icon_file.stem
|
||||
self.svg_icons[name] = str(icon_file)
|
||||
# Try PNG
|
||||
png_path = self.icons_dir / f"{name}.png"
|
||||
if png_path.exists():
|
||||
return QIcon(str(png_path))
|
||||
|
||||
# Return default icon
|
||||
return self._get_default_icon(size, color)
|
||||
|
||||
def get(self, name, size=24, color="white"):
|
||||
"""Get an icon by name as QIcon."""
|
||||
# Check for SVG icon first
|
||||
if name in self.svg_icons:
|
||||
return self._svg_to_icon(self.svg_icons[name], size, color)
|
||||
def get_pixmap(self, name, size=24, color="white"):
|
||||
"""Get icon as QPixmap."""
|
||||
# Try SVG
|
||||
svg_path = self.icons_dir / f"{name}.svg"
|
||||
if svg_path.exists():
|
||||
return self._svg_to_pixmap(svg_path, size, color)
|
||||
|
||||
# Return None if not found - caller should use fallback
|
||||
return None
|
||||
# Try PNG
|
||||
png_path = self.icons_dir / f"{name}.png"
|
||||
if png_path.exists():
|
||||
return QPixmap(str(png_path)).scaled(size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
||||
|
||||
# Return default
|
||||
return self._get_default_pixmap(size, color)
|
||||
|
||||
def _svg_to_icon(self, svg_path, size, color):
|
||||
"""Convert SVG to QIcon."""
|
||||
renderer = QSvgRenderer(svg_path)
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
painter = QPixmap(pixmap)
|
||||
renderer.render(painter)
|
||||
|
||||
return QIcon(pixmap)
|
||||
pixmap = self._svg_to_pixmap(svg_path, size, color)
|
||||
return QIcon(pixmap) if pixmap else self._get_default_icon(size, color)
|
||||
|
||||
def get_pixmap(self, name, size=24):
|
||||
"""Get icon as QPixmap."""
|
||||
if name in self.svg_icons:
|
||||
renderer = QSvgRenderer(self.svg_icons[name])
|
||||
def _svg_to_pixmap(self, svg_path, size, color):
|
||||
"""Convert SVG to QPixmap."""
|
||||
try:
|
||||
renderer = QSvgRenderer(str(svg_path))
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
from PyQt6.QtGui import QPainter
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return pixmap
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[IconManager] Error loading SVG {svg_path}: {e}")
|
||||
return None
|
||||
|
||||
def get_emoji(self, name):
|
||||
"""Get emoji fallback for icon."""
|
||||
return self.EMOJIS.get(name, '◆')
|
||||
def _get_default_icon(self, size, color):
|
||||
"""Get default gear icon."""
|
||||
pixmap = self._get_default_pixmap(size, color)
|
||||
return QIcon(pixmap)
|
||||
|
||||
# Emoji fallbacks
|
||||
EMOJIS = {
|
||||
'search': '🔍',
|
||||
'calculator': '🧮',
|
||||
'music': '🎵',
|
||||
'globe': '🌐',
|
||||
'skills': '📊',
|
||||
'camera': '📷',
|
||||
'close': '✕',
|
||||
'check': '✓',
|
||||
'settings': '⚙️',
|
||||
'file': '📄',
|
||||
'trash': '🗑️',
|
||||
'external': '↗',
|
||||
}
|
||||
def _get_default_pixmap(self, size, color):
|
||||
"""Render default SVG as pixmap."""
|
||||
try:
|
||||
from PyQt6.QtCore import QByteArray
|
||||
|
||||
svg_data = self.DEFAULT_ICON.encode('utf-8')
|
||||
renderer = QSvgRenderer(QByteArray(svg_data))
|
||||
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return pixmap
|
||||
except Exception as e:
|
||||
print(f"[IconManager] Error rendering default icon: {e}")
|
||||
# Return empty pixmap as last resort
|
||||
return QPixmap(size, size)
|
||||
|
||||
def icon_exists(self, name):
|
||||
"""Check if icon exists."""
|
||||
svg_path = self.icons_dir / f"{name}.svg"
|
||||
png_path = self.icons_dir / f"{name}.png"
|
||||
return svg_path.exists() or png_path.exists()
|
||||
|
||||
|
||||
# Plugin icon mappings - using actual icon names (no emojis)
|
||||
PLUGIN_ICONS = {
|
||||
# Core
|
||||
"Dashboard": "grid",
|
||||
"Universal Search": "search",
|
||||
"Calculator": "calculator",
|
||||
"Spotify": "music",
|
||||
"Nexus Search": "globe",
|
||||
"Game Reader": "camera",
|
||||
"Skill Scanner": "trending-up",
|
||||
"Settings": "settings",
|
||||
"Plugin Store": "shopping-bag",
|
||||
|
||||
# Hunting/Mining
|
||||
"Loot Tracker": "package",
|
||||
"Mining Helper": "pickaxe",
|
||||
"Global Tracker": "award",
|
||||
"Codex Tracker": "book",
|
||||
|
||||
# Economy/Crafting
|
||||
"Auction Tracker": "dollar-sign",
|
||||
"DPP Calculator": "crosshair",
|
||||
"Enhancer Calc": "zap",
|
||||
"Inventory": "archive",
|
||||
"Crafting Calc": "tool",
|
||||
|
||||
# Utilities
|
||||
"Chat Logger": "message-square",
|
||||
"Mission Tracker": "map",
|
||||
"TP Runner": "navigation",
|
||||
}
|
||||
|
||||
|
||||
def get_plugin_icon_name(plugin_name):
|
||||
"""Get icon name for a plugin."""
|
||||
return PLUGIN_ICONS.get(plugin_name, "settings") # Default to settings/gear
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_icons = None
|
||||
_icon_manager = None
|
||||
|
||||
def get_icon_manager():
|
||||
"""Get the singleton icon manager."""
|
||||
global _icons
|
||||
if _icons is None:
|
||||
_icons = IconManager()
|
||||
return _icons
|
||||
|
||||
|
||||
def get_icon(name, size=24):
|
||||
"""Quick access to get an icon."""
|
||||
return get_icon_manager().get(name, size)
|
||||
|
||||
|
||||
def get_pixmap(name, size=24):
|
||||
"""Quick access to get a pixmap."""
|
||||
return get_icon_manager().get_pixmap(name, size)
|
||||
"""Get global icon manager instance."""
|
||||
global _icon_manager
|
||||
if _icon_manager is None:
|
||||
_icon_manager = IconManager()
|
||||
return _icon_manager
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""
|
||||
EU-Utility - Overlay Window
|
||||
|
||||
Fully EU-styled overlay window matching Entropia Universe aesthetic.
|
||||
Clean, game-like overlay with icon view and list view options.
|
||||
No emojis - only actual icons.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
|
@ -12,71 +13,25 @@ try:
|
|||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QStackedWidget, QSystemTrayIcon,
|
||||
QMenu, QApplication, QFrame, QGraphicsDropShadowEffect
|
||||
QMenu, QApplication, QFrame, QGraphicsDropShadowEffect,
|
||||
QListWidget, QListWidgetItem, QButtonGroup
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSize
|
||||
from PyQt6.QtGui import QAction, QIcon, QColor, QFont, QPixmap, QPainter
|
||||
from PyQt6.QtSvg import QSvgRenderer
|
||||
from PyQt6.QtGui import QAction, QIcon, QColor, QFont
|
||||
PYQT6_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYQT6_AVAILABLE = False
|
||||
print("PyQt6 not available. Install with: pip install PyQt6")
|
||||
|
||||
from core.eu_styles import EU_COLORS, EU_STYLES, get_eu_style
|
||||
|
||||
|
||||
class IconHelper:
|
||||
"""Helper to load and render SVG icons."""
|
||||
|
||||
ICONS_DIR = Path(__file__).parent.parent / "assets" / "icons"
|
||||
|
||||
@classmethod
|
||||
def get_icon(cls, name, size=24):
|
||||
"""Get QIcon from SVG."""
|
||||
svg_path = cls.ICONS_DIR / f"{name}.svg"
|
||||
if not svg_path.exists():
|
||||
return None
|
||||
|
||||
renderer = QSvgRenderer(str(svg_path))
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
from core.eu_styles import EU_COLORS, EU_STYLES
|
||||
from core.icon_manager import get_icon_manager, get_plugin_icon_name
|
||||
|
||||
|
||||
class OverlayWindow(QMainWindow):
|
||||
"""EU-styled overlay window."""
|
||||
"""Clean EU-styled overlay window with view toggle."""
|
||||
|
||||
visibility_changed = pyqtSignal(bool)
|
||||
|
||||
# Plugin icon mapping with EU accent colors
|
||||
PLUGIN_ICONS = {
|
||||
"Universal Search": ("search", "#ff8c42"),
|
||||
"Calculator": ("calculator", "#ff8c42"),
|
||||
"Spotify": ("music", "#1db954"),
|
||||
"Nexus Search": ("globe", "#ff8c42"),
|
||||
"Game Reader": ("camera", "#ff8c42"),
|
||||
"Skill Scanner": ("skills", "#ff8c42"),
|
||||
"Loot Tracker": ("loot", "#ff8c42"),
|
||||
"Mining Helper": ("mob", "#ff8c42"),
|
||||
"Chat Logger": ("file", "#ff8c42"),
|
||||
"Mission Tracker": ("target", "#ff8c42"),
|
||||
"Codex Tracker": ("mob", "#ff8c42"),
|
||||
"Auction Tracker": ("ped", "#ff8c42"),
|
||||
"DPP Calculator": ("weapon", "#ff8c42"),
|
||||
"Enhancer Calc": ("armor", "#ff8c42"),
|
||||
"TP Runner": ("globe", "#ff8c42"),
|
||||
"Inventory": ("loot", "#ff8c42"),
|
||||
"Settings": ("settings", "#ff8c42"),
|
||||
"Plugin Store": ("external", "#ff8c42"),
|
||||
"Crafting Calc": ("armor", "#ff8c42"),
|
||||
"Global Tracker": ("ped", "#ff8c42"),
|
||||
}
|
||||
|
||||
def __init__(self, plugin_manager=None):
|
||||
super().__init__()
|
||||
|
||||
|
|
@ -86,6 +41,9 @@ class OverlayWindow(QMainWindow):
|
|||
self.plugin_manager = plugin_manager
|
||||
self.is_visible = False
|
||||
self.plugin_buttons = []
|
||||
self.plugin_list_items = []
|
||||
self.icon_manager = get_icon_manager()
|
||||
self.view_mode = "icons" # "icons" or "list"
|
||||
|
||||
self._setup_window()
|
||||
self._setup_ui()
|
||||
|
|
@ -105,8 +63,8 @@ class OverlayWindow(QMainWindow):
|
|||
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
# EU-sized window
|
||||
self.resize(800, 550)
|
||||
# Clean, game-like size
|
||||
self.resize(850, 600)
|
||||
self._center_window()
|
||||
|
||||
def _center_window(self):
|
||||
|
|
@ -117,15 +75,15 @@ class OverlayWindow(QMainWindow):
|
|||
self.move(x, y)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup EU-styled UI."""
|
||||
"""Setup clean EU-styled UI."""
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
|
||||
layout = QVBoxLayout(central)
|
||||
layout.setContentsMargins(15, 15, 15, 15)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# Main container with EU styling
|
||||
# Main container
|
||||
self.container = QFrame()
|
||||
self.container.setObjectName("euContainer")
|
||||
self.container.setStyleSheet(f"""
|
||||
|
|
@ -136,18 +94,17 @@ class OverlayWindow(QMainWindow):
|
|||
}}
|
||||
""")
|
||||
|
||||
# Add shadow
|
||||
shadow = QGraphicsDropShadowEffect()
|
||||
shadow.setBlurRadius(25)
|
||||
shadow.setColor(QColor(0, 0, 0, 120))
|
||||
shadow.setOffset(0, 5)
|
||||
shadow.setBlurRadius(30)
|
||||
shadow.setColor(QColor(0, 0, 0, 150))
|
||||
shadow.setOffset(0, 8)
|
||||
self.container.setGraphicsEffect(shadow)
|
||||
|
||||
container_layout = QVBoxLayout(self.container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
container_layout.setSpacing(0)
|
||||
|
||||
# EU-styled header
|
||||
# Clean header
|
||||
header = QWidget()
|
||||
header.setStyleSheet(f"""
|
||||
QWidget {{
|
||||
|
|
@ -158,34 +115,59 @@ class OverlayWindow(QMainWindow):
|
|||
}}
|
||||
""")
|
||||
header_layout = QHBoxLayout(header)
|
||||
header_layout.setContentsMargins(15, 12, 15, 12)
|
||||
header_layout.setSpacing(12)
|
||||
header_layout.setContentsMargins(20, 15, 20, 15)
|
||||
header_layout.setSpacing(15)
|
||||
|
||||
# Logo/Icon
|
||||
logo = QLabel("⚡")
|
||||
logo.setStyleSheet("font-size: 18px; background: transparent;")
|
||||
header_layout.addWidget(logo)
|
||||
# App icon (actual icon, not emoji)
|
||||
app_icon = QLabel()
|
||||
app_icon_pixmap = self.icon_manager.get_pixmap("target", size=24)
|
||||
app_icon.setPixmap(app_icon_pixmap)
|
||||
app_icon.setFixedSize(24, 24)
|
||||
header_layout.addWidget(app_icon)
|
||||
|
||||
# Title
|
||||
title = QLabel("EU-Utility")
|
||||
title = QLabel("EU-UTILITY")
|
||||
title.setStyleSheet(f"""
|
||||
color: {EU_COLORS['accent_orange']};
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
letter-spacing: 1px;
|
||||
""")
|
||||
header_layout.addWidget(title)
|
||||
|
||||
header_layout.addStretch()
|
||||
|
||||
# Close button (EU style)
|
||||
close_btn = QPushButton("✕")
|
||||
# View toggle buttons
|
||||
view_group = QButtonGroup(self)
|
||||
view_group.setExclusive(True)
|
||||
|
||||
self.icon_view_btn = QPushButton("Grid")
|
||||
self.icon_view_btn.setCheckable(True)
|
||||
self.icon_view_btn.setChecked(True)
|
||||
self.icon_view_btn.setFixedSize(60, 28)
|
||||
self.icon_view_btn.setStyleSheet(self._view_toggle_style())
|
||||
self.icon_view_btn.clicked.connect(lambda: self._set_view_mode("icons"))
|
||||
header_layout.addWidget(self.icon_view_btn)
|
||||
|
||||
self.list_view_btn = QPushButton("List")
|
||||
self.list_view_btn.setCheckable(True)
|
||||
self.list_view_btn.setFixedSize(60, 28)
|
||||
self.list_view_btn.setStyleSheet(self._view_toggle_style())
|
||||
self.list_view_btn.clicked.connect(lambda: self._set_view_mode("list"))
|
||||
header_layout.addWidget(self.list_view_btn)
|
||||
|
||||
view_group.addButton(self.icon_view_btn)
|
||||
view_group.addButton(self.list_view_btn)
|
||||
|
||||
# Close button
|
||||
close_btn = QPushButton("×")
|
||||
close_btn.setFixedSize(28, 28)
|
||||
close_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
color: {EU_COLORS['text_muted']};
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
|
|
@ -199,107 +181,225 @@ class OverlayWindow(QMainWindow):
|
|||
|
||||
container_layout.addWidget(header)
|
||||
|
||||
# Plugin content area
|
||||
# Content area with sidebar and main content
|
||||
content_split = QHBoxLayout()
|
||||
content_split.setSpacing(0)
|
||||
content_split.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Sidebar (plugin selector)
|
||||
self.sidebar = QWidget()
|
||||
self.sidebar.setFixedWidth(200)
|
||||
self.sidebar.setStyleSheet(f"""
|
||||
QWidget {{
|
||||
background-color: rgba(20, 25, 35, 150);
|
||||
border-right: 1px solid {EU_COLORS['border_medium']};
|
||||
}}
|
||||
""")
|
||||
self._setup_sidebar()
|
||||
content_split.addWidget(self.sidebar)
|
||||
|
||||
# Main content
|
||||
self.content_area = QWidget()
|
||||
self.content_area.setStyleSheet("background: transparent;")
|
||||
content_layout = QVBoxLayout(self.content_area)
|
||||
content_layout.setContentsMargins(15, 15, 15, 15)
|
||||
content_layout.setSpacing(12)
|
||||
content_layout.setContentsMargins(20, 20, 20, 20)
|
||||
content_layout.setSpacing(15)
|
||||
|
||||
# Plugin stack
|
||||
self.plugin_stack = QStackedWidget()
|
||||
self.plugin_stack.setStyleSheet("background: transparent;")
|
||||
content_layout.addWidget(self.plugin_stack, 1)
|
||||
|
||||
container_layout.addWidget(self.content_area, 1)
|
||||
|
||||
# EU-styled plugin bar
|
||||
if self.plugin_manager:
|
||||
self._setup_plugin_bar(container_layout)
|
||||
content_split.addWidget(self.content_area, 1)
|
||||
container_layout.addLayout(content_split, 1)
|
||||
|
||||
layout.addWidget(self.container)
|
||||
|
||||
# Load plugins
|
||||
if self.plugin_manager:
|
||||
self._load_plugins()
|
||||
|
||||
def _setup_plugin_bar(self, layout):
|
||||
"""Setup EU-styled plugin icon bar."""
|
||||
bar = QWidget()
|
||||
bar.setStyleSheet(f"""
|
||||
QWidget {{
|
||||
background-color: {EU_COLORS['bg_header']};
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-top: 1px solid {EU_COLORS['border_medium']};
|
||||
def _setup_sidebar(self):
|
||||
"""Setup sidebar with plugin selector."""
|
||||
sidebar_layout = QVBoxLayout(self.sidebar)
|
||||
sidebar_layout.setContentsMargins(0, 10, 0, 10)
|
||||
sidebar_layout.setSpacing(5)
|
||||
|
||||
# Plugins label
|
||||
plugins_label = QLabel("PLUGINS")
|
||||
plugins_label.setStyleSheet(f"""
|
||||
color: {EU_COLORS['text_muted']};
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 5px 15px;
|
||||
""")
|
||||
sidebar_layout.addWidget(plugins_label)
|
||||
|
||||
# Plugin list (for list view)
|
||||
self.plugin_list = QListWidget()
|
||||
self.plugin_list.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.plugin_list.setStyleSheet(f"""
|
||||
QListWidget {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}}
|
||||
QListWidget::item {{
|
||||
color: {EU_COLORS['text_secondary']};
|
||||
padding: 10px 15px;
|
||||
border-left: 3px solid transparent;
|
||||
}}
|
||||
QListWidget::item:hover {{
|
||||
background-color: rgba(255, 255, 255, 10);
|
||||
}}
|
||||
QListWidget::item:selected {{
|
||||
background-color: rgba(255, 140, 66, 30);
|
||||
color: white;
|
||||
border-left: 3px solid {EU_COLORS['accent_orange']};
|
||||
}}
|
||||
""")
|
||||
bar_layout = QHBoxLayout(bar)
|
||||
bar_layout.setContentsMargins(15, 10, 15, 10)
|
||||
bar_layout.setSpacing(8)
|
||||
bar_layout.addStretch()
|
||||
self.plugin_list.itemClicked.connect(self._on_list_item_clicked)
|
||||
sidebar_layout.addWidget(self.plugin_list)
|
||||
|
||||
# Add plugin icons
|
||||
# Icon grid (for icon view)
|
||||
self.icon_grid = QWidget()
|
||||
self.icon_grid.setStyleSheet("background: transparent;")
|
||||
self.icon_grid_layout = QVBoxLayout(self.icon_grid)
|
||||
self.icon_grid_layout.setSpacing(10)
|
||||
self.icon_grid_layout.setContentsMargins(10, 10, 10, 10)
|
||||
sidebar_layout.addWidget(self.icon_grid)
|
||||
|
||||
# Initially show icon grid
|
||||
self.plugin_list.hide()
|
||||
|
||||
def _view_toggle_style(self):
|
||||
"""Get style for view toggle buttons."""
|
||||
return f"""
|
||||
QPushButton {{
|
||||
background-color: rgba(60, 70, 90, 100);
|
||||
color: {EU_COLORS['text_muted']};
|
||||
border: 1px solid {EU_COLORS['border_subtle']};
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(80, 90, 110, 150);
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background-color: {EU_COLORS['accent_orange']};
|
||||
color: white;
|
||||
border-color: {EU_COLORS['accent_orange']};
|
||||
}}
|
||||
"""
|
||||
|
||||
def _load_plugins(self):
|
||||
"""Load plugins into sidebar and stack."""
|
||||
for idx, (plugin_id, plugin) in enumerate(self.plugin_manager.get_all_plugins().items()):
|
||||
btn = QPushButton()
|
||||
# Get icon name
|
||||
icon_name = get_plugin_icon_name(plugin.name)
|
||||
|
||||
icon_name, accent_color = self.PLUGIN_ICONS.get(
|
||||
plugin.name,
|
||||
("target", EU_COLORS['accent_orange'])
|
||||
)
|
||||
# Add to list view
|
||||
list_item = QListWidgetItem(plugin.name)
|
||||
list_item.setData(Qt.ItemDataRole.UserRole, idx)
|
||||
self.plugin_list.addItem(list_item)
|
||||
|
||||
# Load SVG icon
|
||||
icon = IconHelper.get_icon(icon_name, size=18)
|
||||
if icon:
|
||||
btn.setIcon(icon)
|
||||
btn.setIconSize(QSize(18, 18))
|
||||
else:
|
||||
btn.setText("◆")
|
||||
# Add to icon grid
|
||||
icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
|
||||
self.icon_grid_layout.addWidget(icon_btn)
|
||||
|
||||
btn.setFixedSize(36, 36)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {EU_COLORS['bg_panel']};
|
||||
border: 1px solid {EU_COLORS['border_subtle']};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: {EU_COLORS['bg_hover']};
|
||||
border: 1px solid {EU_COLORS['border_orange']};
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background-color: {accent_color};
|
||||
border: 1px solid {accent_color};
|
||||
}}
|
||||
""")
|
||||
btn.setCheckable(True)
|
||||
btn.setToolTip(plugin.name)
|
||||
|
||||
# Add plugin UI
|
||||
# Add plugin UI to stack
|
||||
try:
|
||||
plugin_ui = plugin.get_ui()
|
||||
if plugin_ui:
|
||||
plugin_ui.setStyleSheet("background: transparent;")
|
||||
self.plugin_stack.addWidget(plugin_ui)
|
||||
btn.clicked.connect(
|
||||
lambda checked, i=idx, b=btn: self._switch_plugin(i, b)
|
||||
)
|
||||
self.plugin_buttons.append(btn)
|
||||
bar_layout.addWidget(btn)
|
||||
except Exception as e:
|
||||
print(f"Error loading UI for {plugin.name}: {e}")
|
||||
print(f"[Overlay] Error loading UI for {plugin.name}: {e}")
|
||||
|
||||
bar_layout.addStretch()
|
||||
layout.addWidget(bar)
|
||||
# Add stretch to icon grid
|
||||
self.icon_grid_layout.addStretch()
|
||||
|
||||
# Select first
|
||||
if self.plugin_buttons:
|
||||
self.plugin_buttons[0].setChecked(True)
|
||||
# Select first plugin
|
||||
if self.plugin_list.count() > 0:
|
||||
self.plugin_list.setCurrentRow(0)
|
||||
self.plugin_stack.setCurrentIndex(0)
|
||||
|
||||
def _switch_plugin(self, index, button):
|
||||
"""Switch to selected plugin."""
|
||||
for btn in self.plugin_buttons:
|
||||
if btn != button:
|
||||
btn.setChecked(False)
|
||||
button.setChecked(True)
|
||||
def _create_icon_button(self, name, icon_name, index):
|
||||
"""Create an icon button for the sidebar."""
|
||||
btn = QPushButton()
|
||||
btn.setFixedSize(180, 50)
|
||||
btn.setCheckable(True)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
text-align: left;
|
||||
padding: 5px 10px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(255, 255, 255, 10);
|
||||
border-color: {EU_COLORS['border_subtle']};
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background-color: rgba(255, 140, 66, 30);
|
||||
border-color: {EU_COLORS['accent_orange']};
|
||||
}}
|
||||
""")
|
||||
|
||||
# Button layout
|
||||
btn_layout = QHBoxLayout(btn)
|
||||
btn_layout.setContentsMargins(10, 5, 10, 5)
|
||||
btn_layout.setSpacing(10)
|
||||
|
||||
# Icon
|
||||
icon_label = QLabel()
|
||||
icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=20)
|
||||
icon_label.setPixmap(icon_pixmap)
|
||||
icon_label.setFixedSize(20, 20)
|
||||
btn_layout.addWidget(icon_label)
|
||||
|
||||
# Text
|
||||
text_label = QLabel(name)
|
||||
text_label.setStyleSheet(f"color: {EU_COLORS['text_primary']}; font-size: 12px;")
|
||||
btn_layout.addWidget(text_label)
|
||||
btn_layout.addStretch()
|
||||
|
||||
btn.clicked.connect(lambda: self._on_icon_button_clicked(index, btn))
|
||||
|
||||
if index == 0:
|
||||
btn.setChecked(True)
|
||||
|
||||
return btn
|
||||
|
||||
def _on_list_item_clicked(self, item):
|
||||
"""Handle list item click."""
|
||||
idx = item.data(Qt.ItemDataRole.UserRole)
|
||||
self.plugin_stack.setCurrentIndex(idx)
|
||||
|
||||
def _on_icon_button_clicked(self, index, btn):
|
||||
"""Handle icon button click."""
|
||||
# Uncheck all other buttons
|
||||
for i in range(self.icon_grid_layout.count() - 1): # Exclude stretch
|
||||
widget = self.icon_grid_layout.itemAt(i).widget()
|
||||
if widget and isinstance(widget, QPushButton) and widget != btn:
|
||||
widget.setChecked(False)
|
||||
|
||||
btn.setChecked(True)
|
||||
self.plugin_stack.setCurrentIndex(index)
|
||||
|
||||
def _set_view_mode(self, mode):
|
||||
"""Switch between icon and list view."""
|
||||
self.view_mode = mode
|
||||
|
||||
if mode == "icons":
|
||||
self.plugin_list.hide()
|
||||
self.icon_grid.show()
|
||||
else:
|
||||
self.icon_grid.hide()
|
||||
self.plugin_list.show()
|
||||
|
||||
def _setup_tray(self):
|
||||
"""Setup system tray."""
|
||||
self.tray_icon = QSystemTrayIcon(self)
|
||||
|
|
@ -326,13 +426,13 @@ class OverlayWindow(QMainWindow):
|
|||
}}
|
||||
""")
|
||||
|
||||
show_action = QAction("⚡ Show EU-Utility", self)
|
||||
show_action = QAction("Show EU-Utility", self)
|
||||
show_action.triggered.connect(self.show_overlay)
|
||||
tray_menu.addAction(show_action)
|
||||
|
||||
tray_menu.addSeparator()
|
||||
|
||||
quit_action = QAction("✕ Quit", self)
|
||||
quit_action = QAction("Quit", self)
|
||||
quit_action.triggered.connect(self.quit_app)
|
||||
tray_menu.addAction(quit_action)
|
||||
|
||||
|
|
@ -352,26 +452,12 @@ class OverlayWindow(QMainWindow):
|
|||
self.activateWindow()
|
||||
self.is_visible = True
|
||||
self.visibility_changed.emit(True)
|
||||
|
||||
if self.plugin_manager:
|
||||
for plugin in self.plugin_manager.get_all_plugins().values():
|
||||
try:
|
||||
plugin.on_show()
|
||||
except Exception as e:
|
||||
print(f"Error in on_show for {plugin.name}: {e}")
|
||||
|
||||
def hide_overlay(self):
|
||||
"""Hide overlay."""
|
||||
self.hide()
|
||||
self.is_visible = False
|
||||
self.visibility_changed.emit(False)
|
||||
|
||||
if self.plugin_manager:
|
||||
for plugin in self.plugin_manager.get_all_plugins().values():
|
||||
try:
|
||||
plugin.on_hide()
|
||||
except Exception as e:
|
||||
print(f"Error in on_hide for {plugin.name}: {e}")
|
||||
|
||||
def toggle_overlay(self):
|
||||
"""Toggle overlay."""
|
||||
|
|
@ -384,7 +470,6 @@ class OverlayWindow(QMainWindow):
|
|||
"""Quit application."""
|
||||
if self.plugin_manager:
|
||||
self.plugin_manager.shutdown_all()
|
||||
|
||||
self.tray_icon.hide()
|
||||
QApplication.quit()
|
||||
|
||||
|
|
|
|||