Fix PyInstaller build issues and add right-click edit
## Fixes - Web interface now loads correctly in built app (use sys._MEIPASS for bundled web files) - Macro execution no longer locks up (use pyperclip clipboard for Unicode text support) - Right-click context menu works (use Qt signals instead of fragile parent traversal) ## Changes - web_server.py: Use get_resource_path() for web directory - macro_manager.py: Use clipboard paste for text commands instead of typewrite - gui/main_window.py: Add edit_requested/delete_requested signals to MacroButton - pyproject.toml: Add pyperclip dependency - All .spec files: Add pyperclip to hidden imports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,10 @@ from web_server import WebServer
|
||||
class MacroButton(QPushButton):
|
||||
"""Custom button widget for displaying a macro."""
|
||||
|
||||
# Signals for context menu actions
|
||||
edit_requested = Signal(str)
|
||||
delete_requested = Signal(str)
|
||||
|
||||
def __init__(self, macro_id: str, macro: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.macro_id = macro_id
|
||||
@@ -103,9 +107,9 @@ class MacroButton(QPushButton):
|
||||
|
||||
action = menu.exec_(event.globalPos())
|
||||
if action == edit_action:
|
||||
self.parent().parent().parent().parent().edit_macro(self.macro_id)
|
||||
self.edit_requested.emit(self.macro_id)
|
||||
elif action == delete_action:
|
||||
self.parent().parent().parent().parent().delete_macro(self.macro_id)
|
||||
self.delete_requested.emit(self.macro_id)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@@ -441,6 +445,8 @@ class MainWindow(QMainWindow):
|
||||
for i, (macro_id, macro) in enumerate(filtered):
|
||||
btn = MacroButton(macro_id, macro)
|
||||
btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid))
|
||||
btn.edit_requested.connect(self.edit_macro)
|
||||
btn.delete_requested.connect(self.delete_macro)
|
||||
layout.addWidget(btn, i // cols, i % cols)
|
||||
|
||||
def on_tab_changed(self, index):
|
||||
|
||||
@@ -275,13 +275,21 @@ class MacroManager:
|
||||
cmd_type = cmd.get("type", "")
|
||||
|
||||
if cmd_type == "text":
|
||||
# Type text string
|
||||
# Type text string using clipboard for Unicode support
|
||||
value = cmd.get("value", "")
|
||||
if value:
|
||||
if len(value) == 1:
|
||||
pyautogui.press(value)
|
||||
else:
|
||||
pyautogui.typewrite(value, interval=0.02)
|
||||
try:
|
||||
import pyperclip
|
||||
# Save current clipboard, paste text, restore (optional)
|
||||
pyperclip.copy(value)
|
||||
pyautogui.hotkey("ctrl", "v")
|
||||
time.sleep(0.05) # Small delay after paste
|
||||
except ImportError:
|
||||
# Fallback to typewrite for ASCII-only
|
||||
if len(value) == 1:
|
||||
pyautogui.press(value)
|
||||
else:
|
||||
pyautogui.typewrite(value, interval=0.02)
|
||||
|
||||
elif cmd_type == "key":
|
||||
# Press a single key
|
||||
|
||||
@@ -43,6 +43,7 @@ a = Analysis(
|
||||
'qrcode',
|
||||
'PIL',
|
||||
'pyautogui',
|
||||
'pyperclip',
|
||||
'pystray',
|
||||
'netifaces',
|
||||
'websockets',
|
||||
|
||||
@@ -46,6 +46,7 @@ a = Analysis(
|
||||
'qrcode',
|
||||
'PIL',
|
||||
'pyautogui',
|
||||
'pyperclip',
|
||||
'pystray',
|
||||
'pystray._base',
|
||||
'netifaces',
|
||||
|
||||
@@ -43,6 +43,7 @@ a = Analysis(
|
||||
'qrcode',
|
||||
'PIL',
|
||||
'pyautogui',
|
||||
'pyperclip',
|
||||
'pystray',
|
||||
'netifaces',
|
||||
'websockets',
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies = [
|
||||
"pillow>=10.0.0",
|
||||
# Keyboard/mouse automation
|
||||
"pyautogui>=0.9.54",
|
||||
"pyperclip>=1.8.0",
|
||||
# System tray
|
||||
"pystray>=0.19.5",
|
||||
# Web server
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# FastAPI web server for MacroPad
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -14,6 +15,15 @@ import uvicorn
|
||||
from config import DEFAULT_PORT, VERSION
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""Get the path to a bundled resource file."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
class Command(BaseModel):
|
||||
"""Single command in a macro sequence."""
|
||||
type: str # text, key, hotkey, wait, app
|
||||
@@ -94,12 +104,12 @@ class WebServer:
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Serve static files from web directory
|
||||
web_dir = os.path.join(self.app_dir, "web")
|
||||
# Serve static files from web directory (bundled with app)
|
||||
web_dir = get_resource_path("web")
|
||||
if os.path.exists(web_dir):
|
||||
app.mount("/static", StaticFiles(directory=web_dir), name="static")
|
||||
|
||||
# Serve macro images
|
||||
# Serve macro images (user data directory)
|
||||
images_dir = os.path.join(self.app_dir, "macro_images")
|
||||
if os.path.exists(images_dir):
|
||||
app.mount("/images", StaticFiles(directory=images_dir), name="images")
|
||||
|
||||
Reference in New Issue
Block a user