diff --git a/core/activity_bar.py b/core/activity_bar.py index 0595b6b..b3358ad 100644 --- a/core/activity_bar.py +++ b/core/activity_bar.py @@ -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 ) diff --git a/core/main.py b/core/main.py index a35f666..be570c7 100644 --- a/core/main.py +++ b/core/main.py @@ -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) diff --git a/core/overlay_controller.py b/core/overlay_controller.py index 7aa406c..0d08a7b 100644 --- a/core/overlay_controller.py +++ b/core/overlay_controller.py @@ -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