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