diff --git a/.gitignore b/.gitignore index de08437..289ace3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ macros.json macro_images/ macro_images/* -build/ \ No newline at end of file +build/ +.venv +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md index be5b6d6..94cf36d 100644 --- a/README.md +++ b/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. @@ -8,21 +8,27 @@ A versatile MacroPad server application that lets you create, manage, and execut - **Application Macros**: Launch applications or scripts directly - **Key Modifiers**: Add Ctrl, Alt, Shift modifiers and Enter keypress to your text macros - **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 -- **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 +- **Dark Theme**: Modern dark interface for comfortable use +- **Modular Architecture**: Clean separation of concerns with dedicated modules ## Requirements - Python 3.11+ -- Required Python packages: +- Required Python packages (install via requirements.txt): - tkinter - flask - pyautogui - pystray - Pillow (PIL) - - flask_cors - waitress + - netifaces + - qrcode ## Installation @@ -30,17 +36,13 @@ A versatile MacroPad server application that lets you create, manage, and execut 2. Install the required dependencies: ```bash pip install -r requirements.txt - ``` -3. Run the application: - ```bash - python mp-server-v2.py - ``` + ``` ## Alternative Installation Method #### Windows only 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 > [!IMPORTANT] @@ -50,19 +52,21 @@ A versatile MacroPad server application that lets you create, manage, and execut ### 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 - **Edit Macro**: Modify existing macros - **Delete Macro**: Remove unwanted macros -- **Minimize to Tray**: Hide the application to your system tray -- **Exit**: Close the application completely +- **Sort Options**: Sort the Macros by type, name, and recent usage +- **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 1. Click the "Add Macro" button 2. Fill in the details: - **Name**: A descriptive name for your macro + - **Category**: Assign a category to associate with a tab - **Type**: Choose between Text or Application - **Command/Text**: The text to insert or application command to run - **Modifiers**: Select any combination of Ctrl, Alt, Shift, and Enter diff --git a/config.py b/config.py new file mode 100644 index 0000000..aa32b4f --- /dev/null +++ b/config.py @@ -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"] \ No newline at end of file diff --git a/dist/mp-server-v2.exe b/dist/macropad.exe similarity index 77% rename from dist/mp-server-v2.exe rename to dist/macropad.exe index 363ef1d..20db5ca 100644 Binary files a/dist/mp-server-v2.exe and b/dist/macropad.exe differ diff --git a/dist/mp-server.exe b/dist/mp-server.exe deleted file mode 100644 index 1252e73..0000000 Binary files a/dist/mp-server.exe and /dev/null differ diff --git a/macro_manager.py b/macro_manager.py new file mode 100644 index 0000000..038cf03 --- /dev/null +++ b/macro_manager.py @@ -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 \ No newline at end of file diff --git a/mp-server.spec b/macropad.spec similarity index 88% rename from mp-server.spec rename to macropad.spec index a23f783..ac5f158 100644 --- a/mp-server.spec +++ b/macropad.spec @@ -2,7 +2,7 @@ a = Analysis( - ['mp-server.py'], + ['main.py'], pathex=[], binaries=[], datas=[], @@ -22,7 +22,7 @@ exe = EXE( a.binaries, a.datas, [], - name='mp-server', + name='macropad', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/main.py b/main.py new file mode 100644 index 0000000..bee99a2 --- /dev/null +++ b/main.py @@ -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('', 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("<>", 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("<>", 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( + "", + 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() \ No newline at end of file diff --git a/mp-server-v2.spec b/main.spec similarity index 87% rename from mp-server-v2.spec rename to main.spec index ffcefa3..1ccbe3c 100644 --- a/mp-server-v2.spec +++ b/main.spec @@ -2,7 +2,7 @@ a = Analysis( - ['mp-server-v2.py'], + ['main.py'], pathex=[], binaries=[], datas=[], @@ -22,7 +22,7 @@ exe = EXE( a.binaries, a.datas, [], - name='mp-server-v2', + name='main', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/mp-server-v2.py b/mp-server-v2.py deleted file mode 100644 index bb9afdf..0000000 --- a/mp-server-v2.py +++ /dev/null @@ -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( - "", - 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 = ''' - - - - - -MacroPad Web Interface - - - -
-

MacroPad Web Interface

- -
-
Macro executed successfully!
-
Failed to execute macro
-
- -
- - - - ''' - - @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/') - 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() \ No newline at end of file diff --git a/mp-server.py b/mp-server.py deleted file mode 100644 index 73ad185..0000000 --- a/mp-server.py +++ /dev/null @@ -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("", 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( - "", - 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() - - diff --git a/requirements.txt b/requirements.txt index b5cbb46..6773527 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ pystray flask waitress netifaces -qrcode \ No newline at end of file +qrcode +tkinter \ No newline at end of file diff --git a/ui_components.py b/ui_components.py new file mode 100644 index 0000000..00d940d --- /dev/null +++ b/ui_components.py @@ -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( + "", + 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() \ No newline at end of file diff --git a/version.txt b/version.txt index 167b000..8adc70f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.4 \ No newline at end of file +0.8.0 \ No newline at end of file diff --git a/web_server.py b/web_server.py new file mode 100644 index 0000000..8403ab8 --- /dev/null +++ b/web_server.py @@ -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/') + 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/') + 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 \ No newline at end of file diff --git a/web_templates.py b/web_templates.py new file mode 100644 index 0000000..0ae168e --- /dev/null +++ b/web_templates.py @@ -0,0 +1,292 @@ +# HTML templates for the web interface + +INDEX_HTML = ''' + + + + + +MacroPad Web Interface + + + +
+

MacroPad Web Interface

+ +
+
Macro executed successfully!
+
Failed to execute macro
+
+
+ +
+
+
+ +
+ + + +''' \ No newline at end of file