diff --git a/projects/EU-Utility/assets/icons/archive.svg b/projects/EU-Utility/assets/icons/archive.svg
new file mode 100644
index 0000000..f1e78cb
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/archive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/award.svg b/projects/EU-Utility/assets/icons/award.svg
new file mode 100644
index 0000000..650f409
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/award.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/book.svg b/projects/EU-Utility/assets/icons/book.svg
new file mode 100644
index 0000000..a8f0c69
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/book.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/box.svg b/projects/EU-Utility/assets/icons/box.svg
new file mode 100644
index 0000000..92e0274
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/box.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/dollar-sign.svg b/projects/EU-Utility/assets/icons/dollar-sign.svg
new file mode 100644
index 0000000..071e6d8
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/dollar-sign.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/grid.svg b/projects/EU-Utility/assets/icons/grid.svg
new file mode 100644
index 0000000..b760ac8
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/grid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/map.svg b/projects/EU-Utility/assets/icons/map.svg
new file mode 100644
index 0000000..ffffa1f
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/map.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/message-square.svg b/projects/EU-Utility/assets/icons/message-square.svg
new file mode 100644
index 0000000..f734912
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/message-square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/navigation.svg b/projects/EU-Utility/assets/icons/navigation.svg
new file mode 100644
index 0000000..27307ce
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/navigation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/package.svg b/projects/EU-Utility/assets/icons/package.svg
new file mode 100644
index 0000000..5b714c9
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/package.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/pickaxe.svg b/projects/EU-Utility/assets/icons/pickaxe.svg
new file mode 100644
index 0000000..dbd7656
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/pickaxe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/shopping-bag.svg b/projects/EU-Utility/assets/icons/shopping-bag.svg
new file mode 100644
index 0000000..98de737
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/shopping-bag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/tool.svg b/projects/EU-Utility/assets/icons/tool.svg
new file mode 100644
index 0000000..dbd7656
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/tool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/trending-up.svg b/projects/EU-Utility/assets/icons/trending-up.svg
new file mode 100644
index 0000000..9642a07
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/trending-up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/assets/icons/zap.svg b/projects/EU-Utility/assets/icons/zap.svg
new file mode 100644
index 0000000..2bfaf70
--- /dev/null
+++ b/projects/EU-Utility/assets/icons/zap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/EU-Utility/core/icon_extractor.py b/projects/EU-Utility/core/icon_extractor.py
new file mode 100644
index 0000000..3f4b37b
--- /dev/null
+++ b/projects/EU-Utility/core/icon_extractor.py
@@ -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(' 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
diff --git a/projects/EU-Utility/core/icon_manager.py b/projects/EU-Utility/core/icon_manager.py
index c1c9a6e..4c07825 100644
--- a/projects/EU-Utility/core/icon_manager.py
+++ b/projects/EU-Utility/core/icon_manager.py
@@ -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 = """"""
+
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
diff --git a/projects/EU-Utility/core/overlay_window.py b/projects/EU-Utility/core/overlay_window.py
index dfcdbc9..fef086f 100644
--- a/projects/EU-Utility/core/overlay_window.py
+++ b/projects/EU-Utility/core/overlay_window.py
@@ -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()