refactor: Consolidate activity_bar - replace with enhanced version

This commit is contained in:
devmatrix 2026-02-16 20:59:51 +00:00
parent af2a1c0b12
commit b863a0f17b
3 changed files with 1558 additions and 1351 deletions

991
API.md Normal file
View File

@ -0,0 +1,991 @@
# EU-Utility API Reference
> Complete API documentation for plugin developers
**Version:** 2.1.0
**Last Updated:** 2026-02-16
---
## Table of Contents
1. [Getting Started](#getting-started)
2. [PluginAPI](#pluginapi) - Core services access
3. [WidgetAPI](#widgetapi) - Overlay widget management
4. [ExternalAPI](#externalapi) - REST endpoints and webhooks
5. [Event Bus](#event-bus) - Pub/sub event system
6. [BasePlugin Class](#baseplugin-class) - Plugin base class reference
7. [Nexus API](#nexus-api) - Entropia Nexus integration
8. [Code Examples](#code-examples) - Practical examples
9. [Best Practices](#best-practices)
---
## Getting Started
### Importing the API
```python
from core.api import get_api
class MyPlugin(BasePlugin):
def initialize(self):
self.api = get_api()
```
### API Availability
Always check if a service is available before using it:
```python
if self.api.ocr_available():
text = self.api.recognize_text()
```
---
## PluginAPI
The PluginAPI provides access to all core EU-Utility services.
### Window Manager
```python
# Get EU window information
window = api.get_eu_window()
if window:
print(f"Position: {window['x']}, {window['y']}")
print(f"Size: {window['width']}x{window['height']}")
print(f"Focused: {window['is_focused']}")
# Check if EU is focused
if api.is_eu_focused():
api.play_sound("alert.wav")
# Bring EU to front
api.bring_eu_to_front()
# Check if EU window is visible
is_visible = api.is_eu_visible()
```
### OCR Service
```python
# Check OCR availability
if api.ocr_available():
# Read text from screen region
text = api.recognize_text(region=(100, 100, 200, 50))
print(f"Found: {text}")
# Alternative: Use BasePlugin helper method
result = self.ocr_capture(region=(x, y, width, height))
# Returns: {'text': str, 'confidence': float, 'error': str or None}
```
### Screenshot Service
```python
# Check availability
if api.screenshot_available():
# Capture full screen
img = api.capture_screen()
# Capture specific region
img = api.capture_screen(region=(100, 100, 400, 300))
# Capture and save
img = api.capture_screen(
region=(100, 100, 400, 300),
save_path="screenshot.png"
)
# Alternative: Use BasePlugin helper
img = self.capture_screen(full_screen=True)
img = self.capture_region(x, y, width, height)
```
### Log Reader
```python
# Read recent log lines
lines = api.read_log_lines(count=100)
# Read logs since timestamp
from datetime import datetime, timedelta
recent = api.read_log_since(datetime.now() - timedelta(minutes=5))
# Alternative: Use BasePlugin helper
lines = self.read_log(lines=50, filter_text="loot")
```
### Data Store
```python
# Store data (scoped to plugin)
api.set_data("key", value)
# Retrieve with default
value = api.get_data("key", default=None)
# Delete data
api.delete_data("key")
# Alternative: Use BasePlugin helpers
self.save_data("key", value)
data = self.load_data("key", default=None)
self.delete_data("key")
```
### HTTP Client
```python
# GET request with caching
result = api.http_get(
"https://api.example.com/data",
cache=True,
cache_duration=3600 # 1 hour
)
if result['success']:
data = result['data']
else:
error = result['error']
# POST request
result = api.http_post(
"https://api.example.com/submit",
data={"key": "value"}
)
# Alternative: Use BasePlugin helper
response = self.http_get(url, cache_ttl=300, headers={})
```
### Audio
```python
# Play sound file
api.play_sound("assets/sounds/alert.wav", volume=0.7)
# Simple beep
api.beep()
# Alternative: Use BasePlugin helpers
self.play_sound("hof") # Predefined: 'hof', 'skill_gain', 'alert'
self.set_volume(0.8)
volume = self.get_volume()
```
### Notifications
```python
# Show toast notification
api.show_notification(
title="Loot Alert!",
message="You found something valuable!",
duration=5000, # milliseconds
sound=True
)
# Alternative: Use BasePlugin helpers
self.notify(title, message, notification_type='info', sound=False)
self.notify_info(title, message)
self.notify_success(title, message)
self.notify_warning(title, message)
self.notify_error(title, message, sound=True)
```
### Clipboard
```python
# Copy to clipboard
api.copy_to_clipboard("Text to copy")
# Paste from clipboard
text = api.paste_from_clipboard()
# Alternative: Use BasePlugin helpers
self.copy_to_clipboard(text)
text = self.paste_from_clipboard()
```
### Background Tasks
```python
# Run function in background
def heavy_computation(data):
# Long running task
import time
time.sleep(2)
return f"Processed: {data}"
def on_complete(result):
print(f"Done: {result}")
def on_error(error):
print(f"Error: {error}")
task_id = api.run_task(
heavy_computation,
"my data",
callback=on_complete,
error_handler=on_error
)
# Cancel task
api.cancel_task(task_id)
# Alternative: Use BasePlugin helper
self.run_in_background(func, *args, priority='normal',
on_complete=cb, on_error=err_cb)
```
---
## WidgetAPI
Manage overlay widgets - floating UI components.
### Getting Started
```python
from core.api import get_widget_api
widget_api = get_widget_api()
# Create widget
widget = widget_api.create_widget(
name="loot_tracker",
title="Loot Tracker",
size=(400, 300),
position=(100, 100)
)
widget.show()
```
### Widget Operations
```python
# Show/hide
widget.show()
widget.hide()
# Position
widget.move(500, 200)
x, y = widget.position
# Size
widget.resize(400, 300)
width, height = widget.size
# Opacity (0.0 - 1.0)
widget.set_opacity(0.8)
# Lock/unlock (prevent dragging)
widget.set_locked(True)
# Minimize/restore
widget.minimize()
widget.restore()
# Close
widget.close()
```
### Widget Management
```python
# Get existing widget
widget = widget_api.get_widget("loot_tracker")
# Show/hide specific widget
widget_api.show_widget("loot_tracker")
widget_api.hide_widget("loot_tracker")
# Close widget
widget_api.close_widget("loot_tracker")
# Global operations
widget_api.show_all_widgets()
widget_api.hide_all_widgets()
widget_api.close_all_widgets()
widget_api.set_all_opacity(0.8)
widget_api.lock_all()
widget_api.unlock_all()
```
### Layout Helpers
```python
# Arrange widgets
widget_api.arrange_widgets(layout="grid", spacing=10)
widget_api.arrange_widgets(layout="horizontal")
widget_api.arrange_widgets(layout="vertical")
widget_api.arrange_widgets(layout="cascade")
# Snap to grid
widget_api.snap_to_grid(grid_size=10)
```
### Widget Events
```python
# Handle events
widget.on('moved', lambda data: print(f"Moved to {data['x']}, {data['y']}"))
widget.on('resized', lambda data: print(f"Sized to {data['width']}x{data['height']}"))
widget.on('closing', lambda: print("Widget closing"))
widget.on('closed', lambda: print("Widget closed"))
widget.on('update', lambda data: print(f"Update: {data}"))
```
---
## ExternalAPI
REST endpoints, webhooks, and third-party integrations.
### Getting Started
```python
from core.api import get_external_api
ext = get_external_api()
# Start server
ext.start_server(port=8080)
# Check status
print(ext.get_status())
```
### REST Endpoints
```python
# Using decorator
@ext.endpoint("stats", methods=["GET"])
def get_stats():
return {"kills": 100, "loot": "50 PED"}
@ext.endpoint("loot", methods=["POST"])
def record_loot(data):
save_loot(data)
return {"status": "saved"}
# Programmatic registration
def get_stats_handler(params):
return {"kills": 100}
ext.register_endpoint("stats", get_stats_handler, methods=["GET"])
# Unregister
ext.unregister_endpoint("stats")
```
### Incoming Webhooks
```python
# Register webhook handler
def handle_discord(payload):
print(f"Discord: {payload}")
return {"status": "ok"}
ext.register_webhook(
name="discord",
handler=handle_discord,
secret="my_secret" # Optional HMAC verification
)
# POST to: http://localhost:8080/webhook/discord
```
### Outgoing Webhooks
```python
# POST to external webhook
result = ext.post_webhook(
"https://discord.com/api/webhooks/...",
{"content": "Hello from EU-Utility!"}
)
if result['success']:
print("Sent!")
else:
print(f"Error: {result['error']}")
```
---
## Event Bus
Typed publish-subscribe event system for inter-plugin communication.
### Event Types
```python
from core.event_bus import (
SkillGainEvent,
LootEvent,
DamageEvent,
GlobalEvent,
ChatEvent,
EconomyEvent,
SystemEvent
)
```
### Publishing Events
```python
from core.event_bus import LootEvent, SkillGainEvent
# Publish loot event
self.publish_typed(LootEvent(
mob_name="Daikiba",
items=[{"name": "Animal Oil", "value": 0.05}],
total_tt_value=0.05
))
# Publish skill gain
self.publish_typed(SkillGainEvent(
skill_name="Rifle",
skill_value=25.5,
gain_amount=0.01
))
# Legacy event publishing
api.publish("my_plugin.event", {"data": "value"})
```
### Subscribing to Events
```python
from core.event_bus import LootEvent, SkillGainEvent
class MyPlugin(BasePlugin):
def initialize(self):
# Subscribe to loot events
self.sub_id = self.subscribe_typed(
LootEvent,
self.on_loot
)
# Subscribe with filtering
self.sub_id2 = self.subscribe_typed(
LootEvent,
self.on_dragon_loot,
mob_types=["Dragon", "Drake"]
)
# Subscribe to specific skills
self.sub_id3 = self.subscribe_typed(
SkillGainEvent,
self.on_combat_skill,
skill_names=["Rifle", "Pistol", "Melee"]
)
def on_loot(self, event: LootEvent):
print(f"Loot from {event.mob_name}: {event.items}")
def on_dragon_loot(self, event: LootEvent):
print(f"Dragon loot! Total TT: {event.total_tt_value}")
def shutdown(self):
# Clean up subscriptions
self.unsubscribe_typed(self.sub_id)
self.unsubscribe_typed(self.sub_id2)
self.unsubscribe_typed(self.sub_id3)
# Or: self.unsubscribe_all_typed()
```
### Event Attributes
| Event | Attributes |
|-------|------------|
| `SkillGainEvent` | `skill_name`, `skill_value`, `gain_amount` |
| `LootEvent` | `mob_name`, `items`, `total_tt_value`, `position` |
| `DamageEvent` | `damage_amount`, `damage_type`, `is_critical`, `target_name` |
| `GlobalEvent` | `player_name`, `achievement_type`, `value`, `item_name` |
| `ChatEvent` | `channel`, `sender`, `message` |
| `EconomyEvent` | `transaction_type`, `amount`, `currency`, `description` |
| `SystemEvent` | `message`, `severity` |
### Legacy Event Bus
```python
# Subscribe
sub_id = api.subscribe("loot", on_loot_callback)
# Unsubscribe
api.unsubscribe(sub_id)
# Publish
api.publish("event_type", {"key": "value"})
# Get history
history = api.get_event_history("loot", limit=10)
```
---
## BasePlugin Class
Complete reference for the base plugin class.
### Required Attributes
```python
class MyPlugin(BasePlugin):
name = "My Plugin" # Display name
version = "1.0.0" # Version string
author = "Your Name" # Author name
description = "..." # Short description
```
### Optional Attributes
```python
class MyPlugin(BasePlugin):
icon = "path/to/icon.png" # Icon path
hotkey = "ctrl+shift+y" # Legacy single hotkey
hotkeys = [ # Multi-hotkey format
{
'action': 'toggle',
'description': 'Toggle My Plugin',
'default': 'ctrl+shift+m',
'config_key': 'myplugin_toggle'
}
]
enabled = True # Start enabled
dependencies = { # Dependencies
'pip': ['requests', 'numpy'],
'plugins': ['other_plugin'],
'optional': {'pillow': 'Image processing'}
}
```
### Lifecycle Methods
```python
def initialize(self) -> None:
"""Called when plugin is loaded."""
self.api = get_api()
self.log_info("Initialized!")
def get_ui(self) -> QWidget:
"""Return the plugin's UI widget."""
widget = QWidget()
# ... setup UI ...
return widget
def on_show(self) -> None:
"""Called when overlay becomes visible."""
pass
def on_hide(self) -> None:
"""Called when overlay is hidden."""
pass
def on_hotkey(self) -> None:
"""Called when hotkey is pressed."""
pass
def shutdown(self) -> None:
"""Called when app is closing. Cleanup resources."""
self.unsubscribe_all_typed()
super().shutdown()
```
### Config Methods
```python
# Get config value
theme = self.get_config('theme', default='dark')
# Set config value
self.set_config('theme', 'light')
```
### Logging Methods
```python
self.log_debug("Debug message")
self.log_info("Info message")
self.log_warning("Warning message")
self.log_error("Error message")
```
### Utility Methods
```python
# Format currency
ped_text = self.format_ped(123.45) # "123.45 PED"
pec_text = self.format_pec(50) # "50 PEC"
# Calculate DPP
dpp = self.calculate_dpp(damage=45.0, ammo=100, decay=2.5)
# Calculate markup
markup = self.calculate_markup(price=150.0, tt=100.0) # 150.0%
```
---
## Nexus API
Entropia Nexus integration for game data.
### Searching
```python
# Search for items
results = self.nexus_search("ArMatrix", entity_type="items")
# Search for mobs
mobs = self.nexus_search("Atrox", entity_type="mobs")
# Search for locations
locations = self.nexus_search("Fort", entity_type="locations")
```
### Entity Types
- `items`, `weapons`, `armors`
- `mobs`, `pets`
- `blueprints`, `materials`
- `locations`, `teleporters`, `shops`, `vendors`, `planets`, `areas`
- `skills`
- `enhancers`, `medicaltools`, `finders`, `excavators`, `refiners`
- `vehicles`, `decorations`, `furniture`
- `storagecontainers`, `strongboxes`
### Getting Details
```python
# Get item details
details = self.nexus_get_item_details("armatrix_lp-35")
if details:
print(f"Name: {details['name']}")
print(f"TT Value: {details['tt_value']} PED")
# Get market data
market = self.nexus_get_market_data("armatrix_lp-35")
if market:
print(f"Current markup: {market['current_markup']:.1f}%")
```
---
## Code Examples
### Example 1: Simple Calculator Plugin
```python
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton
)
class MarkupCalculatorPlugin(BasePlugin):
"""Calculate markup percentages."""
name = "Markup Calculator"
version = "1.0.0"
author = "Tutorial"
description = "Calculate item markup"
hotkey = "ctrl+shift+m"
def initialize(self):
self.log_info("Markup Calculator initialized!")
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
# Title
title = QLabel("Markup Calculator")
title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
layout.addWidget(title)
# TT Value input
layout.addWidget(QLabel("TT Value:"))
self.tt_input = QLineEdit()
self.tt_input.setPlaceholderText("100.00")
layout.addWidget(self.tt_input)
# Market Price input
layout.addWidget(QLabel("Market Price:"))
self.price_input = QLineEdit()
self.price_input.setPlaceholderText("150.00")
layout.addWidget(self.price_input)
# Calculate button
calc_btn = QPushButton("Calculate")
calc_btn.clicked.connect(self.calculate)
layout.addWidget(calc_btn)
# Result
self.result_label = QLabel("Markup: -")
self.result_label.setStyleSheet("color: #ffc107; font-size: 16px;")
layout.addWidget(self.result_label)
layout.addStretch()
return widget
def calculate(self):
try:
tt = float(self.tt_input.text() or 0)
price = float(self.price_input.text() or 0)
markup = self.calculate_markup(price, tt)
self.result_label.setText(f"Markup: {markup:.1f}%")
except ValueError:
self.result_label.setText("Invalid input")
```
### Example 2: Event-Driven Tracker
```python
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QListWidget
from core.event_bus import LootEvent, SkillGainEvent
class ActivityTrackerPlugin(BasePlugin):
"""Track recent activity from events."""
name = "Activity Tracker"
version = "1.0.0"
def initialize(self):
self.subscriptions = []
# Subscribe to multiple event types
sub1 = self.subscribe_typed(LootEvent, self.on_loot)
sub2 = self.subscribe_typed(SkillGainEvent, self.on_skill)
self.subscriptions.extend([sub1, sub2])
self.log_info("Activity Tracker initialized")
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("Recent Activity:"))
self.activity_list = QListWidget()
layout.addWidget(self.activity_list)
return widget
def on_loot(self, event: LootEvent):
text = f"Loot: {event.mob_name} - {event.total_tt_value:.2f} PED"
self.activity_list.insertItem(0, text)
def on_skill(self, event: SkillGainEvent):
text = f"Skill: {event.skill_name} +{event.gain_amount:.4f}"
self.activity_list.insertItem(0, text)
def shutdown(self):
for sub_id in self.subscriptions:
self.unsubscribe_typed(sub_id)
```
### Example 3: Background Task Plugin
```python
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QPushButton,
QLabel, QProgressBar
)
class DataFetcherPlugin(BasePlugin):
"""Fetch data in background."""
name = "Data Fetcher"
version = "1.0.0"
def initialize(self):
self.log_info("Data Fetcher initialized")
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
self.status_label = QLabel("Ready")
layout.addWidget(self.status_label)
self.progress = QProgressBar()
self.progress.setRange(0, 0) # Indeterminate
self.progress.hide()
layout.addWidget(self.progress)
fetch_btn = QPushButton("Fetch Data")
fetch_btn.clicked.connect(self.fetch_data)
layout.addWidget(fetch_btn)
layout.addStretch()
return widget
def fetch_data(self):
self.status_label.setText("Fetching...")
self.progress.show()
# Run in background
self.run_in_background(
self._fetch_from_api,
priority='normal',
on_complete=self._on_fetch_complete,
on_error=self._on_fetch_error
)
def _fetch_from_api(self):
# This runs in background thread
import time
time.sleep(2) # Simulate API call
return {"items": ["A", "B", "C"], "count": 3}
def _on_fetch_complete(self, result):
# This runs in main thread - safe to update UI
self.progress.hide()
self.status_label.setText(f"Got {result['count']} items")
self.notify_success("Data Fetched", f"Retrieved {result['count']} items")
def _on_fetch_error(self, error):
self.progress.hide()
self.status_label.setText(f"Error: {error}")
self.notify_error("Fetch Failed", str(error))
```
---
## Best Practices
### 1. Always Use BasePlugin Helpers
```python
# ✅ Good - Uses helper methods
self.save_data("key", value)
data = self.load_data("key", default)
# ❌ Avoid - Direct API access when helper exists
self.api.set_data("key", value)
```
### 2. Handle Errors Gracefully
```python
def initialize(self):
try:
self.api = get_api()
self.log_info("API connected")
except Exception as e:
self.log_error(f"Failed to get API: {e}")
# Continue with limited functionality
```
### 3. Clean Up Resources
```python
def shutdown(self):
# Unsubscribe from events
self.unsubscribe_all_typed()
# Save any pending data
self.save_data("pending", self.pending_data)
super().shutdown()
```
### 4. Don't Block the Main Thread
```python
# ✅ Good - Use background tasks
def heavy_operation(self):
self.run_in_background(
self._do_heavy_work,
on_complete=self._update_ui
)
# ❌ Avoid - Blocks UI
def heavy_operation(self):
result = self._do_heavy_work() # Blocks!
self._update_ui(result)
```
### 5. Use Type Hints
```python
from typing import Optional, Dict, Any, List
from core.event_bus import LootEvent
def on_loot(self, event: LootEvent) -> None:
items: List[Dict[str, Any]] = event.items
total: float = event.total_tt_value
```
### 6. Follow Naming Conventions
```python
# ✅ Good
class LootTrackerPlugin(BasePlugin):
def on_loot_received(self):
pass
# ❌ Avoid
class lootTracker(BasePlugin):
def loot(self):
pass
```
### 7. Document Your Plugin
```python
class MyPlugin(BasePlugin):
"""
Brief description of what this plugin does.
Features:
- Feature 1
- Feature 2
Usage:
1. Step 1
2. Step 2
Hotkeys:
- Ctrl+Shift+Y: Toggle plugin
"""
```
---
## Error Handling
All APIs provide specific exceptions:
```python
from core.api import (
PluginAPIError,
ServiceNotAvailableError,
ExternalAPIError
)
try:
text = api.recognize_text((0, 0, 100, 100))
except ServiceNotAvailableError:
print("OCR not available")
except PluginAPIError as e:
print(f"API error: {e}")
```
---
## See Also
- [Plugin Development Guide](./docs/PLUGIN_DEVELOPMENT.md) - Detailed plugin tutorial
- [Architecture Overview](./ARCHITECTURE.md) - System architecture
- [Nexus API Reference](./docs/NEXUS_API_REFERENCE.md) - Entropia Nexus API
- [Contributing](./CONTRIBUTING.md) - How to contribute

File diff suppressed because it is too large Load Diff

View File

@ -1,715 +0,0 @@
"""
EU-Utility - Enhanced Activity Bar
Windows 11-style taskbar with pinned plugins, app drawer, and search.
"""
import json
from pathlib import Path
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, asdict
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QLineEdit, QMenu, QDialog, QSlider, QComboBox,
QCheckBox, QSpinBox, QApplication, QSizePolicy, QScrollArea,
QGridLayout, QMessageBox, QGraphicsDropShadowEffect
)
from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont, QIcon, QPixmap, QDrag
from PyQt6.QtCore import QMimeData, QByteArray
from core.data.sqlite_store import get_sqlite_store
@dataclass
class ActivityBarConfig:
"""Activity bar configuration."""
enabled: bool = True
position: str = "bottom"
icon_size: int = 32
auto_hide: bool = False
auto_hide_delay: int = 3000
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
}
@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', False),
auto_hide_delay=data.get('auto_hide_delay', 3000),
pinned_plugins=data.get('pinned_plugins', [])
)
class DraggablePluginButton(QPushButton):
"""Plugin button that supports drag-to-pin."""
drag_started = pyqtSignal(str)
def __init__(self, plugin_id: str, plugin_name: str, icon_text: str, parent=None):
super().__init__(parent)
self.plugin_id = plugin_id
self.plugin_name = plugin_name
self.icon_text = icon_text
self.setText(icon_text)
self.setFixedSize(40, 40)
self.setToolTip(plugin_name)
self._setup_style()
def _setup_style(self):
"""Setup button style."""
self.setStyleSheet("""
DraggablePluginButton {
background: transparent;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
}
DraggablePluginButton:hover {
background: rgba(255, 255, 255, 0.1);
}
DraggablePluginButton:pressed {
background: rgba(255, 255, 255, 0.05);
}
""")
self.setCursor(Qt.CursorShape.PointingHandCursor)
def mousePressEvent(self, event):
"""Start drag on middle click or with modifier."""
if event.button() == Qt.MouseButton.LeftButton:
self.drag_start_pos = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Handle drag."""
if not (event.buttons() & Qt.MouseButton.LeftButton):
return
if not hasattr(self, 'drag_start_pos'):
return
# Check if dragged far enough
if (event.pos() - self.drag_start_pos).manhattanLength() < 10:
return
# Start drag
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setText(self.plugin_id)
mime_data.setData('application/x-plugin-id', QByteArray(self.plugin_id.encode()))
drag.setMimeData(mime_data)
# Create drag pixmap
pixmap = QPixmap(40, 40)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setBrush(QColor(255, 140, 66, 200))
painter.drawRoundedRect(0, 0, 40, 40, 8, 8)
painter.setPen(Qt.GlobalColor.white)
painter.setFont(QFont("Segoe UI", 14))
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, self.icon_text)
painter.end()
drag.setPixmap(pixmap)
drag.setHotSpot(QPoint(20, 20))
self.drag_started.emit(self.plugin_id)
drag.exec(Qt.DropAction.MoveAction)
class PinnedPluginsArea(QFrame):
"""Area for pinned plugins with drop support."""
plugin_pinned = pyqtSignal(str)
plugin_unpinned = pyqtSignal(str)
plugin_reordered = pyqtSignal(list) # New order of plugin IDs
def __init__(self, parent=None):
super().__init__(parent)
self.pinned_plugins: List[str] = []
self.buttons: Dict[str, DraggablePluginButton] = {}
self.setAcceptDrops(True)
self._setup_ui()
def _setup_ui(self):
"""Setup UI."""
self.setStyleSheet("background: transparent; border: none;")
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addStretch()
def add_plugin(self, plugin_id: str, plugin_name: str, icon_text: str = ""):
"""Add a pinned plugin."""
if plugin_id in self.pinned_plugins:
return
self.pinned_plugins.append(plugin_id)
btn = DraggablePluginButton(plugin_id, plugin_name, icon_text)
btn.clicked.connect(lambda: self._on_plugin_clicked(plugin_id))
# Insert before stretch
layout = self.layout()
layout.insertWidget(layout.count() - 1, btn)
self.buttons[plugin_id] = btn
# Log
store = get_sqlite_store()
store.log_activity('ui', 'plugin_pinned', f"Plugin: {plugin_id}")
def remove_plugin(self, plugin_id: str):
"""Remove a pinned plugin."""
if plugin_id not in self.pinned_plugins:
return
self.pinned_plugins.remove(plugin_id)
if plugin_id in self.buttons:
btn = self.buttons[plugin_id]
self.layout().removeWidget(btn)
btn.deleteLater()
del self.buttons[plugin_id]
# Log
store = get_sqlite_store()
store.log_activity('ui', 'plugin_unpinned', f"Plugin: {plugin_id}")
def set_plugins(self, plugins: List[tuple]):
"""Set all pinned plugins."""
# Clear existing
for plugin_id in list(self.pinned_plugins):
self.remove_plugin(plugin_id)
# Add new
for plugin_id, plugin_name, icon_text in plugins:
self.add_plugin(plugin_id, plugin_name, icon_text)
def _on_plugin_clicked(self, plugin_id: str):
"""Handle plugin click."""
parent = self.window()
if parent and hasattr(parent, 'show_plugin'):
parent.show_plugin(plugin_id)
def dragEnterEvent(self, event):
"""Accept drag events."""
if event.mimeData().hasText():
event.acceptProposedAction()
def dropEvent(self, event):
"""Handle drop."""
plugin_id = event.mimeData().text()
self.plugin_pinned.emit(plugin_id)
event.acceptProposedAction()
class AppDrawer(QFrame):
"""App drawer popup with all plugins."""
plugin_launched = pyqtSignal(str)
plugin_pin_requested = pyqtSignal(str)
def __init__(self, plugin_manager, parent=None):
super().__init__(parent)
self.plugin_manager = plugin_manager
self.search_text = ""
self._setup_ui()
def _setup_ui(self):
"""Setup drawer UI."""
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedSize(420, 500)
# Frosted glass effect
self.setStyleSheet("""
AppDrawer {
background: rgba(32, 32, 32, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
""")
# Shadow
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(30)
shadow.setColor(QColor(0, 0, 0, 100))
shadow.setOffset(0, 8)
self.setGraphicsEffect(shadow)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
# Header
header = QLabel("All Plugins")
header.setStyleSheet("color: white; font-size: 18px; font-weight: bold;")
layout.addWidget(header)
# Search box
self.search_box = QLineEdit()
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: 10px;
padding: 10px 15px;
font-size: 14px;
}
QLineEdit:focus {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 140, 66, 0.5);
}
""")
self.search_box.textChanged.connect(self._on_search)
layout.addWidget(self.search_box)
# Plugin grid
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
self.grid_widget = QWidget()
self.grid_layout = QGridLayout(self.grid_widget)
self.grid_layout.setSpacing(10)
self.grid_layout.setContentsMargins(0, 0, 0, 0)
scroll.setWidget(self.grid_widget)
layout.addWidget(scroll)
self._refresh_plugins()
def _refresh_plugins(self):
"""Refresh plugin grid."""
# Clear existing
while self.grid_layout.count():
item = self.grid_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
if not self.plugin_manager:
return
all_plugins = self.plugin_manager.get_all_discovered_plugins()
# Filter by search
filtered = []
for plugin_id, plugin_class in all_plugins.items():
name = plugin_class.name.lower()
desc = plugin_class.description.lower()
search = self.search_text.lower()
if not search or search in name or search in desc:
filtered.append((plugin_id, plugin_class))
# Create items
cols = 3
for i, (plugin_id, plugin_class) in enumerate(filtered):
item = self._create_plugin_item(plugin_id, plugin_class)
row = i // cols
col = i % cols
self.grid_layout.addWidget(item, row, col)
self.grid_layout.setColumnStretch(cols, 1)
self.grid_layout.setRowStretch((len(filtered) // cols) + 1, 1)
def _create_plugin_item(self, plugin_id: str, plugin_class) -> QFrame:
"""Create a plugin item."""
frame = QFrame()
frame.setFixedSize(110, 110)
frame.setStyleSheet("""
QFrame {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
}
QFrame:hover {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
""")
frame.setCursor(Qt.CursorShape.PointingHandCursor)
layout = QVBoxLayout(frame)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(6)
# Icon
icon = QLabel(getattr(plugin_class, 'icon', '📦'))
icon.setStyleSheet("font-size: 28px;")
icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon)
# Name
name = QLabel(plugin_class.name)
name.setStyleSheet("color: white; font-size: 11px; font-weight: bold;")
name.setAlignment(Qt.AlignmentFlag.AlignCenter)
name.setWordWrap(True)
layout.addWidget(name)
# Click handler
frame.mousePressEvent = lambda event, pid=plugin_id: self._on_plugin_clicked(pid)
# Context menu
frame.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
frame.customContextMenuRequested.connect(
lambda pos, pid=plugin_id: self._show_context_menu(pos, pid)
)
return frame
def _on_plugin_clicked(self, plugin_id: str):
"""Handle plugin click."""
self.plugin_launched.emit(plugin_id)
self.hide()
def _show_context_menu(self, pos, plugin_id: str):
"""Show 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);
}
""")
pin_action = menu.addAction("📌 Pin to Taskbar")
pin_action.triggered.connect(lambda: self.plugin_pin_requested.emit(plugin_id))
menu.exec(self.mapToGlobal(pos))
def _on_search(self, text: str):
"""Handle search."""
self.search_text = text
self._refresh_plugins()
class EnhancedActivityBar(QFrame):
"""Enhanced activity bar with drag-to-pin and search."""
plugin_requested = pyqtSignal(str)
search_requested = pyqtSignal(str)
settings_requested = pyqtSignal()
def __init__(self, plugin_manager, parent=None):
super().__init__(parent)
self.plugin_manager = plugin_manager
self.config = self._load_config()
self._setup_ui()
self._apply_config()
# Load pinned plugins
self._load_pinned_plugins()
def _setup_ui(self):
"""Setup activity bar UI."""
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool |
Qt.WindowType.WindowDoesNotAcceptFocus
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedHeight(56)
# Main layout
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 4, 12, 4)
layout.setSpacing(8)
# Style
self.setStyleSheet("""
EnhancedActivityBar {
background: rgba(30, 30, 35, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 28px;
}
""")
# Start button
self.start_btn = QPushButton("")
self.start_btn.setFixedSize(40, 40)
self.start_btn.setStyleSheet("""
QPushButton {
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.2);
}
""")
self.start_btn.setToolTip("Open App Drawer")
self.start_btn.clicked.connect(self._toggle_drawer)
layout.addWidget(self.start_btn)
# Search box
self.search_box = QLineEdit()
self.search_box.setFixedSize(180, 36)
self.search_box.setPlaceholderText("Search...")
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 14px;
font-size: 13px;
}
QLineEdit:focus {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 140, 66, 0.5);
}
""")
self.search_box.returnPressed.connect(self._on_search)
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
self.pinned_area = PinnedPluginsArea()
self.pinned_area.plugin_pinned.connect(self._on_plugin_pinned)
self.pinned_area.setAcceptDrops(True)
layout.addWidget(self.pinned_area)
# Spacer
layout.addStretch()
# Clock
self.clock_label = QLabel("12:00")
self.clock_label.setStyleSheet("color: rgba(255, 255, 255, 0.7); font-size: 12px;")
self.clock_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.clock_label)
# Settings button
self.settings_btn = QPushButton("⚙️")
self.settings_btn.setFixedSize(36, 36)
self.settings_btn.setStyleSheet("""
QPushButton {
background: transparent;
color: rgba(255, 255, 255, 0.7);
border: none;
border-radius: 6px;
font-size: 14px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
""")
self.settings_btn.setToolTip("Settings")
self.settings_btn.clicked.connect(self.settings_requested.emit)
layout.addWidget(self.settings_btn)
# Clock timer
self.clock_timer = QTimer(self)
self.clock_timer.timeout.connect(self._update_clock)
self.clock_timer.start(60000)
self._update_clock()
# Auto-hide timer
self.hide_timer = QTimer(self)
self.hide_timer.timeout.connect(self.hide)
# Drawer
self.drawer = None
# Enable drag-drop
self.setAcceptDrops(True)
def _toggle_drawer(self):
"""Toggle app drawer."""
if self.drawer is None:
self.drawer = AppDrawer(self.plugin_manager, self)
self.drawer.plugin_launched.connect(self.plugin_requested.emit)
self.drawer.plugin_pin_requested.connect(self._pin_plugin)
if self.drawer.isVisible():
self.drawer.hide()
else:
# Position drawer
bar_pos = self.pos()
if self.config.position == "bottom":
self.drawer.move(bar_pos.x(), bar_pos.y() - self.drawer.height() - 10)
else:
self.drawer.move(bar_pos.x(), bar_pos.y() + self.height() + 10)
self.drawer.show()
self.drawer.raise_()
def _on_search(self):
"""Handle search."""
text = self.search_box.text().strip()
if text:
self.search_requested.emit(text)
# Log
store = get_sqlite_store()
store.log_activity('ui', 'search', f"Query: {text}")
def _on_plugin_pinned(self, plugin_id: str):
"""Handle plugin pin."""
self._pin_plugin(plugin_id)
def _pin_plugin(self, plugin_id: str):
"""Pin a plugin to the activity bar."""
if not self.plugin_manager:
return
all_plugins = self.plugin_manager.get_all_discovered_plugins()
if plugin_id not in all_plugins:
return
plugin_class = all_plugins[plugin_id]
if plugin_id not in self.config.pinned_plugins:
self.config.pinned_plugins.append(plugin_id)
self._save_config()
icon_text = getattr(plugin_class, 'icon', '')
self.pinned_area.add_plugin(plugin_id, plugin_class.name, icon_text)
def _unpin_plugin(self, plugin_id: str):
"""Unpin a plugin."""
if plugin_id in self.config.pinned_plugins:
self.config.pinned_plugins.remove(plugin_id)
self._save_config()
self.pinned_area.remove_plugin(plugin_id)
def _load_pinned_plugins(self):
"""Load pinned plugins from config."""
if not self.plugin_manager:
return
all_plugins = self.plugin_manager.get_all_discovered_plugins()
plugins = []
for plugin_id in self.config.pinned_plugins:
if plugin_id in all_plugins:
plugin_class = all_plugins[plugin_id]
icon_text = getattr(plugin_class, 'icon', '')
plugins.append((plugin_id, plugin_class.name, icon_text))
self.pinned_area.set_plugins(plugins)
def _update_clock(self):
"""Update clock display."""
from datetime import datetime
self.clock_label.setText(datetime.now().strftime("%H:%M"))
def _apply_config(self):
"""Apply configuration."""
screen = QApplication.primaryScreen().geometry()
if self.config.position == "bottom":
self.move((screen.width() - 700) // 2, screen.height() - 70)
else:
self.move((screen.width() - 700) // 2, 20)
self.setFixedWidth(700)
def _load_config(self) -> ActivityBarConfig:
"""Load configuration."""
config_path = Path("config/activity_bar.json")
if config_path.exists():
try:
data = json.loads(config_path.read_text())
return ActivityBarConfig.from_dict(data)
except:
pass
return ActivityBarConfig()
def _save_config(self):
"""Save configuration."""
config_path = Path("config/activity_bar.json")
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(self.config.to_dict(), indent=2))
def enterEvent(self, event):
"""Mouse entered."""
self.hide_timer.stop()
super().enterEvent(event)
def leaveEvent(self, event):
"""Mouse left."""
if self.config.auto_hide:
self.hide_timer.start(self.config.auto_hide_delay)
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 getattr(self, '_dragging', False):
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
# Global instance
_activity_bar_instance = None
def get_activity_bar(plugin_manager=None) -> Optional[EnhancedActivityBar]:
"""Get or create global activity bar instance."""
global _activity_bar_instance
if _activity_bar_instance is None and plugin_manager:
_activity_bar_instance = EnhancedActivityBar(plugin_manager)
return _activity_bar_instance