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:
LemonNexus 2026-02-15 19:05:57 +00:00
parent acbdef6133
commit 0feaf24732
1 changed files with 363 additions and 370 deletions

View File

@ -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. Windows 11-style taskbar for in-game overlay.
Features: plugin drawer, mini widgets, floating widgets, Features: Transparent background, search box, pinned plugins,
customizable layout (vertical/horizontal/grid), size, opacity. clean minimal design that expands as needed.
""" """
import json import json
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Callable from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from enum import Enum
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QScrollArea, QGridLayout, QMenu, QDialog, QFrame, QLineEdit, QMenu, QDialog, QSlider, QComboBox,
QSlider, QComboBox, QCheckBox, QSpinBox, QTabWidget QCheckBox, QSpinBox, QApplication, QSizePolicy
) )
from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QIcon, QPixmap from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont, QIcon, QPixmap
class ActivityBarLayout(Enum):
"""Activity bar layout modes."""
HORIZONTAL = "horizontal"
VERTICAL = "vertical"
GRID = "grid"
@dataclass @dataclass
class ActivityBarConfig: class ActivityBarConfig:
"""Activity bar configuration.""" """Activity bar configuration."""
enabled: bool = True enabled: bool = True
layout: ActivityBarLayout = ActivityBarLayout.HORIZONTAL position: str = "bottom" # top, bottom
position: str = "bottom" # top, bottom, left, right icon_size: int = 32
size: int = 48 # Icon size in pixels
opacity: float = 0.9
always_visible: bool = False
auto_hide: bool = True auto_hide: bool = True
show_labels: bool = False auto_hide_delay: int = 3000 # milliseconds
grid_columns: int = 4 # For grid layout pinned_plugins: List[str] = None
grid_rows: int = 2
pinned_plugins: List[str] = None # Plugin IDs to show in bar
def __post_init__(self): def __post_init__(self):
if self.pinned_plugins is None: if self.pinned_plugins is None:
self.pinned_plugins = [] 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: Features:
- Plugin drawer (app drawer style) - Transparent background (no background visible)
- Pinned plugins in bar - Windows-style start button
- Mini widgets - Search box for quick access
- Configurable layout (horizontal/vertical/grid) - Pinned plugins expand the bar
- Draggable, resizable - Clean, minimal design
- Opacity control
""" """
widget_requested = pyqtSignal(str) # plugin_id widget_requested = pyqtSignal(str)
drawer_toggled = pyqtSignal(bool) # is_open search_requested = pyqtSignal(str)
def __init__(self, plugin_manager, parent=None): def __init__(self, plugin_manager, parent=None):
super().__init__(parent) super().__init__(parent)
self.plugin_manager = plugin_manager self.plugin_manager = plugin_manager
self.config = self._load_config() self.config = self._load_config()
self.drawer_open = False
# State
self.pinned_buttons: Dict[str, QPushButton] = {} self.pinned_buttons: Dict[str, QPushButton] = {}
self.mini_widgets: Dict[str, QWidget] = {} self.is_expanded = False
self.search_text = ""
self._setup_window() self._setup_window()
self._setup_ui() self._setup_ui()
self._apply_config() self._apply_config()
# Auto-hide timer # Auto-hide
self.hide_timer = QTimer(self) self.hide_timer = QTimer(self)
self.hide_timer.timeout.connect(self._auto_hide) self.hide_timer.timeout.connect(self._auto_hide)
self.hide_timer.setInterval(3000) # 3 seconds
# Show initially # Show if enabled
if self.config.enabled: if self.config.enabled:
self.show() self.show()
def _setup_window(self): def _setup_window(self):
"""Setup window properties for overlay.""" """Setup frameless overlay window."""
self.setWindowFlags( self.setWindowFlags(
Qt.WindowType.FramelessWindowHint | Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.WindowStaysOnTopHint |
@ -95,150 +104,152 @@ class ActivityBar(QFrame):
Qt.WindowType.WindowDoesNotAcceptFocus Qt.WindowType.WindowDoesNotAcceptFocus
) )
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
# Make draggable # Draggable state
self._dragging = False self._dragging = False
self._drag_offset = QPoint() self._drag_offset = QPoint()
def _setup_ui(self): def _setup_ui(self):
"""Setup the activity bar UI.""" """Setup Windows taskbar-style UI."""
self.main_layout = QVBoxLayout(self) # Main layout with minimal margins
self.main_layout.setContentsMargins(4, 4, 4, 4) layout = QHBoxLayout(self)
self.main_layout.setSpacing(2) layout.setContentsMargins(8, 4, 8, 4)
# 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)
layout.setSpacing(4) layout.setSpacing(4)
self.container.setLayout(layout)
# Update container style # Style: No background, just floating elements
self._update_style() self.setStyleSheet("""
WindowsTaskbar {
def _update_style(self): background: transparent;
"""Update container styling.""" border: none;
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;
}}
""") """)
def _create_drawer_button(self) -> QPushButton: # === START BUTTON (Windows icon style) ===
"""Create the app drawer button.""" self.start_btn = QPushButton("") # Windows-like icon
btn = QPushButton("⋮⋮⋮") # Drawer icon self.start_btn.setFixedSize(40, 40)
btn.setFixedSize(self.config.size, self.config.size) self.start_btn.setStyleSheet("""
btn.setStyleSheet(f""" QPushButton {
QPushButton {{ background: rgba(255, 255, 255, 0.1);
background-color: rgba(255, 140, 66, 200);
color: white; color: white;
border: none; border: none;
border-radius: {self.config.size // 4}px; border-radius: 8px;
font-size: 16px; font-size: 20px;
font-weight: bold; font-weight: bold;
}} }
QPushButton:hover {{ QPushButton:hover {
background-color: rgba(255, 140, 66, 255); background: rgba(255, 255, 255, 0.2);
}} }
QPushButton:pressed {
background: rgba(255, 255, 255, 0.15);
}
""") """)
btn.setToolTip("Plugin Drawer") self.start_btn.setToolTip("Open App Drawer")
btn.clicked.connect(self._toggle_drawer) self.start_btn.clicked.connect(self._toggle_drawer)
return btn 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: 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 = QPushButton()
btn.setFixedSize(self.config.size, self.config.size) size = self.config.icon_size
btn.setFixedSize(size + 8, size + 8)
# Get icon # Get plugin icon or use default
icon_name = getattr(plugin_class, 'icon', 'box') icon_text = getattr(plugin_class, 'icon', '')
try: btn.setText(icon_text)
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("📦")
# Style
btn.setStyleSheet(f""" btn.setStyleSheet(f"""
QPushButton {{ QPushButton {{
background-color: rgba(60, 65, 80, 180); background: transparent;
color: white;
border: none; border: none;
border-radius: {self.config.size // 4}px; border-radius: 6px;
font-size: {size // 2}px;
}} }}
QPushButton:hover {{ 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) btn.setToolTip(plugin_class.name)
# Click handler
btn.clicked.connect(lambda: self._on_plugin_clicked(plugin_id)) btn.clicked.connect(lambda: self._on_plugin_clicked(plugin_id))
return btn 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): def _refresh_pinned_plugins(self):
"""Refresh pinned plugins in the bar.""" """Refresh pinned plugin buttons."""
# Clear existing pinned buttons (except drawer) # Clear existing
for btn in self.pinned_buttons.values(): for btn in self.pinned_buttons.values():
btn.deleteLater() btn.deleteLater()
self.pinned_buttons.clear() self.pinned_buttons.clear()
@ -255,10 +266,25 @@ class ActivityBar(QFrame):
plugin_class = all_plugins[plugin_id] plugin_class = all_plugins[plugin_id]
btn = self._create_plugin_button(plugin_id, plugin_class) btn = self._create_plugin_button(plugin_id, plugin_class)
self.pinned_buttons[plugin_id] = btn self.pinned_buttons[plugin_id] = btn
self._add_to_container(btn) self.pinned_layout.addWidget(btn)
def _setup_drawer(self): def _toggle_drawer(self):
"""Setup the plugin drawer panel.""" """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 = QFrame(self.parent())
self.drawer.setWindowFlags( self.drawer.setWindowFlags(
Qt.WindowType.FramelessWindowHint | Qt.WindowType.FramelessWindowHint |
@ -266,148 +292,179 @@ class ActivityBar(QFrame):
Qt.WindowType.Tool Qt.WindowType.Tool
) )
self.drawer.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.drawer.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.drawer.hide() self.drawer.setFixedSize(400, 500)
# Drawer layout # Drawer style: subtle frosted glass
drawer_layout = QVBoxLayout(self.drawer) self.drawer.setStyleSheet("""
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("""
QFrame { QFrame {
background-color: rgba(50, 55, 70, 200); background: rgba(32, 32, 32, 0.95);
border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1);
} border-radius: 12px;
QFrame:hover {
background-color: rgba(70, 75, 90, 220);
} }
""") """)
layout = QVBoxLayout(frame) layout = QVBoxLayout(self.drawer)
layout.setContentsMargins(4, 4, 4, 4) layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(2) layout.setSpacing(12)
# Icon # Header
icon_label = QLabel("📦") header = QLabel("All Plugins")
icon_label.setStyleSheet("font-size: 24px;") header.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(header)
layout.addWidget(icon_label)
# Name # Search in drawer
name_label = QLabel(plugin_class.name[:8]) drawer_search = QLineEdit()
name_label.setStyleSheet("color: white; font-size: 9px;") drawer_search.setPlaceholderText("Search...")
name_label.setAlignment(Qt.AlignmentFlag.AlignCenter) drawer_search.setStyleSheet("""
layout.addWidget(name_label) 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 # Plugin grid
frame.mousePressEvent = lambda e, pid=plugin_id: self._on_drawer_item_clicked(pid) 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): plugins_layout.addStretch()
"""Toggle the plugin drawer.""" layout.addWidget(plugins_widget)
self.drawer_open = not self.drawer_open
if self.drawer_open: def _create_drawer_item(self, plugin_id: str, plugin_class) -> QPushButton:
# Position drawer near activity bar """Create a drawer item (like Start menu app)."""
pos = self.pos() btn = QPushButton(f" {getattr(plugin_class, 'icon', '')} {plugin_class.name}")
if self.config.layout == ActivityBarLayout.HORIZONTAL: btn.setFixedHeight(44)
self.drawer.move(pos.x(), pos.y() - 300) btn.setStyleSheet("""
else: QPushButton {
self.drawer.move(pos.x() + self.width(), pos.y()) background: transparent;
color: white;
self.drawer.resize(360, 300) border: none;
self.drawer.show() border-radius: 8px;
self.drawer_toggled.emit(True) text-align: left;
else: font-size: 13px;
self.drawer.hide() }
self.drawer_toggled.emit(False) 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): def _on_plugin_clicked(self, plugin_id: str):
"""Handle plugin button click.""" """Handle pinned plugin click."""
print(f"[ActivityBar] Plugin clicked: {plugin_id}") print(f"[Taskbar] Plugin clicked: {plugin_id}")
self.widget_requested.emit(plugin_id) self.widget_requested.emit(plugin_id)
self._pulse_animation()
def _on_drawer_item_clicked(self, plugin_id: str): def _on_drawer_item_clicked(self, plugin_id: str):
"""Handle drawer item click.""" """Handle drawer item click."""
print(f"[ActivityBar] Drawer item clicked: {plugin_id}") self.drawer.hide()
self.widget_requested.emit(plugin_id) self._on_plugin_clicked(plugin_id)
self._toggle_drawer() # Close drawer
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): def _auto_hide(self):
"""Auto-hide the activity bar.""" """Auto-hide when mouse leaves."""
if self.config.auto_hide and not self.config.always_visible: if self.config.auto_hide and not self.underMouse():
# Check if mouse is over bar or drawer if not hasattr(self, 'drawer') or not self.drawer.isVisible():
if not self.underMouse() and not self.drawer.underMouse():
self.hide() self.hide()
def enterEvent(self, event): def enterEvent(self, event):
"""Mouse entered activity bar.""" """Mouse entered - stop hide timer."""
if self.config.auto_hide:
self.hide_timer.stop() self.hide_timer.stop()
super().enterEvent(event) super().enterEvent(event)
def leaveEvent(self, event): def leaveEvent(self, event):
"""Mouse left activity bar.""" """Mouse left - start hide timer."""
if self.config.auto_hide and not self.drawer_open: if self.config.auto_hide:
self.hide_timer.start() self.hide_timer.start()
super().leaveEvent(event) super().leaveEvent(event)
@ -419,7 +476,7 @@ class ActivityBar(QFrame):
event.accept() event.accept()
def mouseMoveEvent(self, event: QMouseEvent): def mouseMoveEvent(self, event: QMouseEvent):
"""Drag activity bar.""" """Drag window."""
if self._dragging: if self._dragging:
new_pos = event.globalPosition().toPoint() - self._drag_offset new_pos = event.globalPosition().toPoint() - self._drag_offset
self.move(new_pos) self.move(new_pos)
@ -428,66 +485,33 @@ class ActivityBar(QFrame):
"""Stop dragging.""" """Stop dragging."""
if event.button() == Qt.MouseButton.LeftButton: if event.button() == Qt.MouseButton.LeftButton:
self._dragging = False 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: def _load_config(self) -> ActivityBarConfig:
"""Load activity bar configuration.""" """Load configuration."""
config_path = Path("config/activity_bar.json") config_path = Path("config/activity_bar.json")
if config_path.exists(): if config_path.exists():
try: try:
data = json.loads(config_path.read_text()) data = json.loads(config_path.read_text())
return ActivityBarConfig(**data) return ActivityBarConfig.from_dict(data)
except: except:
pass pass
return ActivityBarConfig() return ActivityBarConfig()
def _save_config(self): def _save_config(self):
"""Save activity bar configuration.""" """Save configuration."""
config_path = Path("config/activity_bar.json") config_path = Path("config/activity_bar.json")
config_path.parent.mkdir(parents=True, exist_ok=True) config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(asdict(self.config), indent=2)) config_path.write_text(json.dumps(self.config.to_dict(), 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()
class ActivityBarSettingsDialog(QDialog): class TaskbarSettingsDialog(QDialog):
"""Settings dialog for activity bar.""" """Settings dialog for Windows Taskbar."""
def __init__(self, config: ActivityBarConfig, parent=None): def __init__(self, config: ActivityBarConfig, parent=None):
super().__init__(parent) super().__init__(parent)
self.config = config self.config = config
self.setWindowTitle("Activity Bar Settings") self.setWindowTitle("Taskbar Settings")
self.setMinimumSize(400, 500) self.setMinimumSize(350, 300)
self._setup_ui() self._setup_ui()
def _setup_ui(self): def _setup_ui(self):
@ -496,47 +520,24 @@ class ActivityBarSettingsDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Form
form = QFormLayout() 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 # Auto-hide
self.autohide_cb = QCheckBox("Auto-hide when not in use") self.autohide_cb = QCheckBox("Auto-hide when not in use")
self.autohide_cb.setChecked(self.config.auto_hide) self.autohide_cb.setChecked(self.config.auto_hide)
form.addRow(self.autohide_cb) form.addRow(self.autohide_cb)
# Show labels # Position
self.labels_cb = QCheckBox("Show plugin labels") self.position_combo = QComboBox()
self.labels_cb.setChecked(self.config.show_labels) self.position_combo.addItems(["Bottom", "Top"])
form.addRow(self.labels_cb) self.position_combo.setCurrentText(self.config.position.title())
form.addRow("Position:", self.position_combo)
# Grid config # Icon size
self.grid_cols = QSpinBox() self.icon_size = QSpinBox()
self.grid_cols.setRange(2, 8) self.icon_size.setRange(24, 48)
self.grid_cols.setValue(self.config.grid_columns) self.icon_size.setValue(self.config.icon_size)
form.addRow("Grid Columns:", self.grid_cols) form.addRow("Icon Size:", self.icon_size)
layout.addLayout(form) layout.addLayout(form)
@ -549,32 +550,24 @@ class ActivityBarSettingsDialog(QDialog):
layout.addWidget(buttons) layout.addWidget(buttons)
def get_config(self) -> ActivityBarConfig: def get_config(self) -> ActivityBarConfig:
"""Get updated configuration.""" """Get updated config."""
layout_map = {
"Horizontal": ActivityBarLayout.HORIZONTAL,
"Vertical": ActivityBarLayout.VERTICAL,
"Grid": ActivityBarLayout.GRID
}
return ActivityBarConfig( return ActivityBarConfig(
enabled=self.enabled_cb.isChecked(), enabled=True,
layout=layout_map.get(self.layout_combo.currentText(), ActivityBarLayout.HORIZONTAL), position=self.position_combo.currentText().lower(),
size=self.size_spin.value(), icon_size=self.icon_size.value(),
opacity=self.opacity_slider.value() / 100,
auto_hide=self.autohide_cb.isChecked(), auto_hide=self.autohide_cb.isChecked(),
show_labels=self.labels_cb.isChecked(), auto_hide_delay=self.config.auto_hide_delay,
grid_columns=self.grid_cols.value(),
pinned_plugins=self.config.pinned_plugins pinned_plugins=self.config.pinned_plugins
) )
# Global instance # Global instance
_activity_bar: Optional[ActivityBar] = None _taskbar_instance: Optional[WindowsTaskbar] = None
def get_activity_bar(plugin_manager=None) -> Optional[ActivityBar]: def get_activity_bar(plugin_manager=None) -> Optional[WindowsTaskbar]:
"""Get or create global activity bar instance.""" """Get or create global taskbar instance."""
global _activity_bar global _taskbar_instance
if _activity_bar is None and plugin_manager: if _taskbar_instance is None and plugin_manager:
_activity_bar = ActivityBar(plugin_manager) _taskbar_instance = WindowsTaskbar(plugin_manager)
return _activity_bar return _taskbar_instance