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):
|
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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ a = Analysis(
|
|||||||
'qrcode',
|
'qrcode',
|
||||||
'PIL',
|
'PIL',
|
||||||
'pyautogui',
|
'pyautogui',
|
||||||
|
'pyperclip',
|
||||||
'pystray',
|
'pystray',
|
||||||
'netifaces',
|
'netifaces',
|
||||||
'websockets',
|
'websockets',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ a = Analysis(
|
|||||||
'qrcode',
|
'qrcode',
|
||||||
'PIL',
|
'PIL',
|
||||||
'pyautogui',
|
'pyautogui',
|
||||||
|
'pyperclip',
|
||||||
'pystray',
|
'pystray',
|
||||||
'pystray._base',
|
'pystray._base',
|
||||||
'netifaces',
|
'netifaces',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ a = Analysis(
|
|||||||
'qrcode',
|
'qrcode',
|
||||||
'PIL',
|
'PIL',
|
||||||
'pyautogui',
|
'pyautogui',
|
||||||
|
'pyperclip',
|
||||||
'pystray',
|
'pystray',
|
||||||
'netifaces',
|
'netifaces',
|
||||||
'websockets',
|
'websockets',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user