feat: Enhanced Activity Bar with drag, resize, opacity, and customization

Activity Bar Improvements:
- Draggable: Grab left handle to move anywhere on screen
- Resizable: Grab right handle to change width (400-1600px)
- Background Opacity: Now actually works (20-100%)
- Search toggle: Right-click menu to show/hide search box
- Clock toggle: Right-click menu to show/hide clock
- Reset Position: Right-click menu to reset to default position
- Settings dialog: Added opacity slider, width, search/clock toggles

Config additions:
- x, y: Custom position
- width: Bar width (400-1600)
- background_opacity: 20-100%
- show_search: Toggle search visibility
- show_clock: Toggle clock visibility

Bug Fixes:
- OverlayController now inherits QObject (fixes 'cannot be converted' error)
- Activity bar saves position when dragged
- Activity bar saves width when resized
This commit is contained in:
LemonNexus 2026-02-16 00:43:56 +00:00
parent 159f7a4a05
commit 4b5096a859
3 changed files with 270 additions and 73 deletions

View File

@ -27,11 +27,17 @@ from core.icon_manager import get_icon_manager
class ActivityBarConfig: class ActivityBarConfig:
"""Activity bar configuration.""" """Activity bar configuration."""
enabled: bool = True enabled: bool = True
position: str = "bottom" # top, bottom position: str = "bottom" # top, bottom, custom
x: int = 100 # custom X position
y: int = 800 # custom Y position
width: int = 800 # custom width
icon_size: int = 32 icon_size: int = 32
auto_hide: bool = True auto_hide: bool = False # Disabled by default for easier use
auto_hide_delay: int = 3000 # milliseconds auto_hide_delay: int = 3000 # milliseconds
auto_show_on_focus: bool = False # DISABLED by default - causes UI freezing auto_show_on_focus: bool = False
background_opacity: int = 90 # 0-100
show_search: bool = True
show_clock: bool = True
pinned_plugins: List[str] = None pinned_plugins: List[str] = None
def __post_init__(self): def __post_init__(self):
@ -42,10 +48,16 @@ class ActivityBarConfig:
return { return {
'enabled': self.enabled, 'enabled': self.enabled,
'position': self.position, 'position': self.position,
'x': self.x,
'y': self.y,
'width': self.width,
'icon_size': self.icon_size, 'icon_size': self.icon_size,
'auto_hide': self.auto_hide, 'auto_hide': self.auto_hide,
'auto_hide_delay': self.auto_hide_delay, 'auto_hide_delay': self.auto_hide_delay,
'auto_show_on_focus': self.auto_show_on_focus, 'auto_show_on_focus': self.auto_show_on_focus,
'background_opacity': self.background_opacity,
'show_search': self.show_search,
'show_clock': self.show_clock,
'pinned_plugins': self.pinned_plugins 'pinned_plugins': self.pinned_plugins
} }
@ -54,9 +66,16 @@ class ActivityBarConfig:
return cls( return cls(
enabled=data.get('enabled', True), enabled=data.get('enabled', True),
position=data.get('position', 'bottom'), position=data.get('position', 'bottom'),
x=data.get('x', 100),
y=data.get('y', 800),
width=data.get('width', 800),
icon_size=data.get('icon_size', 32), icon_size=data.get('icon_size', 32),
auto_hide=data.get('auto_hide', True), auto_hide=data.get('auto_hide', False),
auto_hide_delay=data.get('auto_hide_delay', 3000), auto_hide_delay=data.get('auto_hide_delay', 3000),
auto_show_on_focus=data.get('auto_show_on_focus', False),
background_opacity=data.get('background_opacity', 90),
show_search=data.get('show_search', True),
show_clock=data.get('show_clock', True),
pinned_plugins=data.get('pinned_plugins', []) pinned_plugins=data.get('pinned_plugins', [])
) )
@ -115,19 +134,21 @@ class WindowsTaskbar(QFrame):
self._drag_offset = QPoint() self._drag_offset = QPoint()
def _setup_ui(self): def _setup_ui(self):
"""Setup Windows taskbar-style UI.""" """Setup Windows taskbar-style UI with draggable and resizable features."""
# Main layout with minimal margins # Main layout with minimal margins
layout = QHBoxLayout(self) self.main_layout = QHBoxLayout(self)
layout.setContentsMargins(8, 4, 8, 4) self.main_layout.setContentsMargins(8, 4, 8, 4)
layout.setSpacing(4) self.main_layout.setSpacing(4)
# Style: No background, just floating elements # Apply opacity from config
self.setStyleSheet(""" self._apply_opacity()
WindowsTaskbar {
background: transparent; # === DRAG HANDLE (invisible, left side) ===
border: none; self.drag_handle = QFrame()
} self.drag_handle.setFixedSize(8, 40)
""") self.drag_handle.setStyleSheet("background: transparent; cursor: move;")
self.drag_handle.setToolTip("Drag to move")
self.main_layout.addWidget(self.drag_handle)
# === START BUTTON (Windows-style icon) === # === START BUTTON (Windows-style icon) ===
self.start_btn = QPushButton() self.start_btn = QPushButton()
@ -150,7 +171,7 @@ class WindowsTaskbar(QFrame):
""") """)
self.start_btn.setToolTip("Open App Drawer") self.start_btn.setToolTip("Open App Drawer")
self.start_btn.clicked.connect(self._toggle_drawer) self.start_btn.clicked.connect(self._toggle_drawer)
layout.addWidget(self.start_btn) self.main_layout.addWidget(self.start_btn)
# === SEARCH BOX (Windows 11 style) === # === SEARCH BOX (Windows 11 style) ===
self.search_box = QLineEdit() self.search_box = QLineEdit()
@ -179,31 +200,39 @@ class WindowsTaskbar(QFrame):
""") """)
self.search_box.returnPressed.connect(self._on_search) self.search_box.returnPressed.connect(self._on_search)
self.search_box.textChanged.connect(self._on_search_text_changed) self.search_box.textChanged.connect(self._on_search_text_changed)
layout.addWidget(self.search_box) self.main_layout.addWidget(self.search_box)
# Search visibility
self.search_box.setVisible(self.config.show_search)
# Separator # Separator
separator = QFrame() separator = QFrame()
separator.setFixedSize(1, 24) separator.setFixedSize(1, 24)
separator.setStyleSheet("background: rgba(255, 255, 255, 0.1);") separator.setStyleSheet("background: rgba(255, 255, 255, 0.1);")
layout.addWidget(separator) self.main_layout.addWidget(separator)
# === PINNED PLUGINS AREA (expandable) === # === PINNED PLUGINS AREA (expandable) ===
self.pinned_container = QWidget() self.pinned_container = QWidget()
self.pinned_layout = QHBoxLayout(self.pinned_container) self.pinned_layout = QHBoxLayout(self.pinned_container)
self.pinned_layout.setContentsMargins(0, 0, 0, 0) self.pinned_layout.setContentsMargins(0, 0, 0, 0)
self.pinned_layout.setSpacing(4) self.pinned_layout.setSpacing(4)
layout.addWidget(self.pinned_container) self.main_layout.addWidget(self.pinned_container)
# Spacer to push icons together # Spacer to push icons together
layout.addStretch() self.main_layout.addStretch()
# === CLOCK AREA ===
self.clock_widget = QWidget()
clock_layout = QHBoxLayout(self.clock_widget)
clock_layout.setContentsMargins(0, 0, 0, 0)
clock_layout.setSpacing(4)
# === SYSTEM TRAY AREA ===
# Clock icon # Clock icon
self.clock_icon = QLabel() self.clock_icon = QLabel()
clock_pixmap = self.icon_manager.get_pixmap("clock", size=14) clock_pixmap = self.icon_manager.get_pixmap("clock", size=14)
self.clock_icon.setPixmap(clock_pixmap) self.clock_icon.setPixmap(clock_pixmap)
self.clock_icon.setStyleSheet("padding-right: 4px;") self.clock_icon.setStyleSheet("padding-right: 4px;")
layout.addWidget(self.clock_icon) clock_layout.addWidget(self.clock_icon)
# Clock time # Clock time
self.clock_label = QLabel("12:00") self.clock_label = QLabel("12:00")
@ -213,7 +242,19 @@ class WindowsTaskbar(QFrame):
padding: 0 8px; padding: 0 8px;
""") """)
self.clock_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.clock_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.clock_label) clock_layout.addWidget(self.clock_label)
self.main_layout.addWidget(self.clock_widget)
# Clock visibility
self.clock_widget.setVisible(self.config.show_clock)
# === RESIZE HANDLE (right side) ===
self.resize_handle = QFrame()
self.resize_handle.setFixedSize(8, 40)
self.resize_handle.setStyleSheet("background: transparent; cursor: size-hor-cursor;")
self.resize_handle.setToolTip("Drag to resize")
self.main_layout.addWidget(self.resize_handle)
# Start clock update timer # Start clock update timer
self.clock_timer = QTimer(self) self.clock_timer = QTimer(self)
@ -227,6 +268,9 @@ class WindowsTaskbar(QFrame):
# Refresh pinned plugins # Refresh pinned plugins
self._refresh_pinned_plugins() self._refresh_pinned_plugins()
# Set initial size and position
self._apply_size_and_position()
def _create_plugin_button(self, plugin_id: str, plugin_class) -> QPushButton: def _create_plugin_button(self, plugin_id: str, plugin_class) -> QPushButton:
"""Create a pinned plugin button (taskbar icon style).""" """Create a pinned plugin button (taskbar icon style)."""
@ -352,6 +396,83 @@ class WindowsTaskbar(QFrame):
plugins_layout.addStretch() plugins_layout.addStretch()
layout.addWidget(plugins_widget) layout.addWidget(plugins_widget)
def mousePressEvent(self, event: QMouseEvent):
"""Handle mouse press for dragging."""
if event.button() == Qt.MouseButton.LeftButton:
# Check if clicking on drag handle
if self.drag_handle.geometry().contains(event.pos()):
self._dragging = True
self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
# Check if clicking on resize handle
elif self.resize_handle.geometry().contains(event.pos()):
self._resizing = True
self._resize_start_x = event.globalPosition().x()
self._resize_start_width = self.width()
event.accept()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QMouseEvent):
"""Handle mouse move for dragging and resizing."""
if self._dragging:
new_pos = event.globalPosition().toPoint() - self._drag_offset
self.move(new_pos)
# Save position
self.config.x = new_pos.x()
self.config.y = new_pos.y()
self._save_config()
event.accept()
elif getattr(self, '_resizing', False):
delta = event.globalPosition().x() - self._resize_start_x
new_width = max(400, self._resize_start_width + delta)
self.setFixedWidth(int(new_width))
self.config.width = int(new_width)
self._save_config()
event.accept()
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event: QMouseEvent):
"""Handle mouse release."""
if event.button() == Qt.MouseButton.LeftButton:
self._dragging = False
self._resizing = False
super().mouseReleaseEvent(event)
def _apply_opacity(self):
"""Apply background opacity from config."""
opacity = self.config.background_opacity / 100.0
# Create a background widget with opacity
bg_color = f"rgba(20, 31, 35, {opacity})"
self.setStyleSheet(f"""
WindowsTaskbar {{
background: {bg_color};
border: 1px solid rgba(255, 140, 66, 0.1);
border-radius: 12px;
}}
""")
def _apply_size_and_position(self):
"""Apply saved size and position."""
self.setFixedHeight(56)
self.setFixedWidth(self.config.width)
if self.config.position == "custom":
self.move(self.config.x, self.config.y)
elif self.config.position == "bottom":
screen = QApplication.primaryScreen().geometry()
self.move(
(screen.width() - self.config.width) // 2,
screen.height() - 80
)
elif self.config.position == "top":
screen = QApplication.primaryScreen().geometry()
self.move(
(screen.width() - self.config.width) // 2,
20
)
def _create_drawer_item(self, plugin_id: str, plugin_class) -> QPushButton: def _create_drawer_item(self, plugin_id: str, plugin_class) -> QPushButton:
"""Create a drawer item (like Start menu app).""" """Create a drawer item (like Start menu app)."""
icon_name = getattr(plugin_class, 'icon_name', 'grid') icon_name = getattr(plugin_class, 'icon_name', 'grid')
@ -433,16 +554,52 @@ class WindowsTaskbar(QFrame):
} }
""") """)
settings_action = menu.addAction("Settings") # Toggle search
settings_action.triggered.connect(self._show_settings) search_action = menu.addAction("☑ Search" if self.config.show_search else "☐ Search")
search_action.triggered.connect(self._toggle_search)
# Toggle clock
clock_action = menu.addAction("☑ Clock" if self.config.show_clock else "☐ Clock")
clock_action.triggered.connect(self._toggle_clock)
menu.addSeparator() menu.addSeparator()
# Settings
settings_action = menu.addAction("Settings...")
settings_action.triggered.connect(self._show_settings)
# Reset position
reset_action = menu.addAction("Reset Position")
reset_action.triggered.connect(self._reset_position)
menu.addSeparator()
# Hide
hide_action = menu.addAction("Hide") hide_action = menu.addAction("Hide")
hide_action.triggered.connect(self.hide) hide_action.triggered.connect(self.hide)
menu.exec(self.mapToGlobal(position)) menu.exec(self.mapToGlobal(position))
def _toggle_search(self):
"""Toggle search box visibility."""
self.config.show_search = not self.config.show_search
self.search_box.setVisible(self.config.show_search)
self._save_config()
def _toggle_clock(self):
"""Toggle clock visibility."""
self.config.show_clock = not self.config.show_clock
self.clock_widget.setVisible(self.config.show_clock)
self._save_config()
def _reset_position(self):
"""Reset bar position to default."""
self.config.position = "bottom"
self.config.x = 100
self.config.y = 800
self._apply_size_and_position()
self._save_config()
def _show_settings(self): def _show_settings(self):
"""Show settings dialog.""" """Show settings dialog."""
dialog = TaskbarSettingsDialog(self.config, self) dialog = TaskbarSettingsDialog(self.config, self)
@ -457,15 +614,15 @@ class WindowsTaskbar(QFrame):
# Set auto-hide timer # Set auto-hide timer
self.hide_timer.setInterval(self.config.auto_hide_delay) self.hide_timer.setInterval(self.config.auto_hide_delay)
# Position bar # Apply opacity
screen = QApplication.primaryScreen().geometry() self._apply_opacity()
if self.config.position == "bottom":
self.move((screen.width() - 600) // 2, screen.height() - 60)
else: # top
self.move((screen.width() - 600) // 2, 10)
# Size # Apply size and position
self.setFixedSize(600, 50) self._apply_size_and_position()
# Show/hide search and clock
self.search_box.setVisible(self.config.show_search)
self.clock_widget.setVisible(self.config.show_clock)
def _auto_hide(self): def _auto_hide(self):
"""Auto-hide when mouse leaves.""" """Auto-hide when mouse leaves."""
@ -484,24 +641,6 @@ class WindowsTaskbar(QFrame):
self.hide_timer.start() self.hide_timer.start()
super().leaveEvent(event) super().leaveEvent(event)
def mousePressEvent(self, event: QMouseEvent):
"""Start dragging."""
if event.button() == Qt.MouseButton.LeftButton:
self._dragging = True
self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event: QMouseEvent):
"""Drag window."""
if self._dragging:
new_pos = event.globalPosition().toPoint() - self._drag_offset
self.move(new_pos)
def mouseReleaseEvent(self, event: QMouseEvent):
"""Stop dragging."""
if event.button() == Qt.MouseButton.LeftButton:
self._dragging = False
def _load_config(self) -> ActivityBarConfig: def _load_config(self) -> ActivityBarConfig:
"""Load configuration.""" """Load configuration."""
config_path = Path("config/activity_bar.json") config_path = Path("config/activity_bar.json")
@ -526,36 +665,85 @@ class TaskbarSettingsDialog(QDialog):
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("Taskbar Settings") self.setWindowTitle("Activity Bar Settings")
self.setMinimumSize(350, 300) self.setMinimumSize(400, 450)
self._setup_ui() self._setup_ui()
def _setup_ui(self): def _setup_ui(self):
"""Setup settings UI.""" """Setup settings UI."""
from PyQt6.QtWidgets import QVBoxLayout, QFormLayout, QDialogButtonBox from PyQt6.QtWidgets import QVBoxLayout, QFormLayout, QDialogButtonBox, QGroupBox
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
form = QFormLayout() # Appearance Group
appear_group = QGroupBox("Appearance")
appear_form = QFormLayout(appear_group)
# Auto-hide # Opacity
self.autohide_cb = QCheckBox("Auto-hide when not in use") self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
self.autohide_cb.setChecked(self.config.auto_hide) self.opacity_slider.setRange(20, 100)
form.addRow(self.autohide_cb) self.opacity_slider.setValue(self.config.background_opacity)
self.opacity_label = QLabel(f"{self.config.background_opacity}%")
# Position self.opacity_slider.valueChanged.connect(lambda v: self.opacity_label.setText(f"{v}%"))
self.position_combo = QComboBox() opacity_layout = QHBoxLayout()
self.position_combo.addItems(["Bottom", "Top"]) opacity_layout.addWidget(self.opacity_slider)
self.position_combo.setCurrentText(self.config.position.title()) opacity_layout.addWidget(self.opacity_label)
form.addRow("Position:", self.position_combo) appear_form.addRow("Background Opacity:", opacity_layout)
# Icon size # Icon size
self.icon_size = QSpinBox() self.icon_size = QSpinBox()
self.icon_size.setRange(24, 48) self.icon_size.setRange(24, 48)
self.icon_size.setValue(self.config.icon_size) self.icon_size.setValue(self.config.icon_size)
form.addRow("Icon Size:", self.icon_size) appear_form.addRow("Icon Size:", self.icon_size)
layout.addLayout(form) layout.addWidget(appear_group)
# Features Group
features_group = QGroupBox("Features")
features_form = QFormLayout(features_group)
# Show search
self.show_search_cb = QCheckBox("Show search box")
self.show_search_cb.setChecked(self.config.show_search)
features_form.addRow(self.show_search_cb)
# Show clock
self.show_clock_cb = QCheckBox("Show clock")
self.show_clock_cb.setChecked(self.config.show_clock)
features_form.addRow(self.show_clock_cb)
layout.addWidget(features_group)
# Position Group
pos_group = QGroupBox("Position")
pos_form = QFormLayout(pos_group)
# Position
self.position_combo = QComboBox()
self.position_combo.addItems(["Bottom", "Top", "Custom"])
self.position_combo.setCurrentText(self.config.position.title())
pos_form.addRow("Position:", self.position_combo)
# Width
self.width_spin = QSpinBox()
self.width_spin.setRange(400, 1600)
self.width_spin.setValue(self.config.width)
pos_form.addRow("Width:", self.width_spin)
layout.addWidget(pos_group)
# Behavior Group
behav_group = QGroupBox("Behavior")
behav_form = QFormLayout(behav_group)
# Auto-hide
self.autohide_cb = QCheckBox("Auto-hide when not in use")
self.autohide_cb.setChecked(self.config.auto_hide)
behav_form.addRow(self.autohide_cb)
layout.addWidget(behav_group)
layout.addStretch()
# Buttons # Buttons
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
@ -570,9 +758,16 @@ class TaskbarSettingsDialog(QDialog):
return ActivityBarConfig( return ActivityBarConfig(
enabled=True, enabled=True,
position=self.position_combo.currentText().lower(), position=self.position_combo.currentText().lower(),
x=self.config.x,
y=self.config.y,
width=self.width_spin.value(),
icon_size=self.icon_size.value(), icon_size=self.icon_size.value(),
auto_hide=self.autohide_cb.isChecked(), auto_hide=self.autohide_cb.isChecked(),
auto_hide_delay=self.config.auto_hide_delay, auto_hide_delay=self.config.auto_hide_delay,
auto_show_on_focus=self.config.auto_show_on_focus,
background_opacity=self.opacity_slider.value(),
show_search=self.show_search_cb.isChecked(),
show_clock=self.show_clock_cb.isChecked(),
pinned_plugins=self.config.pinned_plugins pinned_plugins=self.config.pinned_plugins
) )

View File

@ -180,7 +180,8 @@ class EUUtilityApp:
# Get overlay controller with window manager # Get overlay controller with window manager
self.overlay_controller = get_overlay_controller( self.overlay_controller = get_overlay_controller(
self.activity_bar, self.activity_bar,
getattr(self, 'window_manager', None) getattr(self, 'window_manager', None),
parent=self.app # Pass QApplication as parent
) )
# Set mode from settings (default: game focused - Blish HUD style) # Set mode from settings (default: game focused - Blish HUD style)

View File

@ -15,7 +15,7 @@ Modes:
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from PyQt6.QtCore import QTimer, pyqtSignal from PyQt6.QtCore import QTimer, pyqtSignal, QObject
class OverlayMode(Enum): class OverlayMode(Enum):
@ -57,7 +57,7 @@ class OverlayConfig:
) )
class OverlayController: class OverlayController(QObject):
""" """
Controls activity bar visibility based on selected mode. Controls activity bar visibility based on selected mode.
@ -69,7 +69,8 @@ class OverlayController:
visibility_changed = pyqtSignal(bool) # is_visible visibility_changed = pyqtSignal(bool) # is_visible
def __init__(self, activity_bar, window_manager=None): def __init__(self, activity_bar, window_manager=None, parent=None):
super().__init__(parent)
self.activity_bar = activity_bar self.activity_bar = activity_bar
self.window_manager = window_manager self.window_manager = window_manager
self.config = OverlayConfig() self.config = OverlayConfig()
@ -240,11 +241,11 @@ class OverlayController:
# Singleton instance # Singleton instance
_overlay_controller = None _overlay_controller = None
def get_overlay_controller(activity_bar=None, window_manager=None): def get_overlay_controller(activity_bar=None, window_manager=None, parent=None):
"""Get or create the overlay controller singleton.""" """Get or create the overlay controller singleton."""
global _overlay_controller global _overlay_controller
if _overlay_controller is None: if _overlay_controller is None:
if activity_bar is None: if activity_bar is None:
raise ValueError("activity_bar required for first initialization") raise ValueError("activity_bar required for first initialization")
_overlay_controller = OverlayController(activity_bar, window_manager) _overlay_controller = OverlayController(activity_bar, window_manager, parent)
return _overlay_controller return _overlay_controller