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:
"""Activity bar configuration."""
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
auto_hide: bool = True
auto_hide: bool = False # Disabled by default for easier use
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
def __post_init__(self):
@ -42,10 +48,16 @@ class ActivityBarConfig:
return {
'enabled': self.enabled,
'position': self.position,
'x': self.x,
'y': self.y,
'width': self.width,
'icon_size': self.icon_size,
'auto_hide': self.auto_hide,
'auto_hide_delay': self.auto_hide_delay,
'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
}
@ -54,9 +66,16 @@ class ActivityBarConfig:
return cls(
enabled=data.get('enabled', True),
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),
auto_hide=data.get('auto_hide', True),
auto_hide=data.get('auto_hide', False),
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', [])
)
@ -115,19 +134,21 @@ class WindowsTaskbar(QFrame):
self._drag_offset = QPoint()
def _setup_ui(self):
"""Setup Windows taskbar-style UI."""
"""Setup Windows taskbar-style UI with draggable and resizable features."""
# Main layout with minimal margins
layout = QHBoxLayout(self)
layout.setContentsMargins(8, 4, 8, 4)
layout.setSpacing(4)
self.main_layout = QHBoxLayout(self)
self.main_layout.setContentsMargins(8, 4, 8, 4)
self.main_layout.setSpacing(4)
# Style: No background, just floating elements
self.setStyleSheet("""
WindowsTaskbar {
background: transparent;
border: none;
}
""")
# Apply opacity from config
self._apply_opacity()
# === DRAG HANDLE (invisible, left side) ===
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) ===
self.start_btn = QPushButton()
@ -150,7 +171,7 @@ class WindowsTaskbar(QFrame):
""")
self.start_btn.setToolTip("Open App 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) ===
self.search_box = QLineEdit()
@ -179,31 +200,39 @@ class WindowsTaskbar(QFrame):
""")
self.search_box.returnPressed.connect(self._on_search)
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 = QFrame()
separator.setFixedSize(1, 24)
separator.setStyleSheet("background: rgba(255, 255, 255, 0.1);")
layout.addWidget(separator)
self.main_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)
self.main_layout.addWidget(self.pinned_container)
# 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
self.clock_icon = QLabel()
clock_pixmap = self.icon_manager.get_pixmap("clock", size=14)
self.clock_icon.setPixmap(clock_pixmap)
self.clock_icon.setStyleSheet("padding-right: 4px;")
layout.addWidget(self.clock_icon)
clock_layout.addWidget(self.clock_icon)
# Clock time
self.clock_label = QLabel("12:00")
@ -213,7 +242,19 @@ class WindowsTaskbar(QFrame):
padding: 0 8px;
""")
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
self.clock_timer = QTimer(self)
@ -227,6 +268,9 @@ class WindowsTaskbar(QFrame):
# 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:
"""Create a pinned plugin button (taskbar icon style)."""
@ -352,6 +396,83 @@ class WindowsTaskbar(QFrame):
plugins_layout.addStretch()
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:
"""Create a drawer item (like Start menu app)."""
icon_name = getattr(plugin_class, 'icon_name', 'grid')
@ -433,16 +554,52 @@ class WindowsTaskbar(QFrame):
}
""")
settings_action = menu.addAction("Settings")
settings_action.triggered.connect(self._show_settings)
# Toggle search
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()
# 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.triggered.connect(self.hide)
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):
"""Show settings dialog."""
dialog = TaskbarSettingsDialog(self.config, self)
@ -457,15 +614,15 @@ class WindowsTaskbar(QFrame):
# 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)
# Apply opacity
self._apply_opacity()
# Size
self.setFixedSize(600, 50)
# Apply size and position
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):
"""Auto-hide when mouse leaves."""
@ -484,24 +641,6 @@ class WindowsTaskbar(QFrame):
self.hide_timer.start()
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:
"""Load configuration."""
config_path = Path("config/activity_bar.json")
@ -526,36 +665,85 @@ class TaskbarSettingsDialog(QDialog):
def __init__(self, config: ActivityBarConfig, parent=None):
super().__init__(parent)
self.config = config
self.setWindowTitle("Taskbar Settings")
self.setMinimumSize(350, 300)
self.setWindowTitle("Activity Bar Settings")
self.setMinimumSize(400, 450)
self._setup_ui()
def _setup_ui(self):
"""Setup settings UI."""
from PyQt6.QtWidgets import QVBoxLayout, QFormLayout, QDialogButtonBox
from PyQt6.QtWidgets import QVBoxLayout, QFormLayout, QDialogButtonBox, QGroupBox
layout = QVBoxLayout(self)
form = QFormLayout()
# Appearance Group
appear_group = QGroupBox("Appearance")
appear_form = QFormLayout(appear_group)
# 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)
# 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)
# Opacity
self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
self.opacity_slider.setRange(20, 100)
self.opacity_slider.setValue(self.config.background_opacity)
self.opacity_label = QLabel(f"{self.config.background_opacity}%")
self.opacity_slider.valueChanged.connect(lambda v: self.opacity_label.setText(f"{v}%"))
opacity_layout = QHBoxLayout()
opacity_layout.addWidget(self.opacity_slider)
opacity_layout.addWidget(self.opacity_label)
appear_form.addRow("Background Opacity:", opacity_layout)
# 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)
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 = QDialogButtonBox(
@ -570,9 +758,16 @@ class TaskbarSettingsDialog(QDialog):
return ActivityBarConfig(
enabled=True,
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(),
auto_hide=self.autohide_cb.isChecked(),
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
)

View File

@ -180,7 +180,8 @@ class EUUtilityApp:
# Get overlay controller with window manager
self.overlay_controller = get_overlay_controller(
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)

View File

@ -15,7 +15,7 @@ Modes:
from enum import Enum
from dataclasses import dataclass
from typing import Optional
from PyQt6.QtCore import QTimer, pyqtSignal
from PyQt6.QtCore import QTimer, pyqtSignal, QObject
class OverlayMode(Enum):
@ -57,7 +57,7 @@ class OverlayConfig:
)
class OverlayController:
class OverlayController(QObject):
"""
Controls activity bar visibility based on selected mode.
@ -69,7 +69,8 @@ class OverlayController:
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.window_manager = window_manager
self.config = OverlayConfig()
@ -240,11 +241,11 @@ class OverlayController:
# Singleton instance
_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."""
global _overlay_controller
if _overlay_controller is None:
if activity_bar is None:
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