refactor: Consolidate activity_bar - replace with enhanced version
This commit is contained in:
parent
af2a1c0b12
commit
b863a0f17b
|
|
@ -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
|
||||||
1203
core/activity_bar.py
1203
core/activity_bar.py
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
|
||||||
Loading…
Reference in New Issue