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:
2026-01-03 17:37:43 -08:00
parent f61df830ca
commit a71c1f5ec4
7 changed files with 38 additions and 10 deletions

View File

@@ -31,6 +31,10 @@ from web_server import WebServer
class MacroButton(QPushButton): class MacroButton(QPushButton):
"""Custom button widget for displaying a macro.""" """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): def __init__(self, macro_id: str, macro: dict, parent=None):
super().__init__(parent) super().__init__(parent)
self.macro_id = macro_id self.macro_id = macro_id
@@ -103,9 +107,9 @@ class MacroButton(QPushButton):
action = menu.exec_(event.globalPos()) action = menu.exec_(event.globalPos())
if action == edit_action: 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: elif action == delete_action:
self.parent().parent().parent().parent().delete_macro(self.macro_id) self.delete_requested.emit(self.macro_id)
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
@@ -441,6 +445,8 @@ class MainWindow(QMainWindow):
for i, (macro_id, macro) in enumerate(filtered): for i, (macro_id, macro) in enumerate(filtered):
btn = MacroButton(macro_id, macro) btn = MacroButton(macro_id, macro)
btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid)) 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) layout.addWidget(btn, i // cols, i % cols)
def on_tab_changed(self, index): def on_tab_changed(self, index):

View File

@@ -275,13 +275,21 @@ class MacroManager:
cmd_type = cmd.get("type", "") cmd_type = cmd.get("type", "")
if cmd_type == "text": if cmd_type == "text":
# Type text string # Type text string using clipboard for Unicode support
value = cmd.get("value", "") value = cmd.get("value", "")
if value: if value:
if len(value) == 1: try:
pyautogui.press(value) import pyperclip
else: # Save current clipboard, paste text, restore (optional)
pyautogui.typewrite(value, interval=0.02) 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": elif cmd_type == "key":
# Press a single key # Press a single key

View File

@@ -43,6 +43,7 @@ a = Analysis(
'qrcode', 'qrcode',
'PIL', 'PIL',
'pyautogui', 'pyautogui',
'pyperclip',
'pystray', 'pystray',
'netifaces', 'netifaces',
'websockets', 'websockets',

View File

@@ -46,6 +46,7 @@ a = Analysis(
'qrcode', 'qrcode',
'PIL', 'PIL',
'pyautogui', 'pyautogui',
'pyperclip',
'pystray', 'pystray',
'pystray._base', 'pystray._base',
'netifaces', 'netifaces',

View File

@@ -43,6 +43,7 @@ a = Analysis(
'qrcode', 'qrcode',
'PIL', 'PIL',
'pyautogui', 'pyautogui',
'pyperclip',
'pystray', 'pystray',
'netifaces', 'netifaces',
'websockets', 'websockets',

View File

@@ -15,6 +15,7 @@ dependencies = [
"pillow>=10.0.0", "pillow>=10.0.0",
# Keyboard/mouse automation # Keyboard/mouse automation
"pyautogui>=0.9.54", "pyautogui>=0.9.54",
"pyperclip>=1.8.0",
# System tray # System tray
"pystray>=0.19.5", "pystray>=0.19.5",
# Web server # Web server

View File

@@ -1,6 +1,7 @@
# FastAPI web server for MacroPad # FastAPI web server for MacroPad
import os import os
import sys
import asyncio import asyncio
from typing import List, Optional from typing import List, Optional
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -14,6 +15,15 @@ import uvicorn
from config import DEFAULT_PORT, VERSION 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): class Command(BaseModel):
"""Single command in a macro sequence.""" """Single command in a macro sequence."""
type: str # text, key, hotkey, wait, app type: str # text, key, hotkey, wait, app
@@ -94,12 +104,12 @@ class WebServer:
lifespan=lifespan lifespan=lifespan
) )
# Serve static files from web directory # Serve static files from web directory (bundled with app)
web_dir = os.path.join(self.app_dir, "web") web_dir = get_resource_path("web")
if os.path.exists(web_dir): if os.path.exists(web_dir):
app.mount("/static", StaticFiles(directory=web_dir), name="static") 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") images_dir = os.path.join(self.app_dir, "macro_images")
if os.path.exists(images_dir): if os.path.exists(images_dir):
app.mount("/images", StaticFiles(directory=images_dir), name="images") app.mount("/images", StaticFiles(directory=images_dir), name="images")