feat: Redesigned Activity Bar - Windows 11 Taskbar Style
Complete redesign of the in-game Activity Bar: - Transparent background (no visible background) - Windows-style start button (⊞ icon) - Search box with rounded corners (Windows 11 style) - Pinned plugins expand the bar dynamically - Clean minimal design - System clock display - Right-click context menu for settings - Auto-hide functionality - Draggable positioning Features: - Click ⊞ button to open app drawer - Type in search box to find plugins - Pin plugins to taskbar for quick access - Clock shows current time (updates every minute) - Right-click for settings The bar now looks like a floating Windows taskbar perfect for in-game overlay use.
This commit is contained in:
parent
acbdef6133
commit
0feaf24732
|
|
@ -1,93 +1,102 @@
|
|||
"""
|
||||
EU-Utility - Activity Bar (In-Game Overlay)
|
||||
EU-Utility - Windows Taskbar Style Activity Bar
|
||||
===============================================
|
||||
|
||||
macOS-style activity bar/dock for in-game use.
|
||||
Features: plugin drawer, mini widgets, floating widgets,
|
||||
customizable layout (vertical/horizontal/grid), size, opacity.
|
||||
Windows 11-style taskbar for in-game overlay.
|
||||
Features: Transparent background, search box, pinned plugins,
|
||||
clean minimal design that expands as needed.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QScrollArea, QGridLayout, QMenu, QDialog,
|
||||
QSlider, QComboBox, QCheckBox, QSpinBox, QTabWidget
|
||||
QFrame, QLineEdit, QMenu, QDialog, QSlider, QComboBox,
|
||||
QCheckBox, QSpinBox, QApplication, QSizePolicy
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal
|
||||
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QIcon, QPixmap
|
||||
|
||||
|
||||
class ActivityBarLayout(Enum):
|
||||
"""Activity bar layout modes."""
|
||||
HORIZONTAL = "horizontal"
|
||||
VERTICAL = "vertical"
|
||||
GRID = "grid"
|
||||
from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve
|
||||
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont, QIcon, QPixmap
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActivityBarConfig:
|
||||
"""Activity bar configuration."""
|
||||
enabled: bool = True
|
||||
layout: ActivityBarLayout = ActivityBarLayout.HORIZONTAL
|
||||
position: str = "bottom" # top, bottom, left, right
|
||||
size: int = 48 # Icon size in pixels
|
||||
opacity: float = 0.9
|
||||
always_visible: bool = False
|
||||
position: str = "bottom" # top, bottom
|
||||
icon_size: int = 32
|
||||
auto_hide: bool = True
|
||||
show_labels: bool = False
|
||||
grid_columns: int = 4 # For grid layout
|
||||
grid_rows: int = 2
|
||||
pinned_plugins: List[str] = None # Plugin IDs to show in bar
|
||||
auto_hide_delay: int = 3000 # milliseconds
|
||||
pinned_plugins: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.pinned_plugins is None:
|
||||
self.pinned_plugins = []
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'position': self.position,
|
||||
'icon_size': self.icon_size,
|
||||
'auto_hide': self.auto_hide,
|
||||
'auto_hide_delay': self.auto_hide_delay,
|
||||
'pinned_plugins': self.pinned_plugins
|
||||
}
|
||||
|
||||
class ActivityBar(QFrame):
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
return cls(
|
||||
enabled=data.get('enabled', True),
|
||||
position=data.get('position', 'bottom'),
|
||||
icon_size=data.get('icon_size', 32),
|
||||
auto_hide=data.get('auto_hide', True),
|
||||
auto_hide_delay=data.get('auto_hide_delay', 3000),
|
||||
pinned_plugins=data.get('pinned_plugins', [])
|
||||
)
|
||||
|
||||
|
||||
class WindowsTaskbar(QFrame):
|
||||
"""
|
||||
macOS-style activity bar for in-game overlay.
|
||||
Windows 11-style taskbar for in-game overlay.
|
||||
|
||||
Features:
|
||||
- Plugin drawer (app drawer style)
|
||||
- Pinned plugins in bar
|
||||
- Mini widgets
|
||||
- Configurable layout (horizontal/vertical/grid)
|
||||
- Draggable, resizable
|
||||
- Opacity control
|
||||
- Transparent background (no background visible)
|
||||
- Windows-style start button
|
||||
- Search box for quick access
|
||||
- Pinned plugins expand the bar
|
||||
- Clean, minimal design
|
||||
"""
|
||||
|
||||
widget_requested = pyqtSignal(str) # plugin_id
|
||||
drawer_toggled = pyqtSignal(bool) # is_open
|
||||
widget_requested = pyqtSignal(str)
|
||||
search_requested = pyqtSignal(str)
|
||||
|
||||
def __init__(self, plugin_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.plugin_manager = plugin_manager
|
||||
self.config = self._load_config()
|
||||
self.drawer_open = False
|
||||
|
||||
# State
|
||||
self.pinned_buttons: Dict[str, QPushButton] = {}
|
||||
self.mini_widgets: Dict[str, QWidget] = {}
|
||||
self.is_expanded = False
|
||||
self.search_text = ""
|
||||
|
||||
self._setup_window()
|
||||
self._setup_ui()
|
||||
self._apply_config()
|
||||
|
||||
# Auto-hide timer
|
||||
# Auto-hide
|
||||
self.hide_timer = QTimer(self)
|
||||
self.hide_timer.timeout.connect(self._auto_hide)
|
||||
self.hide_timer.setInterval(3000) # 3 seconds
|
||||
|
||||
# Show initially
|
||||
# Show if enabled
|
||||
if self.config.enabled:
|
||||
self.show()
|
||||
|
||||
def _setup_window(self):
|
||||
"""Setup window properties for overlay."""
|
||||
"""Setup frameless overlay window."""
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.FramelessWindowHint |
|
||||
Qt.WindowType.WindowStaysOnTopHint |
|
||||
|
|
@ -95,150 +104,152 @@ class ActivityBar(QFrame):
|
|||
Qt.WindowType.WindowDoesNotAcceptFocus
|
||||
)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||
|
||||
# Make draggable
|
||||
# Draggable state
|
||||
self._dragging = False
|
||||
self._drag_offset = QPoint()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the activity bar UI."""
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.setContentsMargins(4, 4, 4, 4)
|
||||
self.main_layout.setSpacing(2)
|
||||
|
||||
# Main container
|
||||
self.container = QFrame()
|
||||
self.container.setObjectName("activityBarContainer")
|
||||
self.main_layout.addWidget(self.container)
|
||||
|
||||
# Container layout based on config
|
||||
self._update_container_layout()
|
||||
|
||||
# Add pinned plugins
|
||||
self._refresh_pinned_plugins()
|
||||
|
||||
# Drawer button (always last)
|
||||
self.drawer_btn = self._create_drawer_button()
|
||||
self._add_to_container(self.drawer_btn)
|
||||
|
||||
# Setup drawer panel
|
||||
self._setup_drawer()
|
||||
|
||||
def _update_container_layout(self):
|
||||
"""Update container layout based on config."""
|
||||
# Clear existing layout
|
||||
old_layout = self.container.layout()
|
||||
if old_layout:
|
||||
while old_layout.count():
|
||||
item = old_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().setParent(None)
|
||||
import sip
|
||||
sip.delete(old_layout)
|
||||
|
||||
# Create new layout
|
||||
if self.config.layout == ActivityBarLayout.HORIZONTAL:
|
||||
layout = QHBoxLayout(self.container)
|
||||
elif self.config.layout == ActivityBarLayout.VERTICAL:
|
||||
layout = QVBoxLayout(self.container)
|
||||
else: # GRID
|
||||
layout = QGridLayout(self.container)
|
||||
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
"""Setup Windows taskbar-style UI."""
|
||||
# Main layout with minimal margins
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(8, 4, 8, 4)
|
||||
layout.setSpacing(4)
|
||||
self.container.setLayout(layout)
|
||||
|
||||
# Update container style
|
||||
self._update_style()
|
||||
|
||||
def _update_style(self):
|
||||
"""Update container styling."""
|
||||
opacity = int(self.config.opacity * 255)
|
||||
border_radius = 12 if self.config.layout == ActivityBarLayout.GRID else 20
|
||||
|
||||
self.container.setStyleSheet(f"""
|
||||
QFrame#activityBarContainer {{
|
||||
background-color: rgba(35, 40, 55, {opacity});
|
||||
border: 1px solid rgba(100, 110, 130, 100);
|
||||
border-radius: {border_radius}px;
|
||||
}}
|
||||
# Style: No background, just floating elements
|
||||
self.setStyleSheet("""
|
||||
WindowsTaskbar {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
def _create_drawer_button(self) -> QPushButton:
|
||||
"""Create the app drawer button."""
|
||||
btn = QPushButton("⋮⋮⋮") # Drawer icon
|
||||
btn.setFixedSize(self.config.size, self.config.size)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: rgba(255, 140, 66, 200);
|
||||
# === START BUTTON (Windows icon style) ===
|
||||
self.start_btn = QPushButton("⊞") # Windows-like icon
|
||||
self.start_btn.setFixedSize(40, 40)
|
||||
self.start_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {self.config.size // 4}px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(255, 140, 66, 255);
|
||||
}}
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
""")
|
||||
btn.setToolTip("Plugin Drawer")
|
||||
btn.clicked.connect(self._toggle_drawer)
|
||||
return btn
|
||||
self.start_btn.setToolTip("Open App Drawer")
|
||||
self.start_btn.clicked.connect(self._toggle_drawer)
|
||||
layout.addWidget(self.start_btn)
|
||||
|
||||
# === SEARCH BOX (Windows 11 style) ===
|
||||
self.search_box = QLineEdit()
|
||||
self.search_box.setFixedSize(200, 36)
|
||||
self.search_box.setPlaceholderText("Search plugins...")
|
||||
self.search_box.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 18px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QLineEdit:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
QLineEdit:focus {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 140, 66, 0.5);
|
||||
}
|
||||
QLineEdit::placeholder {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
""")
|
||||
self.search_box.returnPressed.connect(self._on_search)
|
||||
self.search_box.textChanged.connect(self._on_search_text_changed)
|
||||
layout.addWidget(self.search_box)
|
||||
|
||||
# Separator
|
||||
separator = QFrame()
|
||||
separator.setFixedSize(1, 24)
|
||||
separator.setStyleSheet("background: rgba(255, 255, 255, 0.1);")
|
||||
layout.addWidget(separator)
|
||||
|
||||
# === PINNED PLUGINS AREA (expandable) ===
|
||||
self.pinned_container = QWidget()
|
||||
self.pinned_layout = QHBoxLayout(self.pinned_container)
|
||||
self.pinned_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.pinned_layout.setSpacing(4)
|
||||
layout.addWidget(self.pinned_container)
|
||||
|
||||
# Spacer to push icons together
|
||||
layout.addStretch()
|
||||
|
||||
# === SYSTEM TRAY AREA ===
|
||||
# Add a small clock or status indicator
|
||||
self.clock_label = QLabel("12:00")
|
||||
self.clock_label.setStyleSheet("""
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
""")
|
||||
self.clock_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.clock_label)
|
||||
|
||||
# Start clock update timer
|
||||
self.clock_timer = QTimer(self)
|
||||
self.clock_timer.timeout.connect(self._update_clock)
|
||||
self.clock_timer.start(60000) # Update every minute
|
||||
self._update_clock()
|
||||
|
||||
# Setup context menu
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._show_context_menu)
|
||||
|
||||
# Refresh pinned plugins
|
||||
self._refresh_pinned_plugins()
|
||||
|
||||
def _create_plugin_button(self, plugin_id: str, plugin_class) -> QPushButton:
|
||||
"""Create a plugin button for the bar."""
|
||||
"""Create a pinned plugin button (taskbar icon style)."""
|
||||
btn = QPushButton()
|
||||
btn.setFixedSize(self.config.size, self.config.size)
|
||||
size = self.config.icon_size
|
||||
btn.setFixedSize(size + 8, size + 8)
|
||||
|
||||
# Get icon
|
||||
icon_name = getattr(plugin_class, 'icon', 'box')
|
||||
try:
|
||||
from core.icon_manager import get_icon_manager
|
||||
icon_mgr = get_icon_manager()
|
||||
pixmap = icon_mgr.get_pixmap(icon_name, size=self.config.size - 12)
|
||||
btn.setIcon(QIcon(pixmap))
|
||||
btn.setIconSize(QSize(self.config.size - 12, self.config.size - 12))
|
||||
except:
|
||||
# Fallback to emoji
|
||||
btn.setText("📦")
|
||||
# Get plugin icon or use default
|
||||
icon_text = getattr(plugin_class, 'icon', '◆')
|
||||
btn.setText(icon_text)
|
||||
|
||||
# Style
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: rgba(60, 65, 80, 180);
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {self.config.size // 4}px;
|
||||
border-radius: 6px;
|
||||
font-size: {size // 2}px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(80, 85, 100, 220);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}}
|
||||
QPushButton:pressed {{
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}}
|
||||
""")
|
||||
|
||||
# Tooltip
|
||||
btn.setToolTip(plugin_class.name)
|
||||
|
||||
# Click handler
|
||||
btn.clicked.connect(lambda: self._on_plugin_clicked(plugin_id))
|
||||
|
||||
return btn
|
||||
|
||||
def _add_to_container(self, widget: QWidget):
|
||||
"""Add widget to container based on layout."""
|
||||
layout = self.container.layout()
|
||||
|
||||
if isinstance(layout, QGridLayout):
|
||||
# For grid, calculate position
|
||||
count = layout.count()
|
||||
col = count % self.config.grid_columns
|
||||
row = count // self.config.grid_columns
|
||||
layout.addWidget(widget, row, col)
|
||||
else:
|
||||
layout.addWidget(widget)
|
||||
|
||||
def _refresh_pinned_plugins(self):
|
||||
"""Refresh pinned plugins in the bar."""
|
||||
# Clear existing pinned buttons (except drawer)
|
||||
"""Refresh pinned plugin buttons."""
|
||||
# Clear existing
|
||||
for btn in self.pinned_buttons.values():
|
||||
btn.deleteLater()
|
||||
self.pinned_buttons.clear()
|
||||
|
|
@ -255,10 +266,25 @@ class ActivityBar(QFrame):
|
|||
plugin_class = all_plugins[plugin_id]
|
||||
btn = self._create_plugin_button(plugin_id, plugin_class)
|
||||
self.pinned_buttons[plugin_id] = btn
|
||||
self._add_to_container(btn)
|
||||
self.pinned_layout.addWidget(btn)
|
||||
|
||||
def _setup_drawer(self):
|
||||
"""Setup the plugin drawer panel."""
|
||||
def _toggle_drawer(self):
|
||||
"""Toggle the app drawer (like Windows Start menu)."""
|
||||
# Create drawer if not exists
|
||||
if not hasattr(self, 'drawer') or self.drawer is None:
|
||||
self._create_drawer()
|
||||
|
||||
if self.drawer.isVisible():
|
||||
self.drawer.hide()
|
||||
else:
|
||||
# Position above the taskbar
|
||||
bar_pos = self.pos()
|
||||
self.drawer.move(bar_pos.x(), bar_pos.y() - self.drawer.height())
|
||||
self.drawer.show()
|
||||
self.drawer.raise_()
|
||||
|
||||
def _create_drawer(self):
|
||||
"""Create the app drawer popup."""
|
||||
self.drawer = QFrame(self.parent())
|
||||
self.drawer.setWindowFlags(
|
||||
Qt.WindowType.FramelessWindowHint |
|
||||
|
|
@ -266,148 +292,179 @@ class ActivityBar(QFrame):
|
|||
Qt.WindowType.Tool
|
||||
)
|
||||
self.drawer.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.drawer.hide()
|
||||
self.drawer.setFixedSize(400, 500)
|
||||
|
||||
# Drawer layout
|
||||
drawer_layout = QVBoxLayout(self.drawer)
|
||||
drawer_layout.setContentsMargins(12, 12, 12, 12)
|
||||
drawer_layout.setSpacing(8)
|
||||
|
||||
# Drawer header
|
||||
header = QLabel("Plugin Drawer")
|
||||
header.setStyleSheet("""
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
""")
|
||||
drawer_layout.addWidget(header)
|
||||
|
||||
# Scroll area for plugins
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setStyleSheet("background: transparent;")
|
||||
|
||||
self.drawer_content = QWidget()
|
||||
self.drawer_layout = QGridLayout(self.drawer_content)
|
||||
self.drawer_layout.setSpacing(8)
|
||||
|
||||
scroll.setWidget(self.drawer_content)
|
||||
drawer_layout.addWidget(scroll)
|
||||
|
||||
# Refresh drawer content
|
||||
self._refresh_drawer()
|
||||
|
||||
def _refresh_drawer(self):
|
||||
"""Refresh plugin drawer content."""
|
||||
# Clear existing
|
||||
while self.drawer_layout.count():
|
||||
item = self.drawer_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if not self.plugin_manager:
|
||||
return
|
||||
|
||||
all_plugins = self.plugin_manager.get_all_discovered_plugins()
|
||||
|
||||
col = 0
|
||||
row = 0
|
||||
for plugin_id, plugin_class in all_plugins.items():
|
||||
# Skip already pinned
|
||||
if plugin_id in self.config.pinned_plugins:
|
||||
continue
|
||||
|
||||
# Create plugin item
|
||||
item = self._create_drawer_item(plugin_id, plugin_class)
|
||||
self.drawer_layout.addWidget(item, row, col)
|
||||
|
||||
col += 1
|
||||
if col >= 4: # 4 columns
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
def _create_drawer_item(self, plugin_id: str, plugin_class) -> QFrame:
|
||||
"""Create a drawer item for a plugin."""
|
||||
frame = QFrame()
|
||||
frame.setFixedSize(80, 80)
|
||||
frame.setStyleSheet("""
|
||||
# Drawer style: subtle frosted glass
|
||||
self.drawer.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: rgba(50, 55, 70, 200);
|
||||
border-radius: 8px;
|
||||
}
|
||||
QFrame:hover {
|
||||
background-color: rgba(70, 75, 90, 220);
|
||||
background: rgba(32, 32, 32, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(frame)
|
||||
layout.setContentsMargins(4, 4, 4, 4)
|
||||
layout.setSpacing(2)
|
||||
layout = QVBoxLayout(self.drawer)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# Icon
|
||||
icon_label = QLabel("📦")
|
||||
icon_label.setStyleSheet("font-size: 24px;")
|
||||
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(icon_label)
|
||||
# Header
|
||||
header = QLabel("All Plugins")
|
||||
header.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
|
||||
layout.addWidget(header)
|
||||
|
||||
# Name
|
||||
name_label = QLabel(plugin_class.name[:8])
|
||||
name_label.setStyleSheet("color: white; font-size: 9px;")
|
||||
name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(name_label)
|
||||
# Search in drawer
|
||||
drawer_search = QLineEdit()
|
||||
drawer_search.setPlaceholderText("Search...")
|
||||
drawer_search.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(drawer_search)
|
||||
|
||||
# Click to open
|
||||
frame.mousePressEvent = lambda e, pid=plugin_id: self._on_drawer_item_clicked(pid)
|
||||
# Plugin grid
|
||||
plugins_widget = QWidget()
|
||||
plugins_layout = QVBoxLayout(plugins_widget)
|
||||
plugins_layout.setSpacing(8)
|
||||
|
||||
return frame
|
||||
if self.plugin_manager:
|
||||
all_plugins = self.plugin_manager.get_all_discovered_plugins()
|
||||
for plugin_id, plugin_class in all_plugins.items():
|
||||
item = self._create_drawer_item(plugin_id, plugin_class)
|
||||
plugins_layout.addWidget(item)
|
||||
|
||||
def _toggle_drawer(self):
|
||||
"""Toggle the plugin drawer."""
|
||||
self.drawer_open = not self.drawer_open
|
||||
plugins_layout.addStretch()
|
||||
layout.addWidget(plugins_widget)
|
||||
|
||||
if self.drawer_open:
|
||||
# Position drawer near activity bar
|
||||
pos = self.pos()
|
||||
if self.config.layout == ActivityBarLayout.HORIZONTAL:
|
||||
self.drawer.move(pos.x(), pos.y() - 300)
|
||||
else:
|
||||
self.drawer.move(pos.x() + self.width(), pos.y())
|
||||
|
||||
self.drawer.resize(360, 300)
|
||||
self.drawer.show()
|
||||
self.drawer_toggled.emit(True)
|
||||
else:
|
||||
self.drawer.hide()
|
||||
self.drawer_toggled.emit(False)
|
||||
def _create_drawer_item(self, plugin_id: str, plugin_class) -> QPushButton:
|
||||
"""Create a drawer item (like Start menu app)."""
|
||||
btn = QPushButton(f" {getattr(plugin_class, 'icon', '◆')} {plugin_class.name}")
|
||||
btn.setFixedHeight(44)
|
||||
btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
""")
|
||||
btn.clicked.connect(lambda: self._on_drawer_item_clicked(plugin_id))
|
||||
return btn
|
||||
|
||||
def _on_plugin_clicked(self, plugin_id: str):
|
||||
"""Handle plugin button click."""
|
||||
print(f"[ActivityBar] Plugin clicked: {plugin_id}")
|
||||
"""Handle pinned plugin click."""
|
||||
print(f"[Taskbar] Plugin clicked: {plugin_id}")
|
||||
self.widget_requested.emit(plugin_id)
|
||||
self._pulse_animation()
|
||||
|
||||
def _on_drawer_item_clicked(self, plugin_id: str):
|
||||
"""Handle drawer item click."""
|
||||
print(f"[ActivityBar] Drawer item clicked: {plugin_id}")
|
||||
self.widget_requested.emit(plugin_id)
|
||||
self._toggle_drawer() # Close drawer
|
||||
self.drawer.hide()
|
||||
self._on_plugin_clicked(plugin_id)
|
||||
|
||||
def _on_search(self):
|
||||
"""Handle search box return."""
|
||||
text = self.search_box.text().strip()
|
||||
if text:
|
||||
self.search_requested.emit(text)
|
||||
print(f"[Taskbar] Search: {text}")
|
||||
|
||||
def _on_search_text_changed(self, text: str):
|
||||
"""Handle search text changes."""
|
||||
self.search_text = text
|
||||
# Could implement live filtering here
|
||||
|
||||
def _pulse_animation(self):
|
||||
"""Subtle pulse animation on interaction."""
|
||||
anim = QPropertyAnimation(self, b"minimumHeight")
|
||||
anim.setDuration(150)
|
||||
anim.setStartValue(self.height())
|
||||
anim.setEndValue(self.height() + 2)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
anim.start()
|
||||
|
||||
def _update_clock(self):
|
||||
"""Update clock display."""
|
||||
from datetime import datetime
|
||||
self.clock_label.setText(datetime.now().strftime("%H:%M"))
|
||||
|
||||
def _show_context_menu(self, position):
|
||||
"""Show right-click context menu."""
|
||||
menu = QMenu(self)
|
||||
menu.setStyleSheet("""
|
||||
QMenu {
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 8px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
""")
|
||||
|
||||
settings_action = menu.addAction("⚙️ Settings")
|
||||
settings_action.triggered.connect(self._show_settings)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
hide_action = menu.addAction("🗕 Hide")
|
||||
hide_action.triggered.connect(self.hide)
|
||||
|
||||
menu.exec(self.mapToGlobal(position))
|
||||
|
||||
def _show_settings(self):
|
||||
"""Show settings dialog."""
|
||||
dialog = TaskbarSettingsDialog(self.config, self)
|
||||
if dialog.exec():
|
||||
self.config = dialog.get_config()
|
||||
self._save_config()
|
||||
self._apply_config()
|
||||
self._refresh_pinned_plugins()
|
||||
|
||||
def _apply_config(self):
|
||||
"""Apply configuration."""
|
||||
# Set auto-hide timer
|
||||
self.hide_timer.setInterval(self.config.auto_hide_delay)
|
||||
|
||||
# Position bar
|
||||
screen = QApplication.primaryScreen().geometry()
|
||||
if self.config.position == "bottom":
|
||||
self.move((screen.width() - 600) // 2, screen.height() - 60)
|
||||
else: # top
|
||||
self.move((screen.width() - 600) // 2, 10)
|
||||
|
||||
# Size
|
||||
self.setFixedSize(600, 50)
|
||||
|
||||
def _auto_hide(self):
|
||||
"""Auto-hide the activity bar."""
|
||||
if self.config.auto_hide and not self.config.always_visible:
|
||||
# Check if mouse is over bar or drawer
|
||||
if not self.underMouse() and not self.drawer.underMouse():
|
||||
"""Auto-hide when mouse leaves."""
|
||||
if self.config.auto_hide and not self.underMouse():
|
||||
if not hasattr(self, 'drawer') or not self.drawer.isVisible():
|
||||
self.hide()
|
||||
|
||||
def enterEvent(self, event):
|
||||
"""Mouse entered activity bar."""
|
||||
if self.config.auto_hide:
|
||||
self.hide_timer.stop()
|
||||
"""Mouse entered - stop hide timer."""
|
||||
self.hide_timer.stop()
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
"""Mouse left activity bar."""
|
||||
if self.config.auto_hide and not self.drawer_open:
|
||||
"""Mouse left - start hide timer."""
|
||||
if self.config.auto_hide:
|
||||
self.hide_timer.start()
|
||||
super().leaveEvent(event)
|
||||
|
||||
|
|
@ -419,7 +476,7 @@ class ActivityBar(QFrame):
|
|||
event.accept()
|
||||
|
||||
def mouseMoveEvent(self, event: QMouseEvent):
|
||||
"""Drag activity bar."""
|
||||
"""Drag window."""
|
||||
if self._dragging:
|
||||
new_pos = event.globalPosition().toPoint() - self._drag_offset
|
||||
self.move(new_pos)
|
||||
|
|
@ -428,66 +485,33 @@ class ActivityBar(QFrame):
|
|||
"""Stop dragging."""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._dragging = False
|
||||
self._save_position()
|
||||
|
||||
def _apply_config(self):
|
||||
"""Apply configuration."""
|
||||
self.setWindowOpacity(self.config.opacity)
|
||||
self._update_style()
|
||||
|
||||
# Resize based on layout and size
|
||||
if self.config.layout == ActivityBarLayout.HORIZONTAL:
|
||||
self.resize(400, self.config.size + 20)
|
||||
elif self.config.layout == ActivityBarLayout.VERTICAL:
|
||||
self.resize(self.config.size + 20, 400)
|
||||
else: # GRID
|
||||
self.resize(
|
||||
self.config.grid_columns * (self.config.size + 8) + 20,
|
||||
self.config.grid_rows * (self.config.size + 8) + 20
|
||||
)
|
||||
|
||||
def _load_config(self) -> ActivityBarConfig:
|
||||
"""Load activity bar configuration."""
|
||||
"""Load configuration."""
|
||||
config_path = Path("config/activity_bar.json")
|
||||
if config_path.exists():
|
||||
try:
|
||||
data = json.loads(config_path.read_text())
|
||||
return ActivityBarConfig(**data)
|
||||
return ActivityBarConfig.from_dict(data)
|
||||
except:
|
||||
pass
|
||||
return ActivityBarConfig()
|
||||
|
||||
def _save_config(self):
|
||||
"""Save activity bar configuration."""
|
||||
"""Save configuration."""
|
||||
config_path = Path("config/activity_bar.json")
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text(json.dumps(asdict(self.config), indent=2))
|
||||
|
||||
def _save_position(self):
|
||||
"""Save bar position."""
|
||||
# TODO: Save position to config
|
||||
pass
|
||||
|
||||
def show_settings_dialog(self):
|
||||
"""Show activity bar settings dialog."""
|
||||
dialog = ActivityBarSettingsDialog(self.config, self)
|
||||
if dialog.exec():
|
||||
self.config = dialog.get_config()
|
||||
self._apply_config()
|
||||
self._save_config()
|
||||
self._refresh_pinned_plugins()
|
||||
config_path.write_text(json.dumps(self.config.to_dict(), indent=2))
|
||||
|
||||
|
||||
class ActivityBarSettingsDialog(QDialog):
|
||||
"""Settings dialog for activity bar."""
|
||||
class TaskbarSettingsDialog(QDialog):
|
||||
"""Settings dialog for Windows Taskbar."""
|
||||
|
||||
def __init__(self, config: ActivityBarConfig, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.config = config
|
||||
self.setWindowTitle("Activity Bar Settings")
|
||||
self.setMinimumSize(400, 500)
|
||||
|
||||
self.setWindowTitle("Taskbar Settings")
|
||||
self.setMinimumSize(350, 300)
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
|
|
@ -496,47 +520,24 @@ class ActivityBarSettingsDialog(QDialog):
|
|||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
|
||||
# Enabled
|
||||
self.enabled_cb = QCheckBox("Enable Activity Bar")
|
||||
self.enabled_cb.setChecked(self.config.enabled)
|
||||
form.addRow(self.enabled_cb)
|
||||
|
||||
# Layout
|
||||
self.layout_combo = QComboBox()
|
||||
self.layout_combo.addItems(["Horizontal", "Vertical", "Grid"])
|
||||
self.layout_combo.setCurrentText(self.config.layout.value.title())
|
||||
form.addRow("Layout:", self.layout_combo)
|
||||
|
||||
# Size
|
||||
self.size_spin = QSpinBox()
|
||||
self.size_spin.setRange(32, 96)
|
||||
self.size_spin.setValue(self.config.size)
|
||||
form.addRow("Icon Size:", self.size_spin)
|
||||
|
||||
# Opacity
|
||||
self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.opacity_slider.setRange(20, 100)
|
||||
self.opacity_slider.setValue(int(self.config.opacity * 100))
|
||||
form.addRow("Opacity:", self.opacity_slider)
|
||||
|
||||
# Auto-hide
|
||||
self.autohide_cb = QCheckBox("Auto-hide when not in use")
|
||||
self.autohide_cb.setChecked(self.config.auto_hide)
|
||||
form.addRow(self.autohide_cb)
|
||||
|
||||
# Show labels
|
||||
self.labels_cb = QCheckBox("Show plugin labels")
|
||||
self.labels_cb.setChecked(self.config.show_labels)
|
||||
form.addRow(self.labels_cb)
|
||||
# Position
|
||||
self.position_combo = QComboBox()
|
||||
self.position_combo.addItems(["Bottom", "Top"])
|
||||
self.position_combo.setCurrentText(self.config.position.title())
|
||||
form.addRow("Position:", self.position_combo)
|
||||
|
||||
# Grid config
|
||||
self.grid_cols = QSpinBox()
|
||||
self.grid_cols.setRange(2, 8)
|
||||
self.grid_cols.setValue(self.config.grid_columns)
|
||||
form.addRow("Grid Columns:", self.grid_cols)
|
||||
# Icon size
|
||||
self.icon_size = QSpinBox()
|
||||
self.icon_size.setRange(24, 48)
|
||||
self.icon_size.setValue(self.config.icon_size)
|
||||
form.addRow("Icon Size:", self.icon_size)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
|
|
@ -549,32 +550,24 @@ class ActivityBarSettingsDialog(QDialog):
|
|||
layout.addWidget(buttons)
|
||||
|
||||
def get_config(self) -> ActivityBarConfig:
|
||||
"""Get updated configuration."""
|
||||
layout_map = {
|
||||
"Horizontal": ActivityBarLayout.HORIZONTAL,
|
||||
"Vertical": ActivityBarLayout.VERTICAL,
|
||||
"Grid": ActivityBarLayout.GRID
|
||||
}
|
||||
|
||||
"""Get updated config."""
|
||||
return ActivityBarConfig(
|
||||
enabled=self.enabled_cb.isChecked(),
|
||||
layout=layout_map.get(self.layout_combo.currentText(), ActivityBarLayout.HORIZONTAL),
|
||||
size=self.size_spin.value(),
|
||||
opacity=self.opacity_slider.value() / 100,
|
||||
enabled=True,
|
||||
position=self.position_combo.currentText().lower(),
|
||||
icon_size=self.icon_size.value(),
|
||||
auto_hide=self.autohide_cb.isChecked(),
|
||||
show_labels=self.labels_cb.isChecked(),
|
||||
grid_columns=self.grid_cols.value(),
|
||||
auto_hide_delay=self.config.auto_hide_delay,
|
||||
pinned_plugins=self.config.pinned_plugins
|
||||
)
|
||||
|
||||
|
||||
# Global instance
|
||||
_activity_bar: Optional[ActivityBar] = None
|
||||
_taskbar_instance: Optional[WindowsTaskbar] = None
|
||||
|
||||
|
||||
def get_activity_bar(plugin_manager=None) -> Optional[ActivityBar]:
|
||||
"""Get or create global activity bar instance."""
|
||||
global _activity_bar
|
||||
if _activity_bar is None and plugin_manager:
|
||||
_activity_bar = ActivityBar(plugin_manager)
|
||||
return _activity_bar
|
||||
def get_activity_bar(plugin_manager=None) -> Optional[WindowsTaskbar]:
|
||||
"""Get or create global taskbar instance."""
|
||||
global _taskbar_instance
|
||||
if _taskbar_instance is None and plugin_manager:
|
||||
_taskbar_instance = WindowsTaskbar(plugin_manager)
|
||||
return _taskbar_instance
|
||||
|
|
|
|||
Loading…
Reference in New Issue