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 # Tab management self.current_sort = "name" # Default sort: name, type, recent self.current_tab = "All" self.macro_tabs = {"All": "All Macros"} # Default tab # 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.tab_bg = "#404040" self.tab_selected = "#007acc" 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) # Configure notebook (tabs) style style.configure("TNotebook", background=self.bg_color, borderwidth=0) style.configure("TNotebook.Tab", background=self.tab_bg, foreground=self.fg_color, padding=[12, 8], borderwidth=0) style.map("TNotebook.Tab", background=[("selected", self.tab_selected)], foreground=[("selected", 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) # Sort controls sort_frame = ttk.Frame(left_frame) 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 buttons ttk.Button(sort_frame, text="Manage Tabs", command=self.manage_tabs).pack(side=tk.RIGHT, padx=(5, 0)) # 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) # 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)) 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) # Initialize tabs and display macros self.setup_tabs() self.display_macros() def setup_tabs(self): """Initialize tabs based on macro categories""" # Clear existing tabs for tab in self.notebook.tabs(): self.notebook.forget(tab) # Always have an "All" tab all_frame = ttk.Frame(self.notebook) self.notebook.add(all_frame, text="All") # Add tabs based on macro types and custom categories unique_types = set() for macro in self.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"]: frame = ttk.Frame(self.notebook) self.notebook.add(frame, text=tab_type) 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 get_sorted_macros(self): """Get macros sorted by current sort option""" macro_list = list(self.macros.items()) if self.current_sort == "name": macro_list.sort(key=lambda x: x[1]["name"].lower()) elif self.current_sort == "type": macro_list.sort(key=lambda x: (x[1].get("type", ""), x[1]["name"].lower())) elif self.current_sort == "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): """Filter macros based on current tab""" if self.current_tab == "All": return macro_list filtered = [] for macro_id, macro in macro_list: # Check type match if macro.get("type", "").title() == self.current_tab: filtered.append((macro_id, macro)) # Check custom category match elif macro.get("category") == self.current_tab: filtered.append((macro_id, macro)) return filtered def manage_tabs(self): """Open dialog to manage custom tabs/categories""" dialog = tk.Toplevel(self.root) dialog.title("Manage Tabs") dialog.geometry("400x300") dialog.transient(self.root) dialog.configure(bg=self.bg_color) # Instructions tk.Label(dialog, text="Assign categories to macros for better organization:", bg=self.bg_color, fg=self.fg_color).pack(pady=10) # Create frame for macro list list_frame = ttk.Frame(dialog) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # Scrollable frame for macros canvas = tk.Canvas(list_frame, bg=self.bg_color, highlightthickness=0) scrollbar = ttk.Scrollbar(list_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") # Category entries for each macro category_vars = {} row = 0 for macro_id, macro in self.macros.items(): frame = ttk.Frame(scrollable_frame) frame.pack(fill="x", pady=2, padx=5) tk.Label(frame, text=macro["name"], bg=self.bg_color, fg=self.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=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) entry.pack(side=tk.RIGHT, padx=(5, 0)) row += 1 # Buttons button_frame = ttk.Frame(dialog) button_frame.pack(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.macros[macro_id]["category"] = category else: self.macros[macro_id].pop("category", None) self.save_macros() self.setup_tabs() 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(button_frame, text="Save", command=save_categories, **button_style).pack(side=tk.LEFT, padx=5) tk.Button(button_frame, text="Cancel", command=dialog.destroy, **button_style).pack(side=tk.LEFT, padx=5) 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): # Get current tab frame try: current_tab_id = self.notebook.select() current_frame = self.notebook.nametowidget(current_tab_id) except: return # Clear the previous content for widget in current_frame.winfo_children(): widget.destroy() # Create scrollable canvas for macros canvas = tk.Canvas(current_frame, bg=self.bg_color, highlightthickness=0) scrollbar = ttk.Scrollbar(current_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") # Get sorted and filtered macros sorted_macros = self.get_sorted_macros() filtered_macros = self.filter_macros_by_tab(sorted_macros) # Display macros for macro_id, macro in filtered_macros: 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) # Display message if no macros if not filtered_macros: label = tk.Label(scrollable_frame, text="No macros in this category", 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("450x400") # Increased height for category field 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="Category:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w") category_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) category_entry.grid(row=1, column=1, padx=5, pady=5) tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=2, 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=2, column=1, sticky="w") tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=3, column=1, sticky="w") tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=4, 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=4, column=1, padx=5, pady=5) # Modifiers frame mod_frame = tk.Frame(dialog, bg=self.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=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=6, 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=6, 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=6, 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() category = category_entry.get().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 and category macro_data = { "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() }, "last_used": 0 } if category: macro_data["category"] = category self.macros[macro_id] = macro_data self.save_macros() self.setup_tabs() self.display_macros() dialog.destroy() tk.Button(dialog, text="Save", command=save_macro, **button_style).grid(row=7, column=0, padx=5, pady=20) tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=7, 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("450x400") # Increased height for category field 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="Category:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w") category_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) category_entry.grid(row=1, column=1, padx=5, pady=5) category_entry.insert(0, macro.get("category", "")) tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=2, 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=2, column=1, sticky="w") tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=3, column=1, sticky="w") tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=4, 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=4, 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=5, 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=6, 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=6, 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=6, 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() category = category_entry.get().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 and category updated_macro = { "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() }, "last_used": macro.get("last_used", 0) } if category: updated_macro["category"] = category self.macros[macro_id] = updated_macro self.save_macros() self.setup_tabs() self.display_macros() dialog.destroy() tk.Button(dialog, text="Save", command=save_edited_macro, **button_style).grid(row=7, column=0, padx=5, pady=20) tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=7, 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.setup_tabs() 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] # 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') 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()