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/config.py b/config.py index 21b6932..4753eae 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,6 @@ # Configuration and constants for MacroPad Server -VERSION = "0.5.4 Beta" +VERSION = "0.7.5 Beta" DEFAULT_PORT = 40000 # UI Theme colors diff --git a/dist/mp-server-v2.exe b/dist/main.exe similarity index 77% rename from dist/mp-server-v2.exe rename to dist/main.exe index 363ef1d..d3a403d 100644 Binary files a/dist/mp-server-v2.exe and b/dist/main.exe differ diff --git a/main.py b/main.py index ef58060..02c4450 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ import socket import qrcode import webbrowser import pystray -from PIL import Image, ImageTk +from PIL import Image, ImageTk, ImageDraw, ImageFont from config import VERSION, DEFAULT_PORT, THEME from macro_manager import MacroManager @@ -46,12 +46,20 @@ class MacroPadServer: # 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 close handler + # 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""" @@ -406,15 +414,53 @@ class MacroPadServer: 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""" - icon_image = Image.new("RGB", (64, 64), THEME['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() + 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 "M" in the center + text = "M" + 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""" @@ -425,14 +471,26 @@ class MacroPadServer: def exit_app(self, icon=None, item=None): """Exit the application""" + self.is_closing = True self.stop_server() - if hasattr(self, 'tray_icon'): - self.tray_icon.stop() - self.root.quit() + + 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""" - # For now just exit, but could minimize to tray + """Handle window close event - exit the application""" self.exit_app() 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-enhanced.py b/mp-server-v2-enhanced.py deleted file mode 100644 index 2f548fb..0000000 --- a/mp-server-v2-enhanced.py +++ /dev/null @@ -1,1183 +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 - - # 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() \ No newline at end of file diff --git a/mp-server-v2-new.py b/mp-server-v2-new.py deleted file mode 100644 index 68f1647..0000000 --- a/mp-server-v2-new.py +++ /dev/null @@ -1,1176 +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 - - # 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() - - # 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-v2.py b/mp-server-v2.py deleted file mode 100644 index f093cb1..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() diff --git a/version.txt b/version.txt index 167b000..da2ac9c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.4 \ No newline at end of file +0.7.5 \ No newline at end of file