Add support for Tabs and other enhancements #1
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,6 @@
|
|||||||
macros.json
|
macros.json
|
||||||
macro_images/
|
macro_images/
|
||||||
macro_images/*
|
macro_images/*
|
||||||
build/
|
build/
|
||||||
|
.venv
|
||||||
|
__pycache__
|
30
README.md
30
README.md
@ -1,4 +1,4 @@
|
|||||||
# MP-Server
|
# MacroPad Server
|
||||||
|
|
||||||
A versatile MacroPad server application that lets you create, manage, and execute custom macros from both a local interface and remotely via a web interface.
|
A versatile MacroPad server application that lets you create, manage, and execute custom macros from both a local interface and remotely via a web interface.
|
||||||
|
|
||||||
@ -8,21 +8,27 @@ A versatile MacroPad server application that lets you create, manage, and execut
|
|||||||
- **Application Macros**: Launch applications or scripts directly
|
- **Application Macros**: Launch applications or scripts directly
|
||||||
- **Key Modifiers**: Add Ctrl, Alt, Shift modifiers and Enter keypress to your text macros
|
- **Key Modifiers**: Add Ctrl, Alt, Shift modifiers and Enter keypress to your text macros
|
||||||
- **Custom Images**: Assign images to macros for easy identification
|
- **Custom Images**: Assign images to macros for easy identification
|
||||||
|
- **Category Management**: Organize macros into custom tabs for better organization
|
||||||
- **Web Interface**: Access and trigger your macros from other devices on your network
|
- **Web Interface**: Access and trigger your macros from other devices on your network
|
||||||
- **System Tray Integration**: Runs silently in your system tray for easy access
|
- **System Tray Integration**: Minimize to tray when minimized, exit when closed
|
||||||
|
- **QR Code Generation**: Quickly connect mobile devices to the web interface
|
||||||
|
- **Sorting Options**: Sort macros by name, type, or recent usage
|
||||||
- **Persistent Storage**: Macros are automatically saved for future sessions
|
- **Persistent Storage**: Macros are automatically saved for future sessions
|
||||||
|
- **Dark Theme**: Modern dark interface for comfortable use
|
||||||
|
- **Modular Architecture**: Clean separation of concerns with dedicated modules
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.11+
|
- Python 3.11+
|
||||||
- Required Python packages:
|
- Required Python packages (install via requirements.txt):
|
||||||
- tkinter
|
- tkinter
|
||||||
- flask
|
- flask
|
||||||
- pyautogui
|
- pyautogui
|
||||||
- pystray
|
- pystray
|
||||||
- Pillow (PIL)
|
- Pillow (PIL)
|
||||||
- flask_cors
|
|
||||||
- waitress
|
- waitress
|
||||||
|
- netifaces
|
||||||
|
- qrcode
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -30,17 +36,13 @@ A versatile MacroPad server application that lets you create, manage, and execut
|
|||||||
2. Install the required dependencies:
|
2. Install the required dependencies:
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
3. Run the application:
|
|
||||||
```bash
|
|
||||||
python mp-server-v2.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternative Installation Method
|
## Alternative Installation Method
|
||||||
#### Windows only
|
#### Windows only
|
||||||
|
|
||||||
1. Create a Folder you wish to run MacroPad from
|
1. Create a Folder you wish to run MacroPad from
|
||||||
2. Download ```mp-serverv2.exe``` from the ```dist``` folder
|
2. Download ```macropad.exe``` from the ```dist``` folder
|
||||||
3. Accept the security notices, and run the application
|
3. Accept the security notices, and run the application
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@ -50,19 +52,21 @@ A versatile MacroPad server application that lets you create, manage, and execut
|
|||||||
|
|
||||||
### Main Interface
|
### Main Interface
|
||||||
|
|
||||||
When launched, MP-Server displays your existing macros with options to:
|
When launched, MacroPad displays your existing macros with options to:
|
||||||
|
|
||||||
- **Add New Macro**: Create text snippets or application shortcuts
|
- **Add New Macro**: Create text snippets or application shortcuts
|
||||||
- **Edit Macro**: Modify existing macros
|
- **Edit Macro**: Modify existing macros
|
||||||
- **Delete Macro**: Remove unwanted macros
|
- **Delete Macro**: Remove unwanted macros
|
||||||
- **Minimize to Tray**: Hide the application to your system tray
|
- **Sort Options**: Sort the Macros by type, name, and recent usage
|
||||||
- **Exit**: Close the application completely
|
- **Manage Tabs**: Assign categories to macros for better organization
|
||||||
|
- **Start Web Server**: Starts the web server to serve the MacroPad web interface.
|
||||||
|
|
||||||
### Creating a Macro
|
### Creating a Macro
|
||||||
|
|
||||||
1. Click the "Add Macro" button
|
1. Click the "Add Macro" button
|
||||||
2. Fill in the details:
|
2. Fill in the details:
|
||||||
- **Name**: A descriptive name for your macro
|
- **Name**: A descriptive name for your macro
|
||||||
|
- **Category**: Assign a category to associate with a tab
|
||||||
- **Type**: Choose between Text or Application
|
- **Type**: Choose between Text or Application
|
||||||
- **Command/Text**: The text to insert or application command to run
|
- **Command/Text**: The text to insert or application command to run
|
||||||
- **Modifiers**: Select any combination of Ctrl, Alt, Shift, and Enter
|
- **Modifiers**: Select any combination of Ctrl, Alt, Shift, and Enter
|
||||||
|
19
config.py
Normal file
19
config.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Configuration and constants for MacroPad Server
|
||||||
|
|
||||||
|
VERSION = "0.8.0 Beta"
|
||||||
|
DEFAULT_PORT = 40000
|
||||||
|
|
||||||
|
# UI Theme colors
|
||||||
|
THEME = {
|
||||||
|
'bg_color': "#2e2e2e",
|
||||||
|
'fg_color': "#ffffff",
|
||||||
|
'highlight_color': "#3e3e3e",
|
||||||
|
'accent_color': "#007acc",
|
||||||
|
'button_bg': "#505050",
|
||||||
|
'button_fg': "#ffffff",
|
||||||
|
'tab_bg': "#404040",
|
||||||
|
'tab_selected': "#007acc"
|
||||||
|
}
|
||||||
|
|
||||||
|
# File extensions for images
|
||||||
|
IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp"]
|
BIN
dist/mp-server-v2.exe → dist/macropad.exe
vendored
BIN
dist/mp-server-v2.exe → dist/macropad.exe
vendored
Binary file not shown.
BIN
dist/mp-server.exe
vendored
BIN
dist/mp-server.exe
vendored
Binary file not shown.
244
macro_manager.py
Normal file
244
macro_manager.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
# Macro management and execution
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import pyautogui
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
class MacroManager:
|
||||||
|
def __init__(self, data_file, images_dir, app_dir):
|
||||||
|
self.data_file = data_file
|
||||||
|
self.images_dir = images_dir
|
||||||
|
self.app_dir = app_dir
|
||||||
|
self.macros = {}
|
||||||
|
self.load_macros()
|
||||||
|
|
||||||
|
def load_macros(self):
|
||||||
|
"""Load macros from JSON file"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.data_file):
|
||||||
|
with open(self.data_file, "r") as file:
|
||||||
|
self.macros = json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading macros: {e}")
|
||||||
|
self.macros = {}
|
||||||
|
|
||||||
|
def save_macros(self):
|
||||||
|
"""Save macros to JSON file"""
|
||||||
|
try:
|
||||||
|
with open(self.data_file, "w") as file:
|
||||||
|
json.dump(self.macros, file, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving macros: {e}")
|
||||||
|
|
||||||
|
def get_sorted_macros(self, sort_by="name"):
|
||||||
|
"""Get macros sorted by specified criteria"""
|
||||||
|
macro_list = list(self.macros.items())
|
||||||
|
|
||||||
|
if sort_by == "name":
|
||||||
|
macro_list.sort(key=lambda x: x[1]["name"].lower())
|
||||||
|
elif sort_by == "type":
|
||||||
|
macro_list.sort(key=lambda x: (x[1].get("type", ""), x[1]["name"].lower()))
|
||||||
|
elif sort_by == "recent":
|
||||||
|
# Sort by last_used timestamp if available, otherwise by name
|
||||||
|
macro_list.sort(key=lambda x: (x[1].get("last_used", 0), x[1]["name"].lower()), reverse=True)
|
||||||
|
|
||||||
|
return macro_list
|
||||||
|
|
||||||
|
def filter_macros_by_tab(self, macro_list, tab_name):
|
||||||
|
"""Filter macros based on tab name"""
|
||||||
|
if tab_name == "All":
|
||||||
|
return macro_list
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
for macro_id, macro in macro_list:
|
||||||
|
# Check type match
|
||||||
|
if macro.get("type", "").title() == tab_name:
|
||||||
|
filtered.append((macro_id, macro))
|
||||||
|
# Check custom category match
|
||||||
|
elif macro.get("category") == tab_name:
|
||||||
|
filtered.append((macro_id, macro))
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def get_unique_tabs(self):
|
||||||
|
"""Get list of unique tabs based on macro types and categories"""
|
||||||
|
tabs = ["All"]
|
||||||
|
unique_types = set()
|
||||||
|
|
||||||
|
for macro in self.macros.values():
|
||||||
|
if macro.get("type"):
|
||||||
|
unique_types.add(macro["type"].title())
|
||||||
|
if macro.get("category"):
|
||||||
|
unique_types.add(macro["category"])
|
||||||
|
|
||||||
|
for tab_type in sorted(unique_types):
|
||||||
|
if tab_type not in ["All"]:
|
||||||
|
tabs.append(tab_type)
|
||||||
|
|
||||||
|
return tabs
|
||||||
|
|
||||||
|
def add_macro(self, name, macro_type, command, category="", modifiers=None, image_path=""):
|
||||||
|
"""Add a new macro"""
|
||||||
|
if modifiers is None:
|
||||||
|
modifiers = {"ctrl": False, "alt": False, "shift": False, "enter": False}
|
||||||
|
|
||||||
|
macro_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Process image if provided
|
||||||
|
image_path_reference = ""
|
||||||
|
if image_path:
|
||||||
|
try:
|
||||||
|
# Generate unique filename for the image
|
||||||
|
file_ext = os.path.splitext(image_path)[1].lower()
|
||||||
|
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
|
||||||
|
dest_path = os.path.join(self.images_dir, unique_filename)
|
||||||
|
|
||||||
|
# Resize image to max 256x256
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
img.thumbnail((256, 256))
|
||||||
|
img.save(dest_path)
|
||||||
|
|
||||||
|
# Store the relative path to the image
|
||||||
|
image_path_reference = os.path.join("macro_images", unique_filename)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing image: {e}")
|
||||||
|
|
||||||
|
# Create macro data
|
||||||
|
macro_data = {
|
||||||
|
"name": name,
|
||||||
|
"type": macro_type,
|
||||||
|
"command": command,
|
||||||
|
"image_path": image_path_reference,
|
||||||
|
"modifiers": modifiers,
|
||||||
|
"last_used": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if category:
|
||||||
|
macro_data["category"] = category
|
||||||
|
|
||||||
|
self.macros[macro_id] = macro_data
|
||||||
|
self.save_macros()
|
||||||
|
return macro_id
|
||||||
|
|
||||||
|
def update_macro(self, macro_id, name, macro_type, command, category="", modifiers=None, image_path=""):
|
||||||
|
"""Update an existing macro"""
|
||||||
|
if macro_id not in self.macros:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if modifiers is None:
|
||||||
|
modifiers = {"ctrl": False, "alt": False, "shift": False, "enter": False}
|
||||||
|
|
||||||
|
macro = self.macros[macro_id]
|
||||||
|
|
||||||
|
# Keep the old image or update with new one
|
||||||
|
image_path_reference = macro.get("image_path", "")
|
||||||
|
if image_path:
|
||||||
|
try:
|
||||||
|
# Generate unique filename for the image
|
||||||
|
file_ext = os.path.splitext(image_path)[1].lower()
|
||||||
|
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
|
||||||
|
dest_path = os.path.join(self.images_dir, unique_filename)
|
||||||
|
|
||||||
|
# Resize image to max 256x256
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
img.thumbnail((256, 256))
|
||||||
|
img.save(dest_path)
|
||||||
|
|
||||||
|
# Store the relative path to the image
|
||||||
|
image_path_reference = os.path.join("macro_images", unique_filename)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing image: {e}")
|
||||||
|
|
||||||
|
# Update macro data
|
||||||
|
updated_macro = {
|
||||||
|
"name": name,
|
||||||
|
"type": macro_type,
|
||||||
|
"command": command,
|
||||||
|
"image_path": image_path_reference,
|
||||||
|
"modifiers": modifiers,
|
||||||
|
"last_used": macro.get("last_used", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category:
|
||||||
|
updated_macro["category"] = category
|
||||||
|
|
||||||
|
self.macros[macro_id] = updated_macro
|
||||||
|
self.save_macros()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_macro(self, macro_id):
|
||||||
|
"""Delete a macro"""
|
||||||
|
if macro_id not in self.macros:
|
||||||
|
return False
|
||||||
|
|
||||||
|
macro = self.macros[macro_id]
|
||||||
|
|
||||||
|
# Delete associated image file if it exists
|
||||||
|
if "image_path" in macro and macro["image_path"]:
|
||||||
|
try:
|
||||||
|
img_path = os.path.join(self.app_dir, macro["image_path"])
|
||||||
|
if os.path.exists(img_path):
|
||||||
|
os.remove(img_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing image file: {e}")
|
||||||
|
|
||||||
|
del self.macros[macro_id]
|
||||||
|
self.save_macros()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute_macro(self, macro_id):
|
||||||
|
"""Execute a macro by ID"""
|
||||||
|
if macro_id not in self.macros:
|
||||||
|
return False
|
||||||
|
|
||||||
|
macro = self.macros[macro_id]
|
||||||
|
|
||||||
|
# Update last_used timestamp for recent sorting
|
||||||
|
self.macros[macro_id]["last_used"] = time.time()
|
||||||
|
self.save_macros()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if macro["type"] == "text":
|
||||||
|
# Handle key modifiers
|
||||||
|
modifiers = macro.get("modifiers", {})
|
||||||
|
|
||||||
|
# Add modifier keys if enabled
|
||||||
|
if modifiers.get("ctrl", False):
|
||||||
|
pyautogui.keyDown('ctrl')
|
||||||
|
if modifiers.get("alt", False):
|
||||||
|
pyautogui.keyDown('alt')
|
||||||
|
if modifiers.get("shift", False):
|
||||||
|
pyautogui.keyDown('shift')
|
||||||
|
|
||||||
|
# Handle single character vs multi-character commands
|
||||||
|
if str(macro["command"]) and len(str(macro["command"])) == 1:
|
||||||
|
pyautogui.keyDown(macro["command"])
|
||||||
|
time.sleep(0.5)
|
||||||
|
pyautogui.keyUp(macro["command"])
|
||||||
|
else:
|
||||||
|
pyautogui.typewrite(macro["command"], interval=0.02)
|
||||||
|
|
||||||
|
# Release modifier keys in reverse order
|
||||||
|
if modifiers.get("shift", False):
|
||||||
|
pyautogui.keyUp('shift')
|
||||||
|
if modifiers.get("alt", False):
|
||||||
|
pyautogui.keyUp('alt')
|
||||||
|
if modifiers.get("ctrl", False):
|
||||||
|
pyautogui.keyUp('ctrl')
|
||||||
|
|
||||||
|
# Add Enter/Return if requested
|
||||||
|
if modifiers.get("enter", False):
|
||||||
|
pyautogui.press('enter')
|
||||||
|
|
||||||
|
elif macro["type"] == "app":
|
||||||
|
subprocess.Popen(macro["command"], shell=True)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error executing macro: {e}")
|
||||||
|
return False
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['mp-server.py'],
|
['main.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=[],
|
||||||
@ -22,7 +22,7 @@ exe = EXE(
|
|||||||
a.binaries,
|
a.binaries,
|
||||||
a.datas,
|
a.datas,
|
||||||
[],
|
[],
|
||||||
name='mp-server',
|
name='macropad',
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
502
main.py
Normal file
502
main.py
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
# Main application file for MacroPad Server
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
import qrcode
|
||||||
|
import webbrowser
|
||||||
|
import pystray
|
||||||
|
from PIL import Image, ImageTk, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
from config import VERSION, DEFAULT_PORT, THEME
|
||||||
|
from macro_manager import MacroManager
|
||||||
|
from web_server import WebServer
|
||||||
|
from ui_components import MacroDialog, MacroSelector, TabManager
|
||||||
|
|
||||||
|
|
||||||
|
class MacroPadServer:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("MacroPad Server")
|
||||||
|
self.root.geometry("800x600")
|
||||||
|
self.configure_styles()
|
||||||
|
|
||||||
|
# Set up directories
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
data_file = os.path.join(base_dir, "macros.json")
|
||||||
|
images_dir = os.path.join(base_dir, "macro_images")
|
||||||
|
os.makedirs(images_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Initialize components
|
||||||
|
self.macro_manager = MacroManager(data_file, images_dir, base_dir)
|
||||||
|
self.web_server = WebServer(self.macro_manager, base_dir, DEFAULT_PORT)
|
||||||
|
|
||||||
|
# UI state
|
||||||
|
self.current_sort = "name"
|
||||||
|
self.current_tab = "All"
|
||||||
|
self.image_cache = {}
|
||||||
|
|
||||||
|
# Server state
|
||||||
|
self.server_running = False
|
||||||
|
self.flask_thread = None
|
||||||
|
|
||||||
|
# Tray state
|
||||||
|
self.tray_icon = None
|
||||||
|
self.is_closing = False
|
||||||
|
|
||||||
|
# Create UI
|
||||||
|
self.create_ui()
|
||||||
|
|
||||||
|
# Set up window event handlers
|
||||||
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
|
self.root.bind('<Unmap>', self.on_minimize)
|
||||||
|
|
||||||
|
# Initialize tray icon
|
||||||
|
self.create_tray_icon()
|
||||||
|
|
||||||
|
def configure_styles(self):
|
||||||
|
"""Configure the dark theme styles"""
|
||||||
|
self.root.configure(bg=THEME['bg_color'])
|
||||||
|
|
||||||
|
style = ttk.Style()
|
||||||
|
style.theme_use("clam")
|
||||||
|
style.configure("TButton", background=THEME['button_bg'], foreground=THEME['button_fg'])
|
||||||
|
style.map("TButton", background=[("active", THEME['accent_color'])])
|
||||||
|
style.configure("TFrame", background=THEME['bg_color'])
|
||||||
|
style.configure("TLabel", background=THEME['bg_color'], foreground=THEME['fg_color'])
|
||||||
|
|
||||||
|
# Configure notebook (tabs) style
|
||||||
|
style.configure("TNotebook", background=THEME['bg_color'], borderwidth=0)
|
||||||
|
style.configure("TNotebook.Tab", background=THEME['tab_bg'], foreground=THEME['fg_color'],
|
||||||
|
padding=[12, 8], borderwidth=0)
|
||||||
|
style.map("TNotebook.Tab",
|
||||||
|
background=[("selected", THEME['tab_selected'])],
|
||||||
|
foreground=[("selected", THEME['fg_color'])],
|
||||||
|
padding=[("selected", [12, 8])]) # Keep same padding when selected
|
||||||
|
|
||||||
|
def create_ui(self):
|
||||||
|
"""Create the main user interface"""
|
||||||
|
# Create main container
|
||||||
|
main_frame = ttk.Frame(self.root)
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
# Left side: Macro list and controls
|
||||||
|
left_frame = ttk.Frame(main_frame)
|
||||||
|
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Sort controls
|
||||||
|
self._create_sort_controls(left_frame)
|
||||||
|
|
||||||
|
# Create notebook for tabs
|
||||||
|
self.notebook = ttk.Notebook(left_frame)
|
||||||
|
self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
self.notebook.bind("<<NotebookTabChanged>>", self.on_tab_change)
|
||||||
|
|
||||||
|
# Button controls
|
||||||
|
self._create_macro_buttons(left_frame)
|
||||||
|
|
||||||
|
# Right side: Server controls
|
||||||
|
right_frame = ttk.Frame(main_frame)
|
||||||
|
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
|
||||||
|
|
||||||
|
self._create_server_controls(right_frame)
|
||||||
|
|
||||||
|
# Version label
|
||||||
|
version_label = tk.Label(self.root, text=VERSION,
|
||||||
|
bg=THEME['bg_color'], fg=THEME['fg_color'],
|
||||||
|
font=('Helvetica', 8))
|
||||||
|
version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
|
||||||
|
|
||||||
|
# Initialize display
|
||||||
|
self.setup_tabs()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def _create_sort_controls(self, parent):
|
||||||
|
"""Create sorting controls"""
|
||||||
|
sort_frame = ttk.Frame(parent)
|
||||||
|
sort_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
ttk.Label(sort_frame, text="Sort by:").pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
self.sort_var = tk.StringVar(value=self.current_sort)
|
||||||
|
sort_combo = ttk.Combobox(sort_frame, textvariable=self.sort_var,
|
||||||
|
values=["name", "type", "recent"],
|
||||||
|
state="readonly", width=10)
|
||||||
|
sort_combo.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
sort_combo.bind("<<ComboboxSelected>>", self.on_sort_change)
|
||||||
|
|
||||||
|
# Tab management button
|
||||||
|
ttk.Button(sort_frame, text="Manage Tabs",
|
||||||
|
command=self.manage_tabs).pack(side=tk.RIGHT, padx=(5, 0))
|
||||||
|
|
||||||
|
def _create_macro_buttons(self, parent):
|
||||||
|
"""Create macro management buttons"""
|
||||||
|
button_frame = ttk.Frame(parent)
|
||||||
|
button_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
ttk.Button(button_frame, text="Add Macro", command=self.add_macro).pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro).pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
def _create_server_controls(self, parent):
|
||||||
|
"""Create web server controls"""
|
||||||
|
server_frame = ttk.Frame(parent)
|
||||||
|
server_frame.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
ttk.Label(server_frame, text="Web Server Control:").grid(row=0, column=0, sticky="w", pady=5)
|
||||||
|
|
||||||
|
self.server_button = ttk.Button(server_frame, text="Start Web Server", command=self.toggle_server)
|
||||||
|
self.server_button.grid(row=0, column=1, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Status display
|
||||||
|
self.status_var = tk.StringVar(value="Web server not running")
|
||||||
|
ttk.Label(server_frame, textvariable=self.status_var).grid(row=1, column=0, columnspan=2, sticky="w", pady=5)
|
||||||
|
|
||||||
|
# QR code display
|
||||||
|
self.qr_label = ttk.Label(parent)
|
||||||
|
self.qr_label.pack(pady=10)
|
||||||
|
|
||||||
|
# URL display
|
||||||
|
self.url_var = tk.StringVar(value="")
|
||||||
|
self.url_label = ttk.Label(parent, textvariable=self.url_var)
|
||||||
|
self.url_label.pack(pady=5)
|
||||||
|
|
||||||
|
# Browser button
|
||||||
|
self.browser_button = ttk.Button(parent, text="Open in Browser",
|
||||||
|
command=self.open_in_browser, state=tk.DISABLED)
|
||||||
|
self.browser_button.pack(pady=5)
|
||||||
|
|
||||||
|
def setup_tabs(self):
|
||||||
|
"""Initialize tabs based on macro categories"""
|
||||||
|
# Clear existing tabs
|
||||||
|
for tab in self.notebook.tabs():
|
||||||
|
self.notebook.forget(tab)
|
||||||
|
|
||||||
|
# Get unique tabs from macro manager
|
||||||
|
tabs = self.macro_manager.get_unique_tabs()
|
||||||
|
|
||||||
|
for tab_name in tabs:
|
||||||
|
frame = ttk.Frame(self.notebook)
|
||||||
|
self.notebook.add(frame, text=tab_name)
|
||||||
|
|
||||||
|
def get_current_tab_name(self):
|
||||||
|
"""Get the name of the currently selected tab"""
|
||||||
|
try:
|
||||||
|
current_tab_id = self.notebook.select()
|
||||||
|
return self.notebook.tab(current_tab_id, "text")
|
||||||
|
except:
|
||||||
|
return "All"
|
||||||
|
|
||||||
|
def on_tab_change(self, event=None):
|
||||||
|
"""Handle tab change event"""
|
||||||
|
self.current_tab = self.get_current_tab_name()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def on_sort_change(self, event=None):
|
||||||
|
"""Handle sort option change"""
|
||||||
|
self.current_sort = self.sort_var.get()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def display_macros(self):
|
||||||
|
"""Display macros in the current tab"""
|
||||||
|
# Get current tab frame
|
||||||
|
try:
|
||||||
|
current_tab_id = self.notebook.select()
|
||||||
|
current_frame = self.notebook.nametowidget(current_tab_id)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear previous content
|
||||||
|
for widget in current_frame.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
# Create scrollable canvas
|
||||||
|
canvas = tk.Canvas(current_frame, bg=THEME['bg_color'], highlightthickness=0)
|
||||||
|
scrollbar = ttk.Scrollbar(current_frame, orient="vertical", command=canvas.yview)
|
||||||
|
scrollable_frame = ttk.Frame(canvas)
|
||||||
|
|
||||||
|
scrollable_frame.bind(
|
||||||
|
"<Configure>",
|
||||||
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
||||||
|
)
|
||||||
|
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||||
|
canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
canvas.pack(side="left", fill="both", expand=True)
|
||||||
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
# Get sorted and filtered macros
|
||||||
|
sorted_macros = self.macro_manager.get_sorted_macros(self.current_sort)
|
||||||
|
filtered_macros = self.macro_manager.filter_macros_by_tab(sorted_macros, self.current_tab)
|
||||||
|
|
||||||
|
# Display macros
|
||||||
|
for macro_id, macro in filtered_macros:
|
||||||
|
self._create_macro_button(scrollable_frame, macro_id, macro)
|
||||||
|
|
||||||
|
# Display message if no macros
|
||||||
|
if not filtered_macros:
|
||||||
|
label = tk.Label(scrollable_frame, text="No macros in this category",
|
||||||
|
bg=THEME['bg_color'], fg=THEME['fg_color'])
|
||||||
|
label.pack(pady=20)
|
||||||
|
|
||||||
|
def _create_macro_button(self, parent, macro_id, macro):
|
||||||
|
"""Create a button for a single macro"""
|
||||||
|
frame = ttk.Frame(parent)
|
||||||
|
frame.pack(fill="x", pady=5, padx=5)
|
||||||
|
|
||||||
|
button = tk.Button(
|
||||||
|
frame, text=macro["name"],
|
||||||
|
bg=THEME['button_bg'], fg=THEME['button_fg'],
|
||||||
|
activebackground=THEME['accent_color'], activeforeground=THEME['button_fg'],
|
||||||
|
relief=tk.RAISED, bd=2, pady=8,
|
||||||
|
command=lambda: self.macro_manager.execute_macro(macro_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add image if available
|
||||||
|
if "image_path" in macro and macro["image_path"]:
|
||||||
|
try:
|
||||||
|
if macro["image_path"] in self.image_cache:
|
||||||
|
button_image = self.image_cache[macro["image_path"]]
|
||||||
|
else:
|
||||||
|
img_path = os.path.join(self.macro_manager.app_dir, macro["image_path"])
|
||||||
|
img = Image.open(img_path)
|
||||||
|
img = img.resize((32, 32))
|
||||||
|
button_image = ImageTk.PhotoImage(img)
|
||||||
|
self.image_cache[macro["image_path"]] = button_image
|
||||||
|
|
||||||
|
button.config(image=button_image, compound=tk.LEFT)
|
||||||
|
button.image = button_image # Keep reference
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading image for {macro['name']}: {e}")
|
||||||
|
|
||||||
|
button.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
def manage_tabs(self):
|
||||||
|
"""Open tab management dialog"""
|
||||||
|
tab_manager = TabManager(self.root, self.macro_manager)
|
||||||
|
tab_manager.show()
|
||||||
|
self.setup_tabs()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def add_macro(self):
|
||||||
|
"""Add a new macro"""
|
||||||
|
dialog = MacroDialog(self.root, self.macro_manager)
|
||||||
|
result = dialog.show()
|
||||||
|
if result:
|
||||||
|
self.setup_tabs()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def edit_macro(self):
|
||||||
|
"""Edit an existing macro"""
|
||||||
|
selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Edit")
|
||||||
|
macro_id = selector.show()
|
||||||
|
if macro_id:
|
||||||
|
dialog = MacroDialog(self.root, self.macro_manager, macro_id)
|
||||||
|
result = dialog.show()
|
||||||
|
if result:
|
||||||
|
self.setup_tabs()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def delete_macro(self):
|
||||||
|
"""Delete a macro"""
|
||||||
|
selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Delete")
|
||||||
|
macro_id = selector.show()
|
||||||
|
if macro_id:
|
||||||
|
macro_name = self.macro_manager.macros[macro_id]["name"]
|
||||||
|
if messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{macro_name}'?"):
|
||||||
|
self.macro_manager.delete_macro(macro_id)
|
||||||
|
self.setup_tabs()
|
||||||
|
self.display_macros()
|
||||||
|
|
||||||
|
def toggle_server(self):
|
||||||
|
"""Toggle web server on/off"""
|
||||||
|
if self.server_running:
|
||||||
|
self.stop_server()
|
||||||
|
else:
|
||||||
|
self.start_server()
|
||||||
|
|
||||||
|
def start_server(self):
|
||||||
|
"""Start the web server"""
|
||||||
|
try:
|
||||||
|
if not self.server_running:
|
||||||
|
self.server_running = True
|
||||||
|
self.flask_thread = threading.Thread(target=self.run_web_server)
|
||||||
|
self.flask_thread.daemon = True
|
||||||
|
self.flask_thread.start()
|
||||||
|
self.server_button.config(text="Stop Web Server")
|
||||||
|
|
||||||
|
# Get IP address and display info
|
||||||
|
ip_address = self.get_ip_address()
|
||||||
|
if ip_address:
|
||||||
|
url = f"http://{ip_address}:{DEFAULT_PORT}"
|
||||||
|
url_text = f"Web UI available at:\n{url}"
|
||||||
|
self.url_var.set(url_text)
|
||||||
|
self.browser_button.config(state=tk.NORMAL)
|
||||||
|
self.generate_qr_code(url)
|
||||||
|
else:
|
||||||
|
self.url_var.set("No network interfaces found")
|
||||||
|
except Exception as e:
|
||||||
|
self.status_var.set(f"Error starting server: {e}")
|
||||||
|
self.server_running = False
|
||||||
|
|
||||||
|
def stop_server(self):
|
||||||
|
"""Stop the web server"""
|
||||||
|
if self.server_running:
|
||||||
|
self.server_running = False
|
||||||
|
self.status_var.set("Web server stopped")
|
||||||
|
self.server_button.config(text="Start Web Server")
|
||||||
|
self.url_var.set("")
|
||||||
|
self.browser_button.config(state=tk.DISABLED)
|
||||||
|
self.qr_label.config(image="")
|
||||||
|
|
||||||
|
def run_web_server(self):
|
||||||
|
"""Run the web server in a separate thread"""
|
||||||
|
self.status_var.set(f"Web server running on port {DEFAULT_PORT}")
|
||||||
|
try:
|
||||||
|
self.web_server.run()
|
||||||
|
except Exception as e:
|
||||||
|
self.status_var.set(f"Web server error: {e}")
|
||||||
|
self.server_running = False
|
||||||
|
|
||||||
|
def get_ip_address(self):
|
||||||
|
"""Get the primary internal IPv4 address"""
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting IP address: {e}")
|
||||||
|
try:
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
ip = socket.gethostbyname(hostname)
|
||||||
|
if not ip.startswith("127."):
|
||||||
|
return ip
|
||||||
|
|
||||||
|
for addr_info in socket.getaddrinfo(hostname, None):
|
||||||
|
potential_ip = addr_info[4][0]
|
||||||
|
if '.' in potential_ip and not potential_ip.startswith("127."):
|
||||||
|
return potential_ip
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
def generate_qr_code(self, url):
|
||||||
|
"""Generate and display QR code for the URL"""
|
||||||
|
try:
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=10,
|
||||||
|
border=4,
|
||||||
|
)
|
||||||
|
qr.add_data(url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
qr_img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
qr_photoimg = ImageTk.PhotoImage(qr_img)
|
||||||
|
|
||||||
|
self.qr_label.config(image=qr_photoimg)
|
||||||
|
self.qr_label.image = qr_photoimg
|
||||||
|
except ImportError:
|
||||||
|
self.qr_label.config(text="QR code generation requires 'qrcode' package")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating QR code: {e}")
|
||||||
|
self.qr_label.config(text="Error generating QR code")
|
||||||
|
|
||||||
|
def open_in_browser(self):
|
||||||
|
"""Open the web interface in browser"""
|
||||||
|
if self.server_running:
|
||||||
|
webbrowser.open(f"http://localhost:{DEFAULT_PORT}")
|
||||||
|
|
||||||
|
def on_minimize(self, event):
|
||||||
|
"""Handle window minimize event"""
|
||||||
|
# Only minimize to tray if the window is being iconified, not just unmapped
|
||||||
|
if event.widget == self.root and self.root.state() == 'iconic':
|
||||||
|
self.root.withdraw() # Hide window
|
||||||
|
|
||||||
|
def create_tray_icon(self):
|
||||||
|
"""Create system tray icon"""
|
||||||
|
try:
|
||||||
|
# Create a simple icon image with M letter
|
||||||
|
icon_image = Image.new("RGB", (64, 64), THEME['accent_color'])
|
||||||
|
draw = ImageDraw.Draw(icon_image)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to use a system font
|
||||||
|
font = ImageFont.truetype("arial.ttf", 40)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
# Try other common fonts
|
||||||
|
font = ImageFont.truetype("calibri.ttf", 40)
|
||||||
|
except:
|
||||||
|
# Fall back to default font
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Draw "MP" in the center
|
||||||
|
text = "MP"
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
text_height = bbox[3] - bbox[1]
|
||||||
|
x = (64 - text_width) // 2
|
||||||
|
y = (64 - text_height) // 2
|
||||||
|
draw.text((x, y), text, fill="white", font=font)
|
||||||
|
|
||||||
|
menu = (
|
||||||
|
pystray.MenuItem('Show', self.show_window),
|
||||||
|
pystray.MenuItem('Exit', self.exit_app)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu)
|
||||||
|
|
||||||
|
# Run tray icon in a separate thread
|
||||||
|
tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
|
||||||
|
tray_thread.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating tray icon: {e}")
|
||||||
|
# Tray icon is optional, continue without it
|
||||||
|
|
||||||
|
def show_window(self, icon=None, item=None):
|
||||||
|
"""Show window from tray"""
|
||||||
|
self.root.deiconify()
|
||||||
|
self.root.state('normal')
|
||||||
|
self.root.lift()
|
||||||
|
self.root.focus_force()
|
||||||
|
|
||||||
|
def exit_app(self, icon=None, item=None):
|
||||||
|
"""Exit the application"""
|
||||||
|
self.is_closing = True
|
||||||
|
self.stop_server()
|
||||||
|
|
||||||
|
if self.tray_icon:
|
||||||
|
try:
|
||||||
|
self.tray_icon.stop()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.root.quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Force exit if needed
|
||||||
|
import os
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def on_closing(self):
|
||||||
|
"""Handle window close event - exit the application"""
|
||||||
|
self.exit_app()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
root = tk.Tk()
|
||||||
|
app = MacroPadServer(root)
|
||||||
|
root.mainloop()
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['mp-server-v2.py'],
|
['main.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=[],
|
||||||
@ -22,7 +22,7 @@ exe = EXE(
|
|||||||
a.binaries,
|
a.binaries,
|
||||||
a.datas,
|
a.datas,
|
||||||
[],
|
[],
|
||||||
name='mp-server-v2',
|
name='main',
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
971
mp-server-v2.py
971
mp-server-v2.py
@ -1,971 +0,0 @@
|
|||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, filedialog, messagebox
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import pyautogui
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
import pystray
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
from flask import Flask, render_template_string, request, jsonify, send_file
|
|
||||||
import webbrowser
|
|
||||||
from waitress import serve
|
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
import qrcode
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
class MacroPadServer:
|
|
||||||
def __init__(self, root):
|
|
||||||
self.root = root
|
|
||||||
self.root.title("MacroPad Server")
|
|
||||||
self.root.geometry("800x600")
|
|
||||||
self.configure_styles()
|
|
||||||
|
|
||||||
# Set Version Str
|
|
||||||
self.version_str = "0.5.4 Beta"
|
|
||||||
|
|
||||||
# Set up directories
|
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
base_dir = os.path.dirname(sys.executable)
|
|
||||||
else:
|
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
self.data_file = os.path.join(base_dir, "macros.json")
|
|
||||||
self.images_dir = os.path.join(base_dir, "macro_images")
|
|
||||||
self.app_dir = base_dir
|
|
||||||
os.makedirs(self.images_dir, exist_ok=True)
|
|
||||||
|
|
||||||
self.macros = {}
|
|
||||||
self.load_macros()
|
|
||||||
self.image_cache = {} # Cache for images
|
|
||||||
|
|
||||||
# Set up server
|
|
||||||
self.server_running = False
|
|
||||||
self.web_app = None
|
|
||||||
self.flask_thread = None
|
|
||||||
self.server_port = 40000
|
|
||||||
|
|
||||||
# Create UI
|
|
||||||
self.create_ui()
|
|
||||||
|
|
||||||
# Set up window close handler
|
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
||||||
|
|
||||||
def configure_styles(self):
|
|
||||||
# Dark theme
|
|
||||||
self.bg_color = "#2e2e2e"
|
|
||||||
self.fg_color = "#ffffff"
|
|
||||||
self.highlight_color = "#3e3e3e"
|
|
||||||
self.accent_color = "#007acc"
|
|
||||||
self.button_bg = "#505050"
|
|
||||||
self.button_fg = "#ffffff"
|
|
||||||
|
|
||||||
self.root.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Configure ttk styles
|
|
||||||
style = ttk.Style()
|
|
||||||
style.theme_use("clam")
|
|
||||||
style.configure("TButton", background=self.button_bg, foreground=self.button_fg)
|
|
||||||
style.map("TButton", background=[("active", self.accent_color)])
|
|
||||||
style.configure("TFrame", background=self.bg_color)
|
|
||||||
style.configure("TLabel", background=self.bg_color, foreground=self.fg_color)
|
|
||||||
|
|
||||||
def create_ui(self):
|
|
||||||
# Create main container
|
|
||||||
main_frame = ttk.Frame(self.root)
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# Left side: Macro list and buttons
|
|
||||||
left_frame = ttk.Frame(main_frame)
|
|
||||||
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Macro list
|
|
||||||
self.listbox_frame = ttk.Frame(left_frame)
|
|
||||||
self.listbox_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
|
||||||
|
|
||||||
# Right side: Details and server controls
|
|
||||||
right_frame = ttk.Frame(main_frame)
|
|
||||||
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
|
|
||||||
|
|
||||||
#Set version details
|
|
||||||
version_label = tk.Label(self.root, text=self.version_str,
|
|
||||||
bg=self.bg_color, fg=self.fg_color,
|
|
||||||
font=('Helvetica', 8)) # Using smaller font for version text
|
|
||||||
version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
|
|
||||||
|
|
||||||
# Button container
|
|
||||||
button_frame = ttk.Frame(left_frame)
|
|
||||||
button_frame.pack(fill=tk.X, pady=(0, 10))
|
|
||||||
|
|
||||||
button_style = {'style': 'TButton'}
|
|
||||||
|
|
||||||
# Add buttons
|
|
||||||
ttk.Button(button_frame, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=2)
|
|
||||||
ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=2)
|
|
||||||
ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
# Server controls
|
|
||||||
server_frame = ttk.Frame(right_frame)
|
|
||||||
server_frame.pack(fill=tk.X, pady=10)
|
|
||||||
|
|
||||||
ttk.Label(server_frame, text="Web Server Control:").grid(row=0, column=0, sticky="w", pady=5)
|
|
||||||
|
|
||||||
self.server_button = ttk.Button(server_frame, text="Start Web Server", command=self.toggle_server)
|
|
||||||
self.server_button.grid(row=0, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Status display
|
|
||||||
self.status_var = tk.StringVar(value="Web server not running")
|
|
||||||
ttk.Label(server_frame, textvariable=self.status_var).grid(row=1, column=0, columnspan=2, sticky="w", pady=5)
|
|
||||||
|
|
||||||
# QR code placeholder
|
|
||||||
self.qr_label = ttk.Label(right_frame)
|
|
||||||
self.qr_label.pack(pady=10)
|
|
||||||
|
|
||||||
# Server URL display
|
|
||||||
self.url_var = tk.StringVar(value="")
|
|
||||||
self.url_label = ttk.Label(right_frame, textvariable=self.url_var)
|
|
||||||
self.url_label.pack(pady=5)
|
|
||||||
|
|
||||||
# Open in browser button
|
|
||||||
self.browser_button = ttk.Button(right_frame, text="Open in Browser",
|
|
||||||
command=self.open_in_browser, state=tk.DISABLED)
|
|
||||||
self.browser_button.pack(pady=5)
|
|
||||||
|
|
||||||
# Display any existing macros
|
|
||||||
self.display_macros()
|
|
||||||
|
|
||||||
def load_macros(self):
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.data_file):
|
|
||||||
with open(self.data_file, "r") as file:
|
|
||||||
self.macros = json.load(file)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading macros: {e}")
|
|
||||||
self.macros = {}
|
|
||||||
|
|
||||||
def save_macros(self):
|
|
||||||
try:
|
|
||||||
with open(self.data_file, "w") as file:
|
|
||||||
json.dump(self.macros, file, indent=4)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving macros: {e}")
|
|
||||||
|
|
||||||
def display_macros(self):
|
|
||||||
# Clear the previous listbox
|
|
||||||
for widget in self.listbox_frame.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
# Create scrollable canvas for macros
|
|
||||||
canvas = tk.Canvas(self.listbox_frame, bg=self.bg_color, highlightthickness=0)
|
|
||||||
scrollbar = ttk.Scrollbar(self.listbox_frame, orient="vertical", command=canvas.yview)
|
|
||||||
scrollable_frame = ttk.Frame(canvas)
|
|
||||||
|
|
||||||
# Configure canvas and scrollable frame
|
|
||||||
scrollable_frame.bind(
|
|
||||||
"<Configure>",
|
|
||||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
||||||
)
|
|
||||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
|
||||||
canvas.configure(yscrollcommand=scrollbar.set)
|
|
||||||
|
|
||||||
# Pack canvas and scrollbar
|
|
||||||
canvas.pack(side="left", fill="both", expand=True)
|
|
||||||
scrollbar.pack(side="right", fill="y")
|
|
||||||
|
|
||||||
# Display macros
|
|
||||||
row = 0
|
|
||||||
for macro_id, macro in self.macros.items():
|
|
||||||
frame = ttk.Frame(scrollable_frame)
|
|
||||||
frame.pack(fill="x", pady=5, padx=5)
|
|
||||||
|
|
||||||
# Create button with image if available
|
|
||||||
button_text = macro["name"]
|
|
||||||
button = tk.Button(
|
|
||||||
frame, text=button_text, bg=self.button_bg, fg=self.button_fg,
|
|
||||||
activebackground=self.accent_color, activeforeground=self.button_fg,
|
|
||||||
relief=tk.RAISED, bd=2, pady=8, command=lambda mid=macro_id: self.execute_macro(mid)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Try to display image on button
|
|
||||||
if "image_path" in macro and macro["image_path"]:
|
|
||||||
try:
|
|
||||||
if macro["image_path"] in self.image_cache:
|
|
||||||
button_image = self.image_cache[macro["image_path"]]
|
|
||||||
else:
|
|
||||||
img_path = os.path.join(self.app_dir, macro["image_path"])
|
|
||||||
img = Image.open(img_path)
|
|
||||||
img = img.resize((32, 32))
|
|
||||||
button_image = ImageTk.PhotoImage(img)
|
|
||||||
self.image_cache[macro["image_path"]] = button_image
|
|
||||||
|
|
||||||
button.config(image=button_image, compound=tk.LEFT)
|
|
||||||
button.image = button_image # Keep a reference
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading image for {macro['name']}: {e}")
|
|
||||||
|
|
||||||
button.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
# Display message if no macros
|
|
||||||
if not self.macros:
|
|
||||||
label = tk.Label(scrollable_frame, text="No macros defined",
|
|
||||||
bg=self.bg_color, fg=self.fg_color)
|
|
||||||
label.pack(pady=20)
|
|
||||||
|
|
||||||
def toggle_server(self):
|
|
||||||
if self.server_running:
|
|
||||||
self.stop_server()
|
|
||||||
else:
|
|
||||||
self.start_server()
|
|
||||||
|
|
||||||
def start_server(self):
|
|
||||||
try:
|
|
||||||
if not self.server_running:
|
|
||||||
self.server_running = True
|
|
||||||
self.flask_thread = threading.Thread(target=self.run_web_server)
|
|
||||||
self.flask_thread.daemon = True
|
|
||||||
self.flask_thread.start()
|
|
||||||
self.server_button.config(text="Stop Web Server")
|
|
||||||
|
|
||||||
# Get the systems internal IP address
|
|
||||||
ip_address = self.get_ip_addresses()
|
|
||||||
if ip_address:
|
|
||||||
# Set the URL display
|
|
||||||
url = f"http://{ip_address}:{self.server_port}"
|
|
||||||
url_text = "Web UI available at:\n" + "\n" + url
|
|
||||||
self.url_var.set(url_text)
|
|
||||||
|
|
||||||
# Enable browser button
|
|
||||||
self.browser_button.config(state=tk.NORMAL)
|
|
||||||
|
|
||||||
# Generate and display QR code for the first IP
|
|
||||||
self.generate_qr_code(url)
|
|
||||||
else:
|
|
||||||
self.url_var.set("No network interfaces found")
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set(f"Error starting server: {e}")
|
|
||||||
self.server_running = False
|
|
||||||
|
|
||||||
def stop_server(self):
|
|
||||||
if self.server_running:
|
|
||||||
self.server_running = False
|
|
||||||
self.status_var.set("Web server stopped")
|
|
||||||
self.server_button.config(text="Start Web Server")
|
|
||||||
self.url_var.set("")
|
|
||||||
self.browser_button.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
# Clear QR code
|
|
||||||
self.qr_label.config(image="")
|
|
||||||
|
|
||||||
# The Flask server will be stopped on the next request
|
|
||||||
|
|
||||||
def get_ip_addresses(self):
|
|
||||||
"""Get the primary internal IPv4 address of the machine."""
|
|
||||||
try:
|
|
||||||
# Create a socket to connect to an external server
|
|
||||||
# This helps determine which network interface is used for outbound connections
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
# We don't need to actually send data - just configure the socket
|
|
||||||
s.connect(("8.8.8.8", 80))
|
|
||||||
# Get the IP address that would be used for this connection
|
|
||||||
ip = s.getsockname()[0]
|
|
||||||
s.close()
|
|
||||||
return ip
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error getting IP address: {e}")
|
|
||||||
# Fallback method if the above doesn't work
|
|
||||||
try:
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
ip = socket.gethostbyname(hostname)
|
|
||||||
# Don't return localhost address
|
|
||||||
if ip.startswith("127."):
|
|
||||||
for addr_info in socket.getaddrinfo(hostname, None):
|
|
||||||
potential_ip = addr_info[4][0]
|
|
||||||
# If IPv4 and not localhost
|
|
||||||
if '.' in potential_ip and not potential_ip.startswith("127."):
|
|
||||||
return potential_ip
|
|
||||||
else:
|
|
||||||
return ip
|
|
||||||
except:
|
|
||||||
return "127.0.0.1" # Last resort fallback
|
|
||||||
|
|
||||||
def generate_qr_code(self, url):
|
|
||||||
try:
|
|
||||||
# Try to import qrcode
|
|
||||||
import qrcode
|
|
||||||
|
|
||||||
# Generate QR code
|
|
||||||
qr = qrcode.QRCode(
|
|
||||||
version=1,
|
|
||||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
||||||
box_size=10,
|
|
||||||
border=4,
|
|
||||||
)
|
|
||||||
qr.add_data(url)
|
|
||||||
qr.make(fit=True)
|
|
||||||
|
|
||||||
# Create an image from the QR code
|
|
||||||
qr_img = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
|
|
||||||
# Convert to PhotoImage for display
|
|
||||||
qr_photoimg = ImageTk.PhotoImage(qr_img)
|
|
||||||
|
|
||||||
# Update label
|
|
||||||
self.qr_label.config(image=qr_photoimg)
|
|
||||||
self.qr_label.image = qr_photoimg # Keep a reference
|
|
||||||
except ImportError:
|
|
||||||
self.qr_label.config(text="QR code generation requires 'qrcode' package")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error generating QR code: {e}")
|
|
||||||
self.qr_label.config(text="Error generating QR code")
|
|
||||||
|
|
||||||
def open_in_browser(self):
|
|
||||||
# Open the web interface in the default browser
|
|
||||||
if self.server_running:
|
|
||||||
webbrowser.open(f"http://localhost:{self.server_port}")
|
|
||||||
|
|
||||||
def create_web_app(self):
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Disable Flask's logging except for errors
|
|
||||||
log = logging.getLogger('werkzeug')
|
|
||||||
log.setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
# Define HTML templates
|
|
||||||
index_html = '''
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MacroPad Web Interface</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #2e2e2e;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
.header-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #007acc;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.refresh-button {
|
|
||||||
background-color: #505050;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.refresh-button:hover {
|
|
||||||
background-color: #007acc;
|
|
||||||
}
|
|
||||||
.refresh-button svg {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
.macro-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.macro-button {
|
|
||||||
background-color: #505050;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px 10px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
.macro-button:hover, .macro-button:active {
|
|
||||||
background-color: #007acc;
|
|
||||||
}
|
|
||||||
.macro-button img {
|
|
||||||
max-width: 64px;
|
|
||||||
max-height: 64px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
text-align: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
background-color: #f44336;
|
|
||||||
color: white;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.macro-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
||||||
}
|
|
||||||
.macro-button {
|
|
||||||
padding: 10px 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.refresh-button {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header-container">
|
|
||||||
<h1>MacroPad Web Interface</h1>
|
|
||||||
<button class="refresh-button" onclick="loadMacros()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
|
||||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
|
||||||
</svg>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="status success" id="success-status">Macro executed successfully!</div>
|
|
||||||
<div class="status error" id="error-status">Failed to execute macro</div>
|
|
||||||
<div class="macro-grid" id="macro-grid">
|
|
||||||
<!-- Macros will be loaded here -->
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
loadMacros();
|
|
||||||
});
|
|
||||||
function loadMacros() {
|
|
||||||
fetch('/api/macros')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(macros => {
|
|
||||||
const macroGrid = document.getElementById('macro-grid');
|
|
||||||
macroGrid.innerHTML = '';
|
|
||||||
if (Object.keys(macros).length === 0) {
|
|
||||||
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros defined.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const [macroId, macro] of Object.entries(macros)) {
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.className = 'macro-button';
|
|
||||||
button.onclick = function() { executeMacro(macroId); };
|
|
||||||
// Add image if available
|
|
||||||
if (macro.image_path) {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
|
|
||||||
img.alt = macro.name;
|
|
||||||
img.onerror = function() {
|
|
||||||
this.style.display = 'none';
|
|
||||||
};
|
|
||||||
button.appendChild(img);
|
|
||||||
}
|
|
||||||
const text = document.createTextNode(macro.name);
|
|
||||||
button.appendChild(text);
|
|
||||||
macroGrid.appendChild(button);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading macros:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function executeMacro(macroId) {
|
|
||||||
fetch('/api/execute', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ macro_id: macroId })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
const successStatus = document.getElementById('success-status');
|
|
||||||
const errorStatus = document.getElementById('error-status');
|
|
||||||
if (data.success) {
|
|
||||||
successStatus.style.display = 'block';
|
|
||||||
errorStatus.style.display = 'none';
|
|
||||||
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
|
|
||||||
} else {
|
|
||||||
errorStatus.style.display = 'block';
|
|
||||||
successStatus.style.display = 'none';
|
|
||||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error executing macro:', error);
|
|
||||||
const errorStatus = document.getElementById('error-status');
|
|
||||||
errorStatus.style.display = 'block';
|
|
||||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template_string(index_html)
|
|
||||||
|
|
||||||
@app.route('/api/macros')
|
|
||||||
def get_macros():
|
|
||||||
return jsonify(self.macros)
|
|
||||||
|
|
||||||
@app.route('/api/image/<path:image_path>')
|
|
||||||
def get_image(image_path):
|
|
||||||
try:
|
|
||||||
# Using os.path.join would translate slashes incorrectly on Windows
|
|
||||||
# Use normpath instead
|
|
||||||
image_path = os.path.join(self.app_dir, image_path)
|
|
||||||
return send_file(image_path)
|
|
||||||
except Exception as e:
|
|
||||||
return str(e), 404
|
|
||||||
|
|
||||||
@app.route('/api/execute', methods=['POST'])
|
|
||||||
def execute_macro():
|
|
||||||
if not self.server_running:
|
|
||||||
return jsonify({"success": False, "error": "Server is shutting down"})
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
if not data or 'macro_id' not in data:
|
|
||||||
return jsonify({"success": False, "error": "Invalid request"})
|
|
||||||
|
|
||||||
macro_id = data['macro_id']
|
|
||||||
success = self.execute_macro(macro_id)
|
|
||||||
return jsonify({"success": success})
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
def run_web_server(self):
|
|
||||||
# Create Flask app
|
|
||||||
self.web_app = self.create_web_app()
|
|
||||||
|
|
||||||
# Update the status in the GUI
|
|
||||||
self.status_var.set(f"Web server running on port {self.server_port}")
|
|
||||||
|
|
||||||
# Run the Flask app with waitress for production-ready serving
|
|
||||||
try:
|
|
||||||
serve(self.web_app, host='0.0.0.0', port=self.server_port, threads=4)
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set(f"Web server error: {e}")
|
|
||||||
self.server_running = False
|
|
||||||
|
|
||||||
def add_macro(self):
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Add Macro")
|
|
||||||
dialog.geometry("450x350") # Increased height for additional options
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Apply dark theme to dialog
|
|
||||||
tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
name_entry.grid(row=0, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
type_var = tk.StringVar(value="text")
|
|
||||||
|
|
||||||
radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color}
|
|
||||||
tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w")
|
|
||||||
tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w")
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
command_text.grid(row=3, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Modifiers frame
|
|
||||||
mod_frame = tk.Frame(dialog, bg=self.bg_color)
|
|
||||||
mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
|
||||||
|
|
||||||
tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
# Add checkboxes for modifiers
|
|
||||||
ctrl_var = tk.BooleanVar(value=False)
|
|
||||||
alt_var = tk.BooleanVar(value=False)
|
|
||||||
shift_var = tk.BooleanVar(value=False)
|
|
||||||
enter_var = tk.BooleanVar(value=False)
|
|
||||||
|
|
||||||
checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color,
|
|
||||||
'activebackground': self.bg_color, 'activeforeground': self.fg_color}
|
|
||||||
|
|
||||||
tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
image_path = tk.StringVar()
|
|
||||||
tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
image_entry = tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
image_entry.grid(row=5, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename(
|
|
||||||
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2)
|
|
||||||
|
|
||||||
def save_macro():
|
|
||||||
name = name_entry.get().strip()
|
|
||||||
if not name:
|
|
||||||
tk.messagebox.showerror("Error", "Macro name is required")
|
|
||||||
return
|
|
||||||
|
|
||||||
macro_type = type_var.get()
|
|
||||||
command = command_text.get("1.0", tk.END).strip()
|
|
||||||
macro_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Process image if provided
|
|
||||||
image_path_reference = ""
|
|
||||||
img_path = image_path.get()
|
|
||||||
if img_path:
|
|
||||||
try:
|
|
||||||
# Generate unique filename for the image
|
|
||||||
file_ext = os.path.splitext(img_path)[1].lower()
|
|
||||||
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
|
|
||||||
dest_path = os.path.join(self.images_dir, unique_filename)
|
|
||||||
|
|
||||||
# Resize image to max 256x256
|
|
||||||
with Image.open(img_path) as img:
|
|
||||||
img.thumbnail((256, 256))
|
|
||||||
img.save(dest_path)
|
|
||||||
|
|
||||||
# Store the relative path to the image
|
|
||||||
image_path_reference = os.path.join("macro_images", unique_filename)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing image: {e}")
|
|
||||||
|
|
||||||
# Create macro with modifier keys
|
|
||||||
self.macros[macro_id] = {
|
|
||||||
"name": name,
|
|
||||||
"type": macro_type,
|
|
||||||
"command": command,
|
|
||||||
"image_path": image_path_reference,
|
|
||||||
"modifiers": {
|
|
||||||
"ctrl": ctrl_var.get(),
|
|
||||||
"alt": alt_var.get(),
|
|
||||||
"shift": shift_var.get(),
|
|
||||||
"enter": enter_var.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_macros()
|
|
||||||
self.display_macros()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Save", command=save_macro, **button_style).grid(row=6, column=0, padx=5, pady=20)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20)
|
|
||||||
|
|
||||||
def edit_macro(self):
|
|
||||||
# First, show a dialog to select which macro to edit
|
|
||||||
if not self.macros:
|
|
||||||
tk.messagebox.showinfo("No Macros", "There are no macros to edit.")
|
|
||||||
return
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Select Macro to Edit")
|
|
||||||
dialog.geometry("200x340")
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Create a listbox to show available macros
|
|
||||||
tk.Label(dialog, text="Select a macro to edit:", bg=self.bg_color, fg=self.fg_color).pack(pady=5)
|
|
||||||
|
|
||||||
listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color)
|
|
||||||
listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
||||||
|
|
||||||
# Populate the listbox with macro names
|
|
||||||
macro_ids = []
|
|
||||||
for macro_id, macro in self.macros.items():
|
|
||||||
listbox.insert(tk.END, macro["name"])
|
|
||||||
macro_ids.append(macro_id)
|
|
||||||
|
|
||||||
def on_select():
|
|
||||||
if not listbox.curselection():
|
|
||||||
tk.messagebox.showwarning("No Selection", "Please select a macro to edit.")
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = listbox.curselection()[0]
|
|
||||||
selected_macro_id = macro_ids[idx]
|
|
||||||
dialog.destroy()
|
|
||||||
self.open_edit_dialog(selected_macro_id)
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Edit Selected", command=on_select, **button_style).pack(pady=10)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5)
|
|
||||||
|
|
||||||
def open_edit_dialog(self, macro_id):
|
|
||||||
# Open dialog to edit the selected macro
|
|
||||||
macro = self.macros[macro_id]
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Edit Macro")
|
|
||||||
dialog.geometry("450x350") # Increased height for additional options
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Apply dark theme to dialog
|
|
||||||
tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
name_entry.grid(row=0, column=1, padx=5, pady=5)
|
|
||||||
name_entry.insert(0, macro["name"])
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
type_var = tk.StringVar(value=macro["type"])
|
|
||||||
|
|
||||||
radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color}
|
|
||||||
tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w")
|
|
||||||
tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w")
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
command_text.grid(row=3, column=1, padx=5, pady=5)
|
|
||||||
command_text.insert("1.0", macro["command"])
|
|
||||||
|
|
||||||
# Modifiers frame
|
|
||||||
mod_frame = tk.Frame(dialog, bg=self.bg_color)
|
|
||||||
mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
|
||||||
|
|
||||||
tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
# Get existing modifiers or set defaults
|
|
||||||
modifiers = macro.get("modifiers", {})
|
|
||||||
|
|
||||||
# Add checkboxes for modifiers
|
|
||||||
ctrl_var = tk.BooleanVar(value=modifiers.get("ctrl", False))
|
|
||||||
alt_var = tk.BooleanVar(value=modifiers.get("alt", False))
|
|
||||||
shift_var = tk.BooleanVar(value=modifiers.get("shift", False))
|
|
||||||
enter_var = tk.BooleanVar(value=modifiers.get("enter", False))
|
|
||||||
|
|
||||||
checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color,
|
|
||||||
'activebackground': self.bg_color, 'activeforeground': self.fg_color}
|
|
||||||
|
|
||||||
tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
image_path = tk.StringVar()
|
|
||||||
tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
image_entry = tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
image_entry.grid(row=5, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename(
|
|
||||||
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2)
|
|
||||||
|
|
||||||
def save_edited_macro():
|
|
||||||
name = name_entry.get().strip()
|
|
||||||
if not name:
|
|
||||||
tk.messagebox.showerror("Error", "Macro name is required")
|
|
||||||
return
|
|
||||||
|
|
||||||
new_type = type_var.get()
|
|
||||||
command = command_text.get("1.0", tk.END).strip()
|
|
||||||
|
|
||||||
# Keep the old image or update with new one
|
|
||||||
image_path_reference = macro.get("image_path", "")
|
|
||||||
img_path = image_path.get()
|
|
||||||
if img_path:
|
|
||||||
try:
|
|
||||||
# Generate unique filename for the image
|
|
||||||
file_ext = os.path.splitext(img_path)[1].lower()
|
|
||||||
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
|
|
||||||
dest_path = os.path.join(self.images_dir, unique_filename)
|
|
||||||
|
|
||||||
# Resize image to max 256x256
|
|
||||||
with Image.open(img_path) as img:
|
|
||||||
img.thumbnail((256, 256))
|
|
||||||
img.save(dest_path)
|
|
||||||
|
|
||||||
# Store the relative path to the image
|
|
||||||
image_path_reference = os.path.join("macro_images", unique_filename)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing image: {e}")
|
|
||||||
|
|
||||||
# Update macro with modifiers
|
|
||||||
self.macros[macro_id] = {
|
|
||||||
"name": name,
|
|
||||||
"type": new_type,
|
|
||||||
"command": command,
|
|
||||||
"image_path": image_path_reference,
|
|
||||||
"modifiers": {
|
|
||||||
"ctrl": ctrl_var.get(),
|
|
||||||
"alt": alt_var.get(),
|
|
||||||
"shift": shift_var.get(),
|
|
||||||
"enter": enter_var.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_macros()
|
|
||||||
self.display_macros()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Save", command=save_edited_macro, **button_style).grid(row=6, column=0, padx=5, pady=20)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20)
|
|
||||||
|
|
||||||
def delete_macro(self):
|
|
||||||
# Show a dialog to select which macro to delete
|
|
||||||
if not self.macros:
|
|
||||||
tk.messagebox.showinfo("No Macros", "There are no macros to delete.")
|
|
||||||
return
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Delete Macro")
|
|
||||||
dialog.geometry("200x340")
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Create a listbox to show available macros
|
|
||||||
tk.Label(dialog, text="Select a macro to delete:", bg=self.bg_color, fg=self.fg_color).pack(pady=5)
|
|
||||||
|
|
||||||
listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color)
|
|
||||||
listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
||||||
|
|
||||||
# Populate the listbox with macro names
|
|
||||||
macro_ids = []
|
|
||||||
for macro_id, macro in self.macros.items():
|
|
||||||
listbox.insert(tk.END, macro["name"])
|
|
||||||
macro_ids.append(macro_id)
|
|
||||||
|
|
||||||
def on_delete():
|
|
||||||
if not listbox.curselection():
|
|
||||||
tk.messagebox.showwarning("No Selection", "Please select a macro to delete.")
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = listbox.curselection()[0]
|
|
||||||
selected_macro_id = macro_ids[idx]
|
|
||||||
selected_name = self.macros[selected_macro_id]["name"]
|
|
||||||
|
|
||||||
# Confirm deletion
|
|
||||||
if tk.messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{selected_name}'?"):
|
|
||||||
# Delete associated image file if it exists
|
|
||||||
macro = self.macros[selected_macro_id]
|
|
||||||
if "image_path" in macro and macro["image_path"]:
|
|
||||||
try:
|
|
||||||
img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"])
|
|
||||||
if os.path.exists(img_path):
|
|
||||||
os.remove(img_path)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error removing image file: {e}")
|
|
||||||
del self.macros[selected_macro_id]
|
|
||||||
self.save_macros()
|
|
||||||
self.display_macros()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Delete Selected", command=on_delete, **button_style).pack(pady=10)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5)
|
|
||||||
|
|
||||||
def execute_macro(self, macro_id):
|
|
||||||
if macro_id not in self.macros:
|
|
||||||
return False
|
|
||||||
|
|
||||||
macro = self.macros[macro_id]
|
|
||||||
try:
|
|
||||||
if macro["type"] == "text":
|
|
||||||
# Handle key modifiers
|
|
||||||
modifiers = macro.get("modifiers", {})
|
|
||||||
|
|
||||||
# Add modifier keys if enabled
|
|
||||||
if modifiers.get("ctrl", False):
|
|
||||||
pyautogui.keyDown('ctrl')
|
|
||||||
if modifiers.get("alt", False):
|
|
||||||
pyautogui.keyDown('alt')
|
|
||||||
if modifiers.get("shift", False):
|
|
||||||
pyautogui.keyDown('shift')
|
|
||||||
if str(macro["command"]) and len(str(macro["command"])) == 1:
|
|
||||||
pyautogui.keyDown(macro["command"])
|
|
||||||
time.sleep(0.5)
|
|
||||||
pyautogui.keyUp(macro["command"])
|
|
||||||
else:
|
|
||||||
pyautogui.typewrite(macro["command"], interval=0.02)
|
|
||||||
# Release modifier keys in reverse order
|
|
||||||
if modifiers.get("shift", False):
|
|
||||||
pyautogui.keyUp('shift')
|
|
||||||
if modifiers.get("alt", False):
|
|
||||||
pyautogui.keyUp('alt')
|
|
||||||
if modifiers.get("ctrl", False):
|
|
||||||
pyautogui.keyUp('ctrl')
|
|
||||||
|
|
||||||
# Add Enter/Return if requested
|
|
||||||
if modifiers.get("enter", False):
|
|
||||||
pyautogui.press('enter')
|
|
||||||
|
|
||||||
elif macro["type"] == "app":
|
|
||||||
subprocess.Popen(macro["command"], shell=True)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error executing macro: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_tray_icon(self):
|
|
||||||
# Create tray icon
|
|
||||||
icon_image = Image.new("RGB", (64, 64), self.accent_color)
|
|
||||||
menu = (
|
|
||||||
pystray.MenuItem('Show', self.show_window),
|
|
||||||
pystray.MenuItem('Exit', self.exit_app)
|
|
||||||
)
|
|
||||||
self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu)
|
|
||||||
self.tray_icon.run_detached()
|
|
||||||
|
|
||||||
def show_window(self, icon=None, item=None):
|
|
||||||
# Show the window from tray
|
|
||||||
self.root.deiconify()
|
|
||||||
self.root.state('normal')
|
|
||||||
self.root.lift()
|
|
||||||
self.root.focus_force()
|
|
||||||
|
|
||||||
def exit_app(self, icon=None, item=None):
|
|
||||||
# Actually exit the app
|
|
||||||
self.stop_server()
|
|
||||||
self.server_running = False
|
|
||||||
if hasattr(self, 'tray_icon'):
|
|
||||||
self.tray_icon.stop()
|
|
||||||
self.root.quit()
|
|
||||||
|
|
||||||
def minimize_to_tray(self):
|
|
||||||
# Create tray icon if it doesn't exist
|
|
||||||
if not hasattr(self, 'tray_icon'):
|
|
||||||
self.create_tray_icon()
|
|
||||||
self.root.withdraw()
|
|
||||||
|
|
||||||
def on_closing(self):
|
|
||||||
# When the window is closed, minimize to tray instead of closing
|
|
||||||
self.minimize_to_tray()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
root = tk.Tk()
|
|
||||||
app = MacroPadServer(root)
|
|
||||||
root.mainloop()
|
|
645
mp-server.py
645
mp-server.py
@ -1,645 +0,0 @@
|
|||||||
import tkinter as tk
|
|
||||||
from tkinter import filedialog, ttk
|
|
||||||
import json
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
import pyautogui
|
|
||||||
import subprocess
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
import pystray # Add this import for system tray functionality
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
class MacroPadServer:
|
|
||||||
def __init__(self, root):
|
|
||||||
self.root = root
|
|
||||||
self.root.title("MacroPad Server")
|
|
||||||
self.root.geometry("800x600")
|
|
||||||
|
|
||||||
# Create the image Directory
|
|
||||||
self.images_dir = "macro_images"
|
|
||||||
os.makedirs(self.images_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Set dark theme colors
|
|
||||||
self.bg_color = "#2E2E2E"
|
|
||||||
self.fg_color = "#FFFFFF"
|
|
||||||
self.highlight_color = "#3D3D3D"
|
|
||||||
self.accent_color = "#007BFF"
|
|
||||||
self.button_bg = "#444444"
|
|
||||||
self.button_fg = "#FFFFFF"
|
|
||||||
|
|
||||||
# Apply theme
|
|
||||||
self.root.configure(bg=self.bg_color)
|
|
||||||
self.style = ttk.Style()
|
|
||||||
self.style.theme_use('clam')
|
|
||||||
|
|
||||||
# Configure styles
|
|
||||||
self.style.configure('TFrame', background=self.bg_color)
|
|
||||||
self.style.configure('TLabel', background=self.bg_color, foreground=self.fg_color)
|
|
||||||
self.style.configure('TButton', background=self.button_bg, foreground=self.button_fg, borderwidth=0)
|
|
||||||
self.style.map('TButton',
|
|
||||||
background=[('active', self.accent_color), ('disabled', self.highlight_color)],
|
|
||||||
foreground=[('active', self.button_fg), ('disabled', '#999999')])
|
|
||||||
self.style.configure('TRadiobutton', background=self.bg_color, foreground=self.fg_color)
|
|
||||||
self.macros = {}
|
|
||||||
self.load_macros()
|
|
||||||
|
|
||||||
# Create GUI
|
|
||||||
self.create_widgets()
|
|
||||||
|
|
||||||
# Start server thread
|
|
||||||
self.server_running = True
|
|
||||||
self.server_thread = threading.Thread(target=self.run_server)
|
|
||||||
self.server_thread.daemon = True
|
|
||||||
self.server_thread.start()
|
|
||||||
|
|
||||||
# Set up system tray icon
|
|
||||||
self.setup_tray_icon()
|
|
||||||
|
|
||||||
# Capture the window close event
|
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
||||||
# Capture the window minimize event
|
|
||||||
self.root.bind("<Unmap>", lambda e: self.minimize_to_tray() if self.root.state() == 'iconic' else None)
|
|
||||||
|
|
||||||
def setup_tray_icon(self):
|
|
||||||
# Create a simple icon for the tray
|
|
||||||
icon_image = Image.new("RGB", (64, 64), color=self.accent_color)
|
|
||||||
|
|
||||||
# Menu for the tray icon
|
|
||||||
menu = (
|
|
||||||
pystray.MenuItem('Show', self.show_window),
|
|
||||||
pystray.MenuItem('Exit', self.exit_app)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.icon = pystray.Icon("macropad_server", icon_image, "MacroPad Server", menu)
|
|
||||||
|
|
||||||
def minimize_to_tray(self):
|
|
||||||
self.root.withdraw() # Hide window
|
|
||||||
if not self.icon.visible:
|
|
||||||
# Start the tray icon if it's not already running
|
|
||||||
self.icon_thread = threading.Thread(target=self.icon.run)
|
|
||||||
self.icon_thread.daemon = True
|
|
||||||
self.icon_thread.start()
|
|
||||||
|
|
||||||
def show_window(self, icon=None, item=None):
|
|
||||||
self.root.deiconify() # Show window
|
|
||||||
self.root.lift() # Bring window to front
|
|
||||||
self.root.focus_force() # Focus the window
|
|
||||||
if self.icon.visible:
|
|
||||||
self.icon.stop() # Remove icon
|
|
||||||
|
|
||||||
def exit_app(self, icon=None, item=None):
|
|
||||||
if self.icon.visible:
|
|
||||||
self.icon.stop()
|
|
||||||
self.server_running = False
|
|
||||||
self.save_macros()
|
|
||||||
self.root.destroy()
|
|
||||||
|
|
||||||
def create_widgets(self):
|
|
||||||
# Toolbar
|
|
||||||
toolbar = tk.Frame(self.root, bg=self.highlight_color)
|
|
||||||
toolbar.pack(side=tk.TOP, fill=tk.X)
|
|
||||||
|
|
||||||
# Custom button style
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'padx': 10,
|
|
||||||
'pady': 5, 'bd': 0, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'relief': tk.FLAT}
|
|
||||||
tk.Button(toolbar, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5)
|
|
||||||
tk.Button(toolbar, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5)
|
|
||||||
tk.Button(toolbar, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5)
|
|
||||||
tk.Button(toolbar, text="Minimize to Tray", command=self.minimize_to_tray, **button_style).pack(side=tk.LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Macro list frame
|
|
||||||
list_frame = tk.Frame(self.root, bg=self.bg_color)
|
|
||||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# Scrollable canvas for macros
|
|
||||||
self.canvas = tk.Canvas(list_frame, bg=self.bg_color, highlightthickness=0)
|
|
||||||
scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=self.canvas.yview)
|
|
||||||
self.scrollable_frame = tk.Frame(self.canvas, bg=self.bg_color)
|
|
||||||
|
|
||||||
self.scrollable_frame.bind(
|
|
||||||
"<Configure>",
|
|
||||||
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
|
||||||
)
|
|
||||||
|
|
||||||
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
|
||||||
self.canvas.configure(yscrollcommand=scrollbar.set)
|
|
||||||
|
|
||||||
self.canvas.pack(side="left", fill="both", expand=True)
|
|
||||||
scrollbar.pack(side="right", fill="y")
|
|
||||||
|
|
||||||
self.display_macros()
|
|
||||||
|
|
||||||
# Status bar
|
|
||||||
self.status_var = tk.StringVar()
|
|
||||||
self.status_var.set("Server ready. Waiting for connections...")
|
|
||||||
status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN,
|
|
||||||
anchor=tk.W, bg=self.highlight_color, fg=self.fg_color)
|
|
||||||
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
|
||||||
|
|
||||||
def load_macros(self):
|
|
||||||
try:
|
|
||||||
if os.path.exists("macros.json"):
|
|
||||||
with open("macros.json", "r") as f:
|
|
||||||
self.macros = json.load(f)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading macros: {e}")
|
|
||||||
self.macros = {}
|
|
||||||
|
|
||||||
def save_macros(self):
|
|
||||||
try:
|
|
||||||
with open("macros.json", "w") as f:
|
|
||||||
json.dump(self.macros, f)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving macros: {e}")
|
|
||||||
|
|
||||||
def display_macros(self):
|
|
||||||
# Clear existing macros
|
|
||||||
for widget in self.scrollable_frame.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
# Display macros in a grid
|
|
||||||
row, col = 0, 0
|
|
||||||
max_cols = 3
|
|
||||||
|
|
||||||
for macro_id, macro in self.macros.items():
|
|
||||||
frame = tk.Frame(self.scrollable_frame, bd=2, relief=tk.RAISED, padx=5, pady=5,
|
|
||||||
bg=self.highlight_color)
|
|
||||||
frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew")
|
|
||||||
|
|
||||||
# Load image if exists
|
|
||||||
if "image_path" in macro and macro["image_path"]:
|
|
||||||
try:
|
|
||||||
img_path = macro["image_path"]
|
|
||||||
img = Image.open(img_path)
|
|
||||||
# Resize for display
|
|
||||||
img.thumbnail((64, 64)) # Keep aspect ratio for display
|
|
||||||
photo = ImageTk.PhotoImage(img)
|
|
||||||
label = tk.Label(frame, image=photo, bg=self.highlight_color)
|
|
||||||
label.image = photo # Keep a reference
|
|
||||||
label.pack()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error displaying image: {e}")
|
|
||||||
tk.Label(frame, text="[No Image]", width=8, height=4, bg=self.highlight_color, fg=self.fg_color).pack()
|
|
||||||
else:
|
|
||||||
tk.Label(frame, text="[No Image]", width=8, height=4, bg=self.highlight_color, fg=self.fg_color).pack()
|
|
||||||
|
|
||||||
tk.Label(frame, text=macro["name"], bg=self.highlight_color, fg=self.fg_color).pack()
|
|
||||||
|
|
||||||
col += 1
|
|
||||||
if col >= max_cols:
|
|
||||||
col = 0
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
def add_macro(self):
|
|
||||||
# Create a dialog to add a new macro
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Add Macro")
|
|
||||||
dialog.geometry("450x350") # Increased height for additional options
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Apply dark theme to dialog
|
|
||||||
tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
name_entry.grid(row=0, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
type_var = tk.StringVar(value="text")
|
|
||||||
|
|
||||||
radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color}
|
|
||||||
tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w")
|
|
||||||
tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w")
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
command_text.grid(row=3, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Key modifiers frame
|
|
||||||
mod_frame = tk.Frame(dialog, bg=self.bg_color)
|
|
||||||
mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
|
||||||
|
|
||||||
tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
# Add checkboxes for modifiers
|
|
||||||
ctrl_var = tk.BooleanVar(value=False)
|
|
||||||
alt_var = tk.BooleanVar(value=False)
|
|
||||||
shift_var = tk.BooleanVar(value=False)
|
|
||||||
enter_var = tk.BooleanVar(value=False)
|
|
||||||
|
|
||||||
checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color,
|
|
||||||
'activebackground': self.bg_color, 'activeforeground': self.fg_color}
|
|
||||||
|
|
||||||
tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
image_path = tk.StringVar()
|
|
||||||
tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color).grid(row=5, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename(
|
|
||||||
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2)
|
|
||||||
|
|
||||||
def save_macro():
|
|
||||||
name = name_entry.get().strip()
|
|
||||||
if not name:
|
|
||||||
tk.messagebox.showerror("Error", "Macro name is required")
|
|
||||||
return
|
|
||||||
|
|
||||||
macro_type = type_var.get()
|
|
||||||
command = command_text.get("1.0", tk.END).strip()
|
|
||||||
|
|
||||||
# Generate a unique ID
|
|
||||||
macro_id = str(len(self.macros) + 1)
|
|
||||||
|
|
||||||
# Process image
|
|
||||||
image_path_reference = ""
|
|
||||||
img_path = image_path.get()
|
|
||||||
if img_path:
|
|
||||||
try:
|
|
||||||
# Generate unique filename for the image
|
|
||||||
file_ext = os.path.splitext(img_path)[1].lower()
|
|
||||||
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
|
|
||||||
dest_path = os.path.join(self.images_dir, unique_filename)
|
|
||||||
|
|
||||||
# Resize image to max 256x256
|
|
||||||
with Image.open(img_path) as img:
|
|
||||||
img.thumbnail((256, 256))
|
|
||||||
img.save(dest_path)
|
|
||||||
|
|
||||||
# Store the relative path to the image
|
|
||||||
image_path_reference = os.path.join("macro_images", unique_filename)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing image: {e}")
|
|
||||||
|
|
||||||
# Create macro with modifier keys
|
|
||||||
self.macros[macro_id] = {
|
|
||||||
"name": name,
|
|
||||||
"type": macro_type,
|
|
||||||
"command": command,
|
|
||||||
"image_path": image_path_reference,
|
|
||||||
"modifiers": {
|
|
||||||
"ctrl": ctrl_var.get(),
|
|
||||||
"alt": alt_var.get(),
|
|
||||||
"shift": shift_var.get(),
|
|
||||||
"enter": enter_var.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_macros()
|
|
||||||
self.display_macros()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Save", command=save_macro, **button_style).grid(row=6, column=0, padx=5, pady=20)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20)
|
|
||||||
|
|
||||||
def edit_macro(self):
|
|
||||||
# First, show a dialog to select which macro to edit
|
|
||||||
if not self.macros:
|
|
||||||
tk.messagebox.showinfo("No Macros", "There are no macros to edit.")
|
|
||||||
return
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Select Macro to Edit")
|
|
||||||
dialog.geometry("200x340")
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Create a listbox to show available macros
|
|
||||||
tk.Label(dialog, text="Select a macro to edit:", bg=self.bg_color, fg=self.fg_color).pack(pady=5)
|
|
||||||
|
|
||||||
listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color)
|
|
||||||
listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
||||||
|
|
||||||
# Populate the listbox with macro names
|
|
||||||
macro_ids = []
|
|
||||||
for macro_id, macro in self.macros.items():
|
|
||||||
listbox.insert(tk.END, macro["name"])
|
|
||||||
macro_ids.append(macro_id)
|
|
||||||
|
|
||||||
def on_select():
|
|
||||||
if not listbox.curselection():
|
|
||||||
tk.messagebox.showwarning("No Selection", "Please select a macro to edit.")
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = listbox.curselection()[0]
|
|
||||||
selected_macro_id = macro_ids[idx]
|
|
||||||
dialog.destroy()
|
|
||||||
self.open_edit_dialog(selected_macro_id)
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Edit Selected", command=on_select, **button_style).pack(pady=10)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5)
|
|
||||||
|
|
||||||
def open_edit_dialog(self, macro_id):
|
|
||||||
# Open dialog to edit the selected macro
|
|
||||||
macro = self.macros[macro_id]
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Edit Macro")
|
|
||||||
dialog.geometry("450x350") # Increased height for additional options
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Apply dark theme to dialog
|
|
||||||
tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
name_entry.grid(row=0, column=1, padx=5, pady=5)
|
|
||||||
name_entry.insert(0, macro["name"])
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
type_var = tk.StringVar(value=macro["type"])
|
|
||||||
|
|
||||||
radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color}
|
|
||||||
tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w")
|
|
||||||
tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w")
|
|
||||||
|
|
||||||
tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
command_text.grid(row=3, column=1, padx=5, pady=5)
|
|
||||||
command_text.insert("1.0", macro["command"])
|
|
||||||
|
|
||||||
# Modifiers frame
|
|
||||||
mod_frame = tk.Frame(dialog, bg=self.bg_color)
|
|
||||||
mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
|
||||||
|
|
||||||
tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
# Get existing modifiers or set defaults
|
|
||||||
modifiers = macro.get("modifiers", {})
|
|
||||||
|
|
||||||
# Add checkboxes for modifiers
|
|
||||||
ctrl_var = tk.BooleanVar(value=modifiers.get("ctrl", False))
|
|
||||||
alt_var = tk.BooleanVar(value=modifiers.get("alt", False))
|
|
||||||
shift_var = tk.BooleanVar(value=modifiers.get("shift", False))
|
|
||||||
enter_var = tk.BooleanVar(value=modifiers.get("enter", False))
|
|
||||||
|
|
||||||
checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color,
|
|
||||||
'activebackground': self.bg_color, 'activeforeground': self.fg_color}
|
|
||||||
|
|
||||||
tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
image_path = tk.StringVar()
|
|
||||||
tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
image_entry = tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
|
|
||||||
image_entry.grid(row=5, column=1, padx=5, pady=5)
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename(
|
|
||||||
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2)
|
|
||||||
|
|
||||||
def save_edited_macro():
|
|
||||||
name = name_entry.get().strip()
|
|
||||||
if not name:
|
|
||||||
tk.messagebox.showerror("Error", "Macro name is required")
|
|
||||||
return
|
|
||||||
|
|
||||||
new_type = type_var.get()
|
|
||||||
command = command_text.get("1.0", tk.END).strip()
|
|
||||||
|
|
||||||
# Keep the old image or update with new one
|
|
||||||
image_path_reference = macro.get("image_path", "")
|
|
||||||
img_path = image_path.get()
|
|
||||||
if img_path:
|
|
||||||
try:
|
|
||||||
# Generate unique filename for the image
|
|
||||||
file_ext = os.path.splitext(img_path)[1].lower()
|
|
||||||
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
|
|
||||||
dest_path = os.path.join(self.images_dir, unique_filename)
|
|
||||||
|
|
||||||
# Resize image to max 256x256
|
|
||||||
with Image.open(img_path) as img:
|
|
||||||
img.thumbnail((256, 256))
|
|
||||||
img.save(dest_path)
|
|
||||||
|
|
||||||
# Store the relative path to the image
|
|
||||||
image_path_reference = os.path.join("macro_images", unique_filename)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing image: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# Update macro with modifiers
|
|
||||||
self.macros[macro_id] = {
|
|
||||||
"name": name,
|
|
||||||
"type": new_type,
|
|
||||||
"command": command,
|
|
||||||
"image_path": image_path_reference,
|
|
||||||
"modifiers": {
|
|
||||||
"ctrl": ctrl_var.get(),
|
|
||||||
"alt": alt_var.get(),
|
|
||||||
"shift": shift_var.get(),
|
|
||||||
"enter": enter_var.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_macros()
|
|
||||||
self.display_macros()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Save", command=save_edited_macro, **button_style).grid(row=6, column=0, padx=5, pady=20)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20)
|
|
||||||
|
|
||||||
def delete_macro(self):
|
|
||||||
# Show a dialog to select which macro to delete
|
|
||||||
if not self.macros:
|
|
||||||
tk.messagebox.showinfo("No Macros", "There are no macros to delete.")
|
|
||||||
return
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(self.root)
|
|
||||||
dialog.title("Delete Macro")
|
|
||||||
dialog.geometry("200x340")
|
|
||||||
dialog.transient(self.root)
|
|
||||||
dialog.configure(bg=self.bg_color)
|
|
||||||
|
|
||||||
# Create a listbox to show available macros
|
|
||||||
tk.Label(dialog, text="Select a macro to delete:", bg=self.bg_color, fg=self.fg_color).pack(pady=5)
|
|
||||||
|
|
||||||
listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color)
|
|
||||||
listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
||||||
|
|
||||||
# Populate the listbox with macro names
|
|
||||||
macro_ids = []
|
|
||||||
for macro_id, macro in self.macros.items():
|
|
||||||
listbox.insert(tk.END, macro["name"])
|
|
||||||
macro_ids.append(macro_id)
|
|
||||||
|
|
||||||
def on_delete():
|
|
||||||
if not listbox.curselection():
|
|
||||||
tk.messagebox.showwarning("No Selection", "Please select a macro to delete.")
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = listbox.curselection()[0]
|
|
||||||
selected_macro_id = macro_ids[idx]
|
|
||||||
selected_name = self.macros[selected_macro_id]["name"]
|
|
||||||
|
|
||||||
# Confirm deletion
|
|
||||||
if tk.messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{selected_name}'?"):
|
|
||||||
# Delete associated image file if it exists
|
|
||||||
macro = self.macros[selected_macro_id]
|
|
||||||
if "image_path" in macro and macro["image_path"]:
|
|
||||||
try:
|
|
||||||
img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"])
|
|
||||||
if os.path.exists(img_path):
|
|
||||||
os.remove(img_path)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error removing image file: {e}")
|
|
||||||
del self.macros[selected_macro_id]
|
|
||||||
self.save_macros()
|
|
||||||
self.display_macros()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
|
|
||||||
'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
|
|
||||||
|
|
||||||
tk.Button(dialog, text="Delete Selected", command=on_delete, **button_style).pack(pady=10)
|
|
||||||
tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5)
|
|
||||||
|
|
||||||
def execute_macro(self, macro_id):
|
|
||||||
if macro_id not in self.macros:
|
|
||||||
return False
|
|
||||||
|
|
||||||
macro = self.macros[macro_id]
|
|
||||||
try:
|
|
||||||
if macro["type"] == "text":
|
|
||||||
# Handle key modifiers
|
|
||||||
modifiers = macro.get("modifiers", {})
|
|
||||||
keys_to_press = []
|
|
||||||
|
|
||||||
# Add modifier keys if enabled
|
|
||||||
if modifiers.get("ctrl", False):
|
|
||||||
keys_to_press.append('ctrl')
|
|
||||||
if modifiers.get("alt", False):
|
|
||||||
keys_to_press.append('alt')
|
|
||||||
if modifiers.get("shift", False):
|
|
||||||
keys_to_press.append('shift')
|
|
||||||
|
|
||||||
# If there are modifier keys, use hotkey functionality
|
|
||||||
if keys_to_press:
|
|
||||||
# For single characters with modifiers, use hotkey
|
|
||||||
if len(macro["command"]) == 1:
|
|
||||||
keys_to_press.append(macro["command"], interval=0.02)
|
|
||||||
pyautogui.hotkey(*keys_to_press)
|
|
||||||
else:
|
|
||||||
# For longer text, press modifiers, type text, then release
|
|
||||||
for key in keys_to_press:
|
|
||||||
pyautogui.keyDown(key)
|
|
||||||
pyautogui.typewrite(macro["command"], interval=0.02)
|
|
||||||
for key in reversed(keys_to_press):
|
|
||||||
pyautogui.keyUp(key)
|
|
||||||
else:
|
|
||||||
# No modifiers, just type the text
|
|
||||||
pyautogui.typewrite(macro["command"], interval=0.02)
|
|
||||||
|
|
||||||
# Add Enter/Return if requested
|
|
||||||
if modifiers.get("enter", False):
|
|
||||||
pyautogui.press('enter')
|
|
||||||
|
|
||||||
elif macro["type"] == "app":
|
|
||||||
subprocess.Popen(macro["command"], shell=True)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error executing macro: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run_server(self):
|
|
||||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
server_socket.bind(('0.0.0.0', 40000))
|
|
||||||
server_socket.listen(5)
|
|
||||||
|
|
||||||
while self.server_running:
|
|
||||||
client_socket, address = server_socket.accept()
|
|
||||||
self.status_var.set(f"Client connected from {address}")
|
|
||||||
|
|
||||||
client_thread = threading.Thread(target=self.handle_client, args=(client_socket,))
|
|
||||||
client_thread.daemon = True
|
|
||||||
client_thread.start()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Server error: {e}")
|
|
||||||
finally:
|
|
||||||
server_socket.close()
|
|
||||||
|
|
||||||
def handle_client(self, client_socket):
|
|
||||||
try:
|
|
||||||
while self.server_running:
|
|
||||||
data = b""
|
|
||||||
while True:
|
|
||||||
chunk = client_socket.recv(4096)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
data += chunk
|
|
||||||
try:
|
|
||||||
# Try to see if we've received a complete JSON object
|
|
||||||
json_data = json.loads(data.decode('utf-8'))
|
|
||||||
break # If successful, break out of the inner loop
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# If it's not a complete JSON object yet, keep reading
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
request = json_data
|
|
||||||
if request['action'] == 'get_macros':
|
|
||||||
# Send all macros to client
|
|
||||||
client_socket.send(json.dumps(self.macros).encode('utf-8'))
|
|
||||||
elif request['action'] == 'execute':
|
|
||||||
# Execute the specified macro
|
|
||||||
success = self.execute_macro(request['macro_id'])
|
|
||||||
response = {'success': success}
|
|
||||||
client_socket.send(json.dumps(response).encode('utf-8'))
|
|
||||||
elif request['action'] == 'get_image':
|
|
||||||
image_path = request['image_path']
|
|
||||||
if os.path.exists(image_path):
|
|
||||||
try:
|
|
||||||
with open(image_path, 'rb') as f:
|
|
||||||
image_data =f.read()
|
|
||||||
image_size = len(image_data)
|
|
||||||
client_socket.send(str(image_size).encode('utf-8'))
|
|
||||||
client_socket.recv(1024) # Wait for acknowledgment
|
|
||||||
client_socket.sendall(image_data)
|
|
||||||
except FileNotFoundError:
|
|
||||||
client_socket.send(b"ERROR: Image not found")
|
|
||||||
except Exception as e:
|
|
||||||
client_socket.send(f"ERROR: {str(e)}".encode('utf-8'))
|
|
||||||
else:
|
|
||||||
client_socket.send(json.dumps({'error': 'Unknown action'}).encode('utf-8'))
|
|
||||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
||||||
error_msg = {'error': f'Invalid request: {str(e)}'}
|
|
||||||
client_socket.send(json.dumps(error_msg).encode('utf-8'))
|
|
||||||
print(f"JSON processing error: {e}, Data received: {data[:100]}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Client handling error: {e}")
|
|
||||||
finally:
|
|
||||||
client_socket.close()
|
|
||||||
self.status_var.set("Client disconnected. Waiting for connections...")
|
|
||||||
|
|
||||||
def on_closing(self):
|
|
||||||
# When the window is closed, minimize to tray instead of closing
|
|
||||||
self.minimize_to_tray()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
root = tk.Tk()
|
|
||||||
app = MacroPadServer(root)
|
|
||||||
root.mainloop()
|
|
||||||
|
|
||||||
|
|
@ -4,4 +4,5 @@ pystray
|
|||||||
flask
|
flask
|
||||||
waitress
|
waitress
|
||||||
netifaces
|
netifaces
|
||||||
qrcode
|
qrcode
|
||||||
|
tkinter
|
283
ui_components.py
Normal file
283
ui_components.py
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
# UI components and dialogs for the desktop application
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, filedialog, messagebox
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from config import THEME
|
||||||
|
|
||||||
|
|
||||||
|
class MacroDialog:
|
||||||
|
"""Dialog for adding/editing macros"""
|
||||||
|
|
||||||
|
def __init__(self, parent, macro_manager, macro_id=None):
|
||||||
|
self.parent = parent
|
||||||
|
self.macro_manager = macro_manager
|
||||||
|
self.macro_id = macro_id
|
||||||
|
self.dialog = None
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Show the dialog and return the result"""
|
||||||
|
self.dialog = tk.Toplevel(self.parent)
|
||||||
|
self.dialog.title("Edit Macro" if self.macro_id else "Add Macro")
|
||||||
|
self.dialog.geometry("450x400")
|
||||||
|
self.dialog.transient(self.parent)
|
||||||
|
self.dialog.configure(bg=THEME['bg_color'])
|
||||||
|
self.dialog.grab_set()
|
||||||
|
|
||||||
|
# If editing, get existing macro data
|
||||||
|
if self.macro_id:
|
||||||
|
macro = self.macro_manager.macros.get(self.macro_id, {})
|
||||||
|
else:
|
||||||
|
macro = {}
|
||||||
|
|
||||||
|
self._create_widgets(macro)
|
||||||
|
|
||||||
|
# Wait for dialog to close
|
||||||
|
self.dialog.wait_window()
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
def _create_widgets(self, macro):
|
||||||
|
"""Create the dialog widgets"""
|
||||||
|
# Name
|
||||||
|
tk.Label(self.dialog, text="Macro Name:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
self.name_entry = tk.Entry(self.dialog, width=30, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
|
||||||
|
self.name_entry.grid(row=0, column=1, padx=5, pady=5)
|
||||||
|
self.name_entry.insert(0, macro.get("name", ""))
|
||||||
|
|
||||||
|
# Category
|
||||||
|
tk.Label(self.dialog, text="Category:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
self.category_entry = tk.Entry(self.dialog, width=30, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
|
||||||
|
self.category_entry.grid(row=1, column=1, padx=5, pady=5)
|
||||||
|
self.category_entry.insert(0, macro.get("category", ""))
|
||||||
|
|
||||||
|
# Type
|
||||||
|
tk.Label(self.dialog, text="Type:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=2, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
self.type_var = tk.StringVar(value=macro.get("type", "text"))
|
||||||
|
|
||||||
|
radio_style = {'bg': THEME['bg_color'], 'fg': THEME['fg_color'], 'selectcolor': THEME['accent_color']}
|
||||||
|
tk.Radiobutton(self.dialog, text="Text", variable=self.type_var, value="text", **radio_style).grid(row=2, column=1, sticky="w")
|
||||||
|
tk.Radiobutton(self.dialog, text="Application", variable=self.type_var, value="app", **radio_style).grid(row=3, column=1, sticky="w")
|
||||||
|
|
||||||
|
# Command/Text
|
||||||
|
tk.Label(self.dialog, text="Command/Text:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=4, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
self.command_text = tk.Text(self.dialog, width=30, height=5, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
|
||||||
|
self.command_text.grid(row=4, column=1, padx=5, pady=5)
|
||||||
|
self.command_text.insert("1.0", macro.get("command", ""))
|
||||||
|
|
||||||
|
# Modifiers
|
||||||
|
self._create_modifiers(macro)
|
||||||
|
|
||||||
|
# Image
|
||||||
|
self.image_path = tk.StringVar()
|
||||||
|
tk.Label(self.dialog, text="Image:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=6, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
image_entry = tk.Entry(self.dialog, textvariable=self.image_path, width=30, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
|
||||||
|
image_entry.grid(row=6, column=1, padx=5, pady=5)
|
||||||
|
|
||||||
|
button_style = {'bg': THEME['button_bg'], 'fg': THEME['button_fg'], 'activebackground': THEME['accent_color'],
|
||||||
|
'activeforeground': THEME['button_fg'], 'bd': 0, 'relief': tk.FLAT}
|
||||||
|
|
||||||
|
tk.Button(self.dialog, text="Browse...", command=self._browse_image, **button_style).grid(row=6, column=2)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
tk.Button(self.dialog, text="Save", command=self._save, **button_style).grid(row=7, column=0, padx=5, pady=20)
|
||||||
|
tk.Button(self.dialog, text="Cancel", command=self._cancel, **button_style).grid(row=7, column=1, padx=5, pady=20)
|
||||||
|
|
||||||
|
def _create_modifiers(self, macro):
|
||||||
|
"""Create modifier checkboxes"""
|
||||||
|
mod_frame = tk.Frame(self.dialog, bg=THEME['bg_color'])
|
||||||
|
mod_frame.grid(row=5, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
||||||
|
|
||||||
|
tk.Label(mod_frame, text="Key Modifiers:", bg=THEME['bg_color'], fg=THEME['fg_color']).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
modifiers = macro.get("modifiers", {})
|
||||||
|
self.ctrl_var = tk.BooleanVar(value=modifiers.get("ctrl", False))
|
||||||
|
self.alt_var = tk.BooleanVar(value=modifiers.get("alt", False))
|
||||||
|
self.shift_var = tk.BooleanVar(value=modifiers.get("shift", False))
|
||||||
|
self.enter_var = tk.BooleanVar(value=modifiers.get("enter", False))
|
||||||
|
|
||||||
|
checkbox_style = {'bg': THEME['bg_color'], 'fg': THEME['fg_color'], 'selectcolor': THEME['accent_color'],
|
||||||
|
'activebackground': THEME['bg_color'], 'activeforeground': THEME['fg_color']}
|
||||||
|
|
||||||
|
tk.Checkbutton(mod_frame, text="Ctrl", variable=self.ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
||||||
|
tk.Checkbutton(mod_frame, text="Alt", variable=self.alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
||||||
|
tk.Checkbutton(mod_frame, text="Shift", variable=self.shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
||||||
|
tk.Checkbutton(mod_frame, text="Add Enter", variable=self.enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
def _browse_image(self):
|
||||||
|
"""Browse for image file"""
|
||||||
|
filename = filedialog.askopenfilename(
|
||||||
|
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])
|
||||||
|
if filename:
|
||||||
|
self.image_path.set(filename)
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
"""Save the macro"""
|
||||||
|
name = self.name_entry.get().strip()
|
||||||
|
if not name:
|
||||||
|
messagebox.showerror("Error", "Macro name is required")
|
||||||
|
return
|
||||||
|
|
||||||
|
macro_type = self.type_var.get()
|
||||||
|
command = self.command_text.get("1.0", tk.END).strip()
|
||||||
|
category = self.category_entry.get().strip()
|
||||||
|
|
||||||
|
modifiers = {
|
||||||
|
"ctrl": self.ctrl_var.get(),
|
||||||
|
"alt": self.alt_var.get(),
|
||||||
|
"shift": self.shift_var.get(),
|
||||||
|
"enter": self.enter_var.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.macro_id:
|
||||||
|
# Update existing macro
|
||||||
|
success = self.macro_manager.update_macro(
|
||||||
|
self.macro_id, name, macro_type, command, category, modifiers, self.image_path.get())
|
||||||
|
else:
|
||||||
|
# Add new macro
|
||||||
|
self.macro_id = self.macro_manager.add_macro(
|
||||||
|
name, macro_type, command, category, modifiers, self.image_path.get())
|
||||||
|
success = bool(self.macro_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.result = self.macro_id
|
||||||
|
self.dialog.destroy()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("Error", "Failed to save macro")
|
||||||
|
|
||||||
|
def _cancel(self):
|
||||||
|
"""Cancel dialog"""
|
||||||
|
self.result = None
|
||||||
|
self.dialog.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class MacroSelector:
|
||||||
|
"""Dialog for selecting a macro from a list"""
|
||||||
|
|
||||||
|
def __init__(self, parent, macro_manager, title="Select Macro"):
|
||||||
|
self.parent = parent
|
||||||
|
self.macro_manager = macro_manager
|
||||||
|
self.title = title
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Show the selection dialog"""
|
||||||
|
if not self.macro_manager.macros:
|
||||||
|
messagebox.showinfo("No Macros", "There are no macros available.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
dialog = tk.Toplevel(self.parent)
|
||||||
|
dialog.title(self.title)
|
||||||
|
dialog.geometry("200x340")
|
||||||
|
dialog.transient(self.parent)
|
||||||
|
dialog.configure(bg=THEME['bg_color'])
|
||||||
|
dialog.grab_set()
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
tk.Label(dialog, text=f"{self.title}:", bg=THEME['bg_color'], fg=THEME['fg_color']).pack(pady=5)
|
||||||
|
|
||||||
|
# Listbox
|
||||||
|
listbox = tk.Listbox(dialog, bg=THEME['highlight_color'], fg=THEME['fg_color'], selectbackground=THEME['accent_color'])
|
||||||
|
listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||||
|
|
||||||
|
# Populate listbox
|
||||||
|
macro_ids = []
|
||||||
|
for macro_id, macro in self.macro_manager.macros.items():
|
||||||
|
listbox.insert(tk.END, macro["name"])
|
||||||
|
macro_ids.append(macro_id)
|
||||||
|
|
||||||
|
def on_select():
|
||||||
|
if not listbox.curselection():
|
||||||
|
messagebox.showwarning("No Selection", f"Please select a macro.")
|
||||||
|
return
|
||||||
|
idx = listbox.curselection()[0]
|
||||||
|
self.result = macro_ids[idx]
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
button_style = {'bg': THEME['button_bg'], 'fg': THEME['button_fg'], 'activebackground': THEME['accent_color'],
|
||||||
|
'activeforeground': THEME['button_fg'], 'bd': 0, 'relief': tk.FLAT}
|
||||||
|
|
||||||
|
tk.Button(dialog, text="Select", command=on_select, **button_style).pack(pady=10)
|
||||||
|
tk.Button(dialog, text="Cancel", command=lambda: dialog.destroy(), **button_style).pack(pady=5)
|
||||||
|
|
||||||
|
dialog.wait_window()
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
class TabManager:
|
||||||
|
"""Dialog for managing macro categories/tabs"""
|
||||||
|
|
||||||
|
def __init__(self, parent, macro_manager):
|
||||||
|
self.parent = parent
|
||||||
|
self.macro_manager = macro_manager
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Show tab management dialog"""
|
||||||
|
dialog = tk.Toplevel(self.parent)
|
||||||
|
dialog.title("Manage Tabs")
|
||||||
|
dialog.geometry("450x400") # Increased width and height
|
||||||
|
dialog.transient(self.parent)
|
||||||
|
dialog.configure(bg=THEME['bg_color'])
|
||||||
|
dialog.grab_set()
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
tk.Label(dialog, text="Assign categories to macros for better organization:",
|
||||||
|
bg=THEME['bg_color'], fg=THEME['fg_color']).pack(pady=10)
|
||||||
|
|
||||||
|
# Create scrollable frame
|
||||||
|
list_frame = tk.Frame(dialog, bg=THEME['bg_color'])
|
||||||
|
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||||
|
|
||||||
|
canvas = tk.Canvas(list_frame, bg=THEME['bg_color'], highlightthickness=0)
|
||||||
|
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
|
||||||
|
scrollable_frame = tk.Frame(canvas, bg=THEME['bg_color'])
|
||||||
|
|
||||||
|
scrollable_frame.bind(
|
||||||
|
"<Configure>",
|
||||||
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
||||||
|
)
|
||||||
|
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||||
|
canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
canvas.pack(side="left", fill="both", expand=True)
|
||||||
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
# Category entries for each macro
|
||||||
|
category_vars = {}
|
||||||
|
for macro_id, macro in self.macro_manager.macros.items():
|
||||||
|
frame = tk.Frame(scrollable_frame, bg=THEME['bg_color'])
|
||||||
|
frame.pack(fill="x", pady=2, padx=5)
|
||||||
|
|
||||||
|
tk.Label(frame, text=macro["name"], bg=THEME['bg_color'], fg=THEME['fg_color'],
|
||||||
|
width=20, anchor="w").pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
category_var = tk.StringVar(value=macro.get("category", ""))
|
||||||
|
category_vars[macro_id] = category_var
|
||||||
|
entry = tk.Entry(frame, textvariable=category_var, width=15,
|
||||||
|
bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
|
||||||
|
entry.pack(side=tk.RIGHT, padx=(5, 0))
|
||||||
|
|
||||||
|
# Buttons - use a fixed frame at bottom
|
||||||
|
button_frame = tk.Frame(dialog, bg=THEME['bg_color'])
|
||||||
|
button_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
def save_categories():
|
||||||
|
for macro_id, category_var in category_vars.items():
|
||||||
|
category = category_var.get().strip()
|
||||||
|
if category:
|
||||||
|
self.macro_manager.macros[macro_id]["category"] = category
|
||||||
|
else:
|
||||||
|
self.macro_manager.macros[macro_id].pop("category", None)
|
||||||
|
|
||||||
|
self.macro_manager.save_macros()
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
button_style = {'bg': THEME['button_bg'], 'fg': THEME['button_fg'], 'activebackground': THEME['accent_color'],
|
||||||
|
'activeforeground': THEME['button_fg'], 'bd': 0, 'relief': tk.FLAT}
|
||||||
|
|
||||||
|
tk.Button(button_frame, text="Save", command=save_categories, **button_style).pack(side=tk.LEFT, padx=5)
|
||||||
|
tk.Button(button_frame, text="Cancel", command=lambda: dialog.destroy(), **button_style).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
dialog.wait_window()
|
@ -1 +1 @@
|
|||||||
0.5.4
|
0.8.0
|
99
web_server.py
Normal file
99
web_server.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Web server component for MacroPad
|
||||||
|
|
||||||
|
from flask import Flask, render_template_string, request, jsonify, send_file
|
||||||
|
from waitress import serve
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from web_templates import INDEX_HTML
|
||||||
|
|
||||||
|
|
||||||
|
class WebServer:
|
||||||
|
def __init__(self, macro_manager, app_dir, port=40000):
|
||||||
|
self.macro_manager = macro_manager
|
||||||
|
self.app_dir = app_dir
|
||||||
|
self.port = port
|
||||||
|
self.app = None
|
||||||
|
|
||||||
|
def create_app(self):
|
||||||
|
"""Create and configure Flask application"""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Disable Flask's logging except for errors
|
||||||
|
log = logging.getLogger('werkzeug')
|
||||||
|
log.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template_string(INDEX_HTML)
|
||||||
|
|
||||||
|
@app.route('/api/tabs')
|
||||||
|
def get_tabs():
|
||||||
|
"""Get all available tabs (similar to setup_tabs logic)"""
|
||||||
|
tabs = ["All"]
|
||||||
|
|
||||||
|
# Add tabs based on macro types and custom categories
|
||||||
|
unique_types = set()
|
||||||
|
for macro in self.macro_manager.macros.values():
|
||||||
|
if macro.get("type"):
|
||||||
|
unique_types.add(macro["type"].title())
|
||||||
|
# Check for custom category
|
||||||
|
if macro.get("category"):
|
||||||
|
unique_types.add(macro["category"])
|
||||||
|
|
||||||
|
for tab_type in sorted(unique_types):
|
||||||
|
if tab_type not in ["All"]:
|
||||||
|
tabs.append(tab_type)
|
||||||
|
|
||||||
|
return jsonify(tabs)
|
||||||
|
|
||||||
|
@app.route('/api/macros')
|
||||||
|
def get_macros():
|
||||||
|
return jsonify(self.macro_manager.macros)
|
||||||
|
|
||||||
|
@app.route('/api/macros/<tab_name>')
|
||||||
|
def get_macros_by_tab(tab_name):
|
||||||
|
"""Filter macros by tab (similar to filter_macros_by_tab logic)"""
|
||||||
|
if tab_name == "All":
|
||||||
|
return jsonify(self.macro_manager.macros)
|
||||||
|
|
||||||
|
filtered_macros = {}
|
||||||
|
for macro_id, macro in self.macro_manager.macros.items():
|
||||||
|
# Check type match
|
||||||
|
if macro.get("type", "").title() == tab_name:
|
||||||
|
filtered_macros[macro_id] = macro
|
||||||
|
# Check custom category match
|
||||||
|
elif macro.get("category") == tab_name:
|
||||||
|
filtered_macros[macro_id] = macro
|
||||||
|
|
||||||
|
return jsonify(filtered_macros)
|
||||||
|
|
||||||
|
@app.route('/api/image/<path:image_path>')
|
||||||
|
def get_image(image_path):
|
||||||
|
try:
|
||||||
|
image_path = os.path.join(self.app_dir, image_path)
|
||||||
|
return send_file(image_path)
|
||||||
|
except Exception as e:
|
||||||
|
return str(e), 404
|
||||||
|
|
||||||
|
@app.route('/api/execute', methods=['POST'])
|
||||||
|
def execute_macro():
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'macro_id' not in data:
|
||||||
|
return jsonify({"success": False, "error": "Invalid request"})
|
||||||
|
|
||||||
|
macro_id = data['macro_id']
|
||||||
|
success = self.macro_manager.execute_macro(macro_id)
|
||||||
|
return jsonify({"success": success})
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
return app
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the web server"""
|
||||||
|
if not self.app:
|
||||||
|
self.create_app()
|
||||||
|
|
||||||
|
try:
|
||||||
|
serve(self.app, host='0.0.0.0', port=self.port, threads=4)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
292
web_templates.py
Normal file
292
web_templates.py
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
# HTML templates for the web interface
|
||||||
|
|
||||||
|
INDEX_HTML = '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MacroPad Web Interface</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #007acc;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
background-color: #505050;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.refresh-button:hover {
|
||||||
|
background-color: #007acc;
|
||||||
|
}
|
||||||
|
.refresh-button svg {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.tab-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #404040;
|
||||||
|
}
|
||||||
|
.tab-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.tab-button {
|
||||||
|
background-color: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.tab-button:hover {
|
||||||
|
background-color: #505050;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
background-color: #007acc;
|
||||||
|
border-bottom-color: #007acc;
|
||||||
|
}
|
||||||
|
.macro-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.macro-button {
|
||||||
|
background-color: #505050;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
.macro-button:hover, .macro-button:active {
|
||||||
|
background-color: #007acc;
|
||||||
|
}
|
||||||
|
.macro-button img {
|
||||||
|
max-width: 64px;
|
||||||
|
max-height: 64px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.macro-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
}
|
||||||
|
.macro-button {
|
||||||
|
padding: 10px 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.tab-button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header-container">
|
||||||
|
<h1>MacroPad Web Interface</h1>
|
||||||
|
<button class="refresh-button" onclick="loadTabs()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||||
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||||
|
</svg>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="status success" id="success-status">Macro executed successfully!</div>
|
||||||
|
<div class="status error" id="error-status">Failed to execute macro</div>
|
||||||
|
<div class="tab-container">
|
||||||
|
<div class="tab-list" id="tab-list">
|
||||||
|
<!-- Tabs will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="macro-grid" id="macro-grid">
|
||||||
|
<!-- Macros will be loaded here -->
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let currentTab = 'All';
|
||||||
|
let allMacros = {};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadTabs();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadTabs() {
|
||||||
|
fetch('/api/tabs')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(tabs => {
|
||||||
|
const tabList = document.getElementById('tab-list');
|
||||||
|
tabList.innerHTML = '';
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'tab-button';
|
||||||
|
button.textContent = tab;
|
||||||
|
button.onclick = function() { switchTab(tab); };
|
||||||
|
|
||||||
|
if (tab === currentTab) {
|
||||||
|
button.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
tabList.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load macros for current tab
|
||||||
|
loadMacros(currentTab);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading tabs:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
currentTab = tabName;
|
||||||
|
|
||||||
|
// Update tab button states
|
||||||
|
const tabButtons = document.querySelectorAll('.tab-button');
|
||||||
|
tabButtons.forEach(button => {
|
||||||
|
button.classList.remove('active');
|
||||||
|
if (button.textContent === tabName) {
|
||||||
|
button.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load macros for selected tab
|
||||||
|
loadMacros(tabName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMacros(tab = 'All') {
|
||||||
|
const url = tab === 'All' ? '/api/macros' : `/api/macros/${encodeURIComponent(tab)}`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(macros => {
|
||||||
|
allMacros = macros;
|
||||||
|
displayMacros(macros);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading macros:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayMacros(macros) {
|
||||||
|
const macroGrid = document.getElementById('macro-grid');
|
||||||
|
macroGrid.innerHTML = '';
|
||||||
|
|
||||||
|
if (Object.keys(macros).length === 0) {
|
||||||
|
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros in this category.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [macroId, macro] of Object.entries(macros)) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'macro-button';
|
||||||
|
button.onclick = function() { executeMacro(macroId); };
|
||||||
|
|
||||||
|
// Add image if available
|
||||||
|
if (macro.image_path) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
|
||||||
|
img.alt = macro.name;
|
||||||
|
img.onerror = function() {
|
||||||
|
this.style.display = 'none';
|
||||||
|
};
|
||||||
|
button.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = document.createTextNode(macro.name);
|
||||||
|
button.appendChild(text);
|
||||||
|
macroGrid.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeMacro(macroId) {
|
||||||
|
fetch('/api/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ macro_id: macroId })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const successStatus = document.getElementById('success-status');
|
||||||
|
const errorStatus = document.getElementById('error-status');
|
||||||
|
if (data.success) {
|
||||||
|
successStatus.style.display = 'block';
|
||||||
|
errorStatus.style.display = 'none';
|
||||||
|
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
|
||||||
|
} else {
|
||||||
|
errorStatus.style.display = 'block';
|
||||||
|
successStatus.style.display = 'none';
|
||||||
|
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error executing macro:', error);
|
||||||
|
const errorStatus = document.getElementById('error-status');
|
||||||
|
errorStatus.style.display = 'block';
|
||||||
|
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
Loading…
x
Reference in New Issue
Block a user