Add support for Tabs and other enhancements #1
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,3 +2,5 @@ macros.json
 | 
			
		||||
macro_images/
 | 
			
		||||
macro_images/*
 | 
			
		||||
build/
 | 
			
		||||
.venv
 | 
			
		||||
__pycache__
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								dist/mp-server-v2.exe → dist/main.exe
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/mp-server-v2.exe → dist/main.exe
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										70
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								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
 | 
			
		||||
@@ -47,11 +47,19 @@ class MacroPadServer:
 | 
			
		||||
        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('<Unmap>', 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"""
 | 
			
		||||
        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)
 | 
			
		||||
        self.tray_icon.run_detached()
 | 
			
		||||
            
 | 
			
		||||
            # 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'):
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1176
									
								
								mp-server-v2-new.py
									
									
									
									
									
								
							
							
						
						
									
										1176
									
								
								mp-server-v2-new.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										971
									
								
								mp-server-v2.py
									
									
									
									
									
								
							
							
						
						
									
										971
									
								
								mp-server-v2.py
									
									
									
									
									
								
							@@ -1,971 +0,0 @@
 | 
			
		||||
import tkinter as tk
 | 
			
		||||
from tkinter import ttk, filedialog, messagebox
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import uuid
 | 
			
		||||
import pyautogui
 | 
			
		||||
import subprocess
 | 
			
		||||
import threading
 | 
			
		||||
import pystray
 | 
			
		||||
from PIL import Image, ImageTk
 | 
			
		||||
from flask import Flask, render_template_string, request, jsonify, send_file
 | 
			
		||||
import webbrowser
 | 
			
		||||
from waitress import serve
 | 
			
		||||
import logging
 | 
			
		||||
import socket
 | 
			
		||||
import qrcode
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
class MacroPadServer:
 | 
			
		||||
    def __init__(self, root):
 | 
			
		||||
        self.root = root
 | 
			
		||||
        self.root.title("MacroPad Server")
 | 
			
		||||
        self.root.geometry("800x600")
 | 
			
		||||
        self.configure_styles()
 | 
			
		||||
        
 | 
			
		||||
        # Set Version Str
 | 
			
		||||
        self.version_str = "0.5.4 Beta"
 | 
			
		||||
 | 
			
		||||
        # Set up directories
 | 
			
		||||
        if getattr(sys, 'frozen', False):
 | 
			
		||||
            base_dir = os.path.dirname(sys.executable)
 | 
			
		||||
        else:
 | 
			
		||||
            base_dir = os.path.dirname(os.path.abspath(__file__))
 | 
			
		||||
        self.data_file = os.path.join(base_dir, "macros.json")
 | 
			
		||||
        self.images_dir = os.path.join(base_dir, "macro_images")
 | 
			
		||||
        self.app_dir = base_dir
 | 
			
		||||
        os.makedirs(self.images_dir, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
        self.macros = {}
 | 
			
		||||
        self.load_macros()
 | 
			
		||||
        self.image_cache = {}  # Cache for images
 | 
			
		||||
 | 
			
		||||
        # Set up server
 | 
			
		||||
        self.server_running = False
 | 
			
		||||
        self.web_app = None
 | 
			
		||||
        self.flask_thread = None
 | 
			
		||||
        self.server_port = 40000
 | 
			
		||||
 | 
			
		||||
        # Create UI
 | 
			
		||||
        self.create_ui()
 | 
			
		||||
 | 
			
		||||
        # Set up window close handler
 | 
			
		||||
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
 | 
			
		||||
 | 
			
		||||
    def configure_styles(self):
 | 
			
		||||
        # Dark theme
 | 
			
		||||
        self.bg_color = "#2e2e2e"
 | 
			
		||||
        self.fg_color = "#ffffff"
 | 
			
		||||
        self.highlight_color = "#3e3e3e"
 | 
			
		||||
        self.accent_color = "#007acc"
 | 
			
		||||
        self.button_bg = "#505050"
 | 
			
		||||
        self.button_fg = "#ffffff"
 | 
			
		||||
 | 
			
		||||
        self.root.configure(bg=self.bg_color)
 | 
			
		||||
 | 
			
		||||
        # Configure ttk styles
 | 
			
		||||
        style = ttk.Style()
 | 
			
		||||
        style.theme_use("clam")
 | 
			
		||||
        style.configure("TButton", background=self.button_bg, foreground=self.button_fg)
 | 
			
		||||
        style.map("TButton", background=[("active", self.accent_color)])
 | 
			
		||||
        style.configure("TFrame", background=self.bg_color)
 | 
			
		||||
        style.configure("TLabel", background=self.bg_color, foreground=self.fg_color)
 | 
			
		||||
 | 
			
		||||
    def create_ui(self):
 | 
			
		||||
        # Create main container
 | 
			
		||||
        main_frame = ttk.Frame(self.root)
 | 
			
		||||
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
 | 
			
		||||
 | 
			
		||||
        # Left side: Macro list and buttons
 | 
			
		||||
        left_frame = ttk.Frame(main_frame)
 | 
			
		||||
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
 | 
			
		||||
 | 
			
		||||
        # Macro list
 | 
			
		||||
        self.listbox_frame = ttk.Frame(left_frame)
 | 
			
		||||
        self.listbox_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
 | 
			
		||||
 | 
			
		||||
        # Right side: Details and server controls
 | 
			
		||||
        right_frame = ttk.Frame(main_frame)
 | 
			
		||||
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
 | 
			
		||||
 | 
			
		||||
        #Set version details
 | 
			
		||||
        version_label = tk.Label(self.root, text=self.version_str, 
 | 
			
		||||
                            bg=self.bg_color, fg=self.fg_color, 
 | 
			
		||||
                            font=('Helvetica', 8))  # Using smaller font for version text
 | 
			
		||||
        version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
 | 
			
		||||
 | 
			
		||||
        # Button container
 | 
			
		||||
        button_frame = ttk.Frame(left_frame)
 | 
			
		||||
        button_frame.pack(fill=tk.X, pady=(0, 10))
 | 
			
		||||
 | 
			
		||||
        button_style = {'style': 'TButton'}
 | 
			
		||||
 | 
			
		||||
        # Add buttons
 | 
			
		||||
        ttk.Button(button_frame, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=2)
 | 
			
		||||
        ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=2)
 | 
			
		||||
        ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=2)
 | 
			
		||||
 | 
			
		||||
        # Server controls
 | 
			
		||||
        server_frame = ttk.Frame(right_frame)
 | 
			
		||||
        server_frame.pack(fill=tk.X, pady=10)
 | 
			
		||||
 | 
			
		||||
        ttk.Label(server_frame, text="Web Server Control:").grid(row=0, column=0, sticky="w", pady=5)
 | 
			
		||||
 | 
			
		||||
        self.server_button = ttk.Button(server_frame, text="Start Web Server", command=self.toggle_server)
 | 
			
		||||
        self.server_button.grid(row=0, column=1, padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        # Status display
 | 
			
		||||
        self.status_var = tk.StringVar(value="Web server not running")
 | 
			
		||||
        ttk.Label(server_frame, textvariable=self.status_var).grid(row=1, column=0, columnspan=2, sticky="w", pady=5)
 | 
			
		||||
 | 
			
		||||
        # QR code placeholder
 | 
			
		||||
        self.qr_label = ttk.Label(right_frame)
 | 
			
		||||
        self.qr_label.pack(pady=10)
 | 
			
		||||
 | 
			
		||||
        # Server URL display
 | 
			
		||||
        self.url_var = tk.StringVar(value="")
 | 
			
		||||
        self.url_label = ttk.Label(right_frame, textvariable=self.url_var)
 | 
			
		||||
        self.url_label.pack(pady=5)
 | 
			
		||||
        
 | 
			
		||||
        # Open in browser button
 | 
			
		||||
        self.browser_button = ttk.Button(right_frame, text="Open in Browser", 
 | 
			
		||||
                                         command=self.open_in_browser, state=tk.DISABLED)
 | 
			
		||||
        self.browser_button.pack(pady=5)
 | 
			
		||||
 | 
			
		||||
        # Display any existing macros
 | 
			
		||||
        self.display_macros()
 | 
			
		||||
 | 
			
		||||
    def load_macros(self):
 | 
			
		||||
        try:
 | 
			
		||||
            if os.path.exists(self.data_file):
 | 
			
		||||
                with open(self.data_file, "r") as file:
 | 
			
		||||
                    self.macros = json.load(file)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error loading macros: {e}")
 | 
			
		||||
            self.macros = {}
 | 
			
		||||
 | 
			
		||||
    def save_macros(self):
 | 
			
		||||
        try:
 | 
			
		||||
            with open(self.data_file, "w") as file:
 | 
			
		||||
                json.dump(self.macros, file, indent=4)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error saving macros: {e}")
 | 
			
		||||
 | 
			
		||||
    def display_macros(self):
 | 
			
		||||
        # Clear the previous listbox
 | 
			
		||||
        for widget in self.listbox_frame.winfo_children():
 | 
			
		||||
            widget.destroy()
 | 
			
		||||
 | 
			
		||||
        # Create scrollable canvas for macros
 | 
			
		||||
        canvas = tk.Canvas(self.listbox_frame, bg=self.bg_color, highlightthickness=0)
 | 
			
		||||
        scrollbar = ttk.Scrollbar(self.listbox_frame, orient="vertical", command=canvas.yview)
 | 
			
		||||
        scrollable_frame = ttk.Frame(canvas)
 | 
			
		||||
 | 
			
		||||
        # Configure canvas and scrollable frame
 | 
			
		||||
        scrollable_frame.bind(
 | 
			
		||||
            "<Configure>",
 | 
			
		||||
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
 | 
			
		||||
        )
 | 
			
		||||
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
 | 
			
		||||
        canvas.configure(yscrollcommand=scrollbar.set)
 | 
			
		||||
 | 
			
		||||
        # Pack canvas and scrollbar
 | 
			
		||||
        canvas.pack(side="left", fill="both", expand=True)
 | 
			
		||||
        scrollbar.pack(side="right", fill="y")
 | 
			
		||||
 | 
			
		||||
        # Display macros
 | 
			
		||||
        row = 0
 | 
			
		||||
        for macro_id, macro in self.macros.items():
 | 
			
		||||
            frame = ttk.Frame(scrollable_frame)
 | 
			
		||||
            frame.pack(fill="x", pady=5, padx=5)
 | 
			
		||||
            
 | 
			
		||||
            # Create button with image if available
 | 
			
		||||
            button_text = macro["name"]
 | 
			
		||||
            button = tk.Button(
 | 
			
		||||
                frame, text=button_text, bg=self.button_bg, fg=self.button_fg,
 | 
			
		||||
                activebackground=self.accent_color, activeforeground=self.button_fg,
 | 
			
		||||
                relief=tk.RAISED, bd=2, pady=8, command=lambda mid=macro_id: self.execute_macro(mid)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Try to display image on button
 | 
			
		||||
            if "image_path" in macro and macro["image_path"]:
 | 
			
		||||
                try:
 | 
			
		||||
                    if macro["image_path"] in self.image_cache:
 | 
			
		||||
                        button_image = self.image_cache[macro["image_path"]]
 | 
			
		||||
                    else:
 | 
			
		||||
                        img_path = os.path.join(self.app_dir, macro["image_path"])
 | 
			
		||||
                        img = Image.open(img_path)
 | 
			
		||||
                        img = img.resize((32, 32))
 | 
			
		||||
                        button_image = ImageTk.PhotoImage(img)
 | 
			
		||||
                        self.image_cache[macro["image_path"]] = button_image
 | 
			
		||||
                    
 | 
			
		||||
                    button.config(image=button_image, compound=tk.LEFT)
 | 
			
		||||
                    button.image = button_image  # Keep a reference
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    print(f"Error loading image for {macro['name']}: {e}")
 | 
			
		||||
 | 
			
		||||
            button.pack(side=tk.LEFT, fill=tk.X, expand=True)
 | 
			
		||||
            row += 1
 | 
			
		||||
 | 
			
		||||
        # Display message if no macros
 | 
			
		||||
        if not self.macros:
 | 
			
		||||
            label = tk.Label(scrollable_frame, text="No macros defined",
 | 
			
		||||
                          bg=self.bg_color, fg=self.fg_color)
 | 
			
		||||
            label.pack(pady=20)
 | 
			
		||||
 | 
			
		||||
    def toggle_server(self):
 | 
			
		||||
        if self.server_running:
 | 
			
		||||
            self.stop_server()
 | 
			
		||||
        else:
 | 
			
		||||
            self.start_server()
 | 
			
		||||
 | 
			
		||||
    def start_server(self):
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.server_running:
 | 
			
		||||
                self.server_running = True
 | 
			
		||||
                self.flask_thread = threading.Thread(target=self.run_web_server)
 | 
			
		||||
                self.flask_thread.daemon = True
 | 
			
		||||
                self.flask_thread.start()
 | 
			
		||||
                self.server_button.config(text="Stop Web Server")
 | 
			
		||||
                
 | 
			
		||||
                # Get the systems internal IP address
 | 
			
		||||
                ip_address = self.get_ip_addresses()
 | 
			
		||||
                if ip_address:
 | 
			
		||||
                    # Set the URL display
 | 
			
		||||
                    url = f"http://{ip_address}:{self.server_port}"
 | 
			
		||||
                    url_text = "Web UI available at:\n" + "\n" + url
 | 
			
		||||
                    self.url_var.set(url_text)
 | 
			
		||||
                    
 | 
			
		||||
                    # Enable browser button
 | 
			
		||||
                    self.browser_button.config(state=tk.NORMAL)
 | 
			
		||||
                    
 | 
			
		||||
                    # Generate and display QR code for the first IP
 | 
			
		||||
                    self.generate_qr_code(url)
 | 
			
		||||
                else:
 | 
			
		||||
                    self.url_var.set("No network interfaces found")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            self.status_var.set(f"Error starting server: {e}")
 | 
			
		||||
            self.server_running = False
 | 
			
		||||
 | 
			
		||||
    def stop_server(self):
 | 
			
		||||
        if self.server_running:
 | 
			
		||||
            self.server_running = False
 | 
			
		||||
            self.status_var.set("Web server stopped")
 | 
			
		||||
            self.server_button.config(text="Start Web Server")
 | 
			
		||||
            self.url_var.set("")
 | 
			
		||||
            self.browser_button.config(state=tk.DISABLED)
 | 
			
		||||
            
 | 
			
		||||
            # Clear QR code
 | 
			
		||||
            self.qr_label.config(image="")
 | 
			
		||||
            
 | 
			
		||||
            # The Flask server will be stopped on the next request
 | 
			
		||||
 | 
			
		||||
    def get_ip_addresses(self):
 | 
			
		||||
        """Get the primary internal IPv4 address of the machine."""
 | 
			
		||||
        try:
 | 
			
		||||
            # Create a socket to connect to an external server
 | 
			
		||||
            # This helps determine which network interface is used for outbound connections
 | 
			
		||||
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 | 
			
		||||
            # We don't need to actually send data - just configure the socket
 | 
			
		||||
            s.connect(("8.8.8.8", 80))
 | 
			
		||||
            # Get the IP address that would be used for this connection
 | 
			
		||||
            ip = s.getsockname()[0]
 | 
			
		||||
            s.close()
 | 
			
		||||
            return ip
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error getting IP address: {e}")
 | 
			
		||||
            # Fallback method if the above doesn't work
 | 
			
		||||
            try:
 | 
			
		||||
                hostname = socket.gethostname()
 | 
			
		||||
                ip = socket.gethostbyname(hostname)
 | 
			
		||||
                # Don't return localhost address
 | 
			
		||||
                if ip.startswith("127."):
 | 
			
		||||
                    for addr_info in socket.getaddrinfo(hostname, None):
 | 
			
		||||
                        potential_ip = addr_info[4][0]
 | 
			
		||||
                        # If IPv4 and not localhost
 | 
			
		||||
                        if '.' in potential_ip and not potential_ip.startswith("127."):
 | 
			
		||||
                            return potential_ip
 | 
			
		||||
                else:
 | 
			
		||||
                    return ip
 | 
			
		||||
            except:
 | 
			
		||||
                return "127.0.0.1"  # Last resort fallback
 | 
			
		||||
 | 
			
		||||
    def generate_qr_code(self, url):
 | 
			
		||||
        try:
 | 
			
		||||
            # Try to import qrcode
 | 
			
		||||
            import qrcode
 | 
			
		||||
            
 | 
			
		||||
            # Generate QR code
 | 
			
		||||
            qr = qrcode.QRCode(
 | 
			
		||||
                version=1,
 | 
			
		||||
                error_correction=qrcode.constants.ERROR_CORRECT_L,
 | 
			
		||||
                box_size=10,
 | 
			
		||||
                border=4,
 | 
			
		||||
            )
 | 
			
		||||
            qr.add_data(url)
 | 
			
		||||
            qr.make(fit=True)
 | 
			
		||||
            
 | 
			
		||||
            # Create an image from the QR code
 | 
			
		||||
            qr_img = qr.make_image(fill_color="black", back_color="white")
 | 
			
		||||
            
 | 
			
		||||
            # Convert to PhotoImage for display
 | 
			
		||||
            qr_photoimg = ImageTk.PhotoImage(qr_img)
 | 
			
		||||
            
 | 
			
		||||
            # Update label
 | 
			
		||||
            self.qr_label.config(image=qr_photoimg)
 | 
			
		||||
            self.qr_label.image = qr_photoimg  # Keep a reference
 | 
			
		||||
        except ImportError:
 | 
			
		||||
            self.qr_label.config(text="QR code generation requires 'qrcode' package")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error generating QR code: {e}")
 | 
			
		||||
            self.qr_label.config(text="Error generating QR code")
 | 
			
		||||
 | 
			
		||||
    def open_in_browser(self):
 | 
			
		||||
        # Open the web interface in the default browser
 | 
			
		||||
        if self.server_running:
 | 
			
		||||
            webbrowser.open(f"http://localhost:{self.server_port}")
 | 
			
		||||
 | 
			
		||||
    def create_web_app(self):
 | 
			
		||||
        app = Flask(__name__)
 | 
			
		||||
        
 | 
			
		||||
        # Disable Flask's logging except for errors
 | 
			
		||||
        log = logging.getLogger('werkzeug')
 | 
			
		||||
        log.setLevel(logging.ERROR)
 | 
			
		||||
        
 | 
			
		||||
        # Define HTML templates
 | 
			
		||||
        index_html = '''
 | 
			
		||||
        <!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
<meta charset="UTF-8">
 | 
			
		||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
<title>MacroPad Web Interface</title>
 | 
			
		||||
<style>
 | 
			
		||||
body {
 | 
			
		||||
font-family: Arial, sans-serif;
 | 
			
		||||
margin: 0;
 | 
			
		||||
padding: 20px;
 | 
			
		||||
background-color: #2e2e2e;
 | 
			
		||||
color: #ffffff;
 | 
			
		||||
}
 | 
			
		||||
.header-container {
 | 
			
		||||
display: flex;
 | 
			
		||||
justify-content: space-between;
 | 
			
		||||
align-items: center;
 | 
			
		||||
margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
h1 {
 | 
			
		||||
color: #007acc;
 | 
			
		||||
margin: 0;
 | 
			
		||||
}
 | 
			
		||||
.refresh-button {
 | 
			
		||||
background-color: #505050;
 | 
			
		||||
color: white;
 | 
			
		||||
border: none;
 | 
			
		||||
border-radius: 8px;
 | 
			
		||||
padding: 10px 15px;
 | 
			
		||||
font-size: 16px;
 | 
			
		||||
cursor: pointer;
 | 
			
		||||
transition: background-color 0.3s;
 | 
			
		||||
display: flex;
 | 
			
		||||
align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.refresh-button:hover {
 | 
			
		||||
background-color: #007acc;
 | 
			
		||||
}
 | 
			
		||||
.refresh-button svg {
 | 
			
		||||
margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
.macro-grid {
 | 
			
		||||
display: grid;
 | 
			
		||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
 | 
			
		||||
gap: 15px;
 | 
			
		||||
margin-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
.macro-button {
 | 
			
		||||
background-color: #505050;
 | 
			
		||||
color: white;
 | 
			
		||||
border: none;
 | 
			
		||||
border-radius: 8px;
 | 
			
		||||
padding: 15px 10px;
 | 
			
		||||
text-align: center;
 | 
			
		||||
font-size: 16px;
 | 
			
		||||
cursor: pointer;
 | 
			
		||||
transition: background-color 0.3s;
 | 
			
		||||
display: flex;
 | 
			
		||||
flex-direction: column;
 | 
			
		||||
align-items: center;
 | 
			
		||||
justify-content: center;
 | 
			
		||||
min-height: 100px;
 | 
			
		||||
}
 | 
			
		||||
.macro-button:hover, .macro-button:active {
 | 
			
		||||
background-color: #007acc;
 | 
			
		||||
}
 | 
			
		||||
.macro-button img {
 | 
			
		||||
max-width: 64px;
 | 
			
		||||
max-height: 64px;
 | 
			
		||||
margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
.status {
 | 
			
		||||
text-align: center;
 | 
			
		||||
margin: 20px 0;
 | 
			
		||||
padding: 10px;
 | 
			
		||||
border-radius: 5px;
 | 
			
		||||
}
 | 
			
		||||
.success {
 | 
			
		||||
background-color: #4CAF50;
 | 
			
		||||
color: white;
 | 
			
		||||
display: none;
 | 
			
		||||
}
 | 
			
		||||
.error {
 | 
			
		||||
background-color: #f44336;
 | 
			
		||||
color: white;
 | 
			
		||||
display: none;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 600px) {
 | 
			
		||||
.macro-grid {
 | 
			
		||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
 | 
			
		||||
}
 | 
			
		||||
.macro-button {
 | 
			
		||||
padding: 10px 5px;
 | 
			
		||||
font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
h1 {
 | 
			
		||||
font-size: 24px;
 | 
			
		||||
}
 | 
			
		||||
.refresh-button {
 | 
			
		||||
padding: 8px 12px;
 | 
			
		||||
font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div class="header-container">
 | 
			
		||||
  <h1>MacroPad Web Interface</h1>
 | 
			
		||||
  <button class="refresh-button" onclick="loadMacros()">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
 | 
			
		||||
      <path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
 | 
			
		||||
      <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
    Refresh
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="status success" id="success-status">Macro executed successfully!</div>
 | 
			
		||||
<div class="status error" id="error-status">Failed to execute macro</div>
 | 
			
		||||
<div class="macro-grid" id="macro-grid">
 | 
			
		||||
<!-- Macros will be loaded here -->
 | 
			
		||||
</div>
 | 
			
		||||
<script>
 | 
			
		||||
document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
loadMacros();
 | 
			
		||||
});
 | 
			
		||||
function loadMacros() {
 | 
			
		||||
fetch('/api/macros')
 | 
			
		||||
.then(response => response.json())
 | 
			
		||||
.then(macros => {
 | 
			
		||||
const macroGrid = document.getElementById('macro-grid');
 | 
			
		||||
macroGrid.innerHTML = '';
 | 
			
		||||
if (Object.keys(macros).length === 0) {
 | 
			
		||||
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros defined.</p>';
 | 
			
		||||
return;
 | 
			
		||||
}
 | 
			
		||||
for (const [macroId, macro] of Object.entries(macros)) {
 | 
			
		||||
const button = document.createElement('button');
 | 
			
		||||
button.className = 'macro-button';
 | 
			
		||||
button.onclick = function() { executeMacro(macroId); };
 | 
			
		||||
// Add image if available
 | 
			
		||||
if (macro.image_path) {
 | 
			
		||||
const img = document.createElement('img');
 | 
			
		||||
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
 | 
			
		||||
img.alt = macro.name;
 | 
			
		||||
img.onerror = function() {
 | 
			
		||||
this.style.display = 'none';
 | 
			
		||||
};
 | 
			
		||||
button.appendChild(img);
 | 
			
		||||
}
 | 
			
		||||
const text = document.createTextNode(macro.name);
 | 
			
		||||
button.appendChild(text);
 | 
			
		||||
macroGrid.appendChild(button);
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
.catch(error => {
 | 
			
		||||
console.error('Error loading macros:', error);
 | 
			
		||||
});
 | 
			
		||||
}
 | 
			
		||||
function executeMacro(macroId) {
 | 
			
		||||
fetch('/api/execute', {
 | 
			
		||||
method: 'POST',
 | 
			
		||||
headers: {
 | 
			
		||||
'Content-Type': 'application/json'
 | 
			
		||||
},
 | 
			
		||||
body: JSON.stringify({ macro_id: macroId })
 | 
			
		||||
})
 | 
			
		||||
.then(response => response.json())
 | 
			
		||||
.then(data => {
 | 
			
		||||
const successStatus = document.getElementById('success-status');
 | 
			
		||||
const errorStatus = document.getElementById('error-status');
 | 
			
		||||
if (data.success) {
 | 
			
		||||
successStatus.style.display = 'block';
 | 
			
		||||
errorStatus.style.display = 'none';
 | 
			
		||||
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
 | 
			
		||||
} else {
 | 
			
		||||
errorStatus.style.display = 'block';
 | 
			
		||||
successStatus.style.display = 'none';
 | 
			
		||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
.catch(error => {
 | 
			
		||||
console.error('Error executing macro:', error);
 | 
			
		||||
const errorStatus = document.getElementById('error-status');
 | 
			
		||||
errorStatus.style.display = 'block';
 | 
			
		||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
 | 
			
		||||
});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
        '''
 | 
			
		||||
        
 | 
			
		||||
        @app.route('/')
 | 
			
		||||
        def index():
 | 
			
		||||
            return render_template_string(index_html)
 | 
			
		||||
        
 | 
			
		||||
        @app.route('/api/macros')
 | 
			
		||||
        def get_macros():
 | 
			
		||||
            return jsonify(self.macros)
 | 
			
		||||
        
 | 
			
		||||
        @app.route('/api/image/<path:image_path>')
 | 
			
		||||
        def get_image(image_path):
 | 
			
		||||
            try:
 | 
			
		||||
                # Using os.path.join would translate slashes incorrectly on Windows
 | 
			
		||||
                # Use normpath instead
 | 
			
		||||
                image_path = os.path.join(self.app_dir, image_path)
 | 
			
		||||
                return send_file(image_path)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                return str(e), 404
 | 
			
		||||
        
 | 
			
		||||
        @app.route('/api/execute', methods=['POST'])
 | 
			
		||||
        def execute_macro():
 | 
			
		||||
            if not self.server_running:
 | 
			
		||||
                return jsonify({"success": False, "error": "Server is shutting down"})
 | 
			
		||||
            
 | 
			
		||||
            data = request.get_json()
 | 
			
		||||
            if not data or 'macro_id' not in data:
 | 
			
		||||
                return jsonify({"success": False, "error": "Invalid request"})
 | 
			
		||||
            
 | 
			
		||||
            macro_id = data['macro_id']
 | 
			
		||||
            success = self.execute_macro(macro_id)
 | 
			
		||||
            return jsonify({"success": success})
 | 
			
		||||
        
 | 
			
		||||
        return app
 | 
			
		||||
 | 
			
		||||
    def run_web_server(self):
 | 
			
		||||
        # Create Flask app
 | 
			
		||||
        self.web_app = self.create_web_app()
 | 
			
		||||
        
 | 
			
		||||
        # Update the status in the GUI
 | 
			
		||||
        self.status_var.set(f"Web server running on port {self.server_port}")
 | 
			
		||||
        
 | 
			
		||||
        # Run the Flask app with waitress for production-ready serving
 | 
			
		||||
        try:
 | 
			
		||||
            serve(self.web_app, host='0.0.0.0', port=self.server_port, threads=4)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            self.status_var.set(f"Web server error: {e}")
 | 
			
		||||
            self.server_running = False
 | 
			
		||||
 | 
			
		||||
    def add_macro(self):
 | 
			
		||||
        dialog = tk.Toplevel(self.root)
 | 
			
		||||
        dialog.title("Add Macro")
 | 
			
		||||
        dialog.geometry("450x350")  # Increased height for additional options
 | 
			
		||||
        dialog.transient(self.root)
 | 
			
		||||
        dialog.configure(bg=self.bg_color)
 | 
			
		||||
 | 
			
		||||
        # Apply dark theme to dialog
 | 
			
		||||
        tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
 | 
			
		||||
        name_entry.grid(row=0, column=1, padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        type_var = tk.StringVar(value="text")
 | 
			
		||||
 | 
			
		||||
        radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color}
 | 
			
		||||
        tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w")
 | 
			
		||||
        tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w")
 | 
			
		||||
 | 
			
		||||
        tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
 | 
			
		||||
        command_text.grid(row=3, column=1, padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        # Modifiers frame
 | 
			
		||||
        mod_frame = tk.Frame(dialog, bg=self.bg_color)
 | 
			
		||||
        mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
 | 
			
		||||
        # Add checkboxes for modifiers
 | 
			
		||||
        ctrl_var = tk.BooleanVar(value=False)
 | 
			
		||||
        alt_var = tk.BooleanVar(value=False)
 | 
			
		||||
        shift_var = tk.BooleanVar(value=False)
 | 
			
		||||
        enter_var = tk.BooleanVar(value=False)
 | 
			
		||||
 | 
			
		||||
        checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color,
 | 
			
		||||
                          'activebackground': self.bg_color, 'activeforeground': self.fg_color}
 | 
			
		||||
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
 | 
			
		||||
        image_path = tk.StringVar()
 | 
			
		||||
        tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        image_entry = tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
 | 
			
		||||
        image_entry.grid(row=5, column=1, padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
 | 
			
		||||
                        'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
 | 
			
		||||
 | 
			
		||||
        tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename(
 | 
			
		||||
            filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2)
 | 
			
		||||
 | 
			
		||||
        def save_macro():
 | 
			
		||||
            name = name_entry.get().strip()
 | 
			
		||||
            if not name:
 | 
			
		||||
                tk.messagebox.showerror("Error", "Macro name is required")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            macro_type = type_var.get()
 | 
			
		||||
            command = command_text.get("1.0", tk.END).strip()
 | 
			
		||||
            macro_id = str(uuid.uuid4())
 | 
			
		||||
 | 
			
		||||
            # Process image if provided
 | 
			
		||||
            image_path_reference = ""
 | 
			
		||||
            img_path = image_path.get()
 | 
			
		||||
            if img_path:
 | 
			
		||||
                try:
 | 
			
		||||
                    # Generate unique filename for the image
 | 
			
		||||
                    file_ext = os.path.splitext(img_path)[1].lower()
 | 
			
		||||
                    unique_filename = f"{uuid.uuid4().hex}{file_ext}"
 | 
			
		||||
                    dest_path = os.path.join(self.images_dir, unique_filename)
 | 
			
		||||
                    
 | 
			
		||||
                    # Resize image to max 256x256
 | 
			
		||||
                    with Image.open(img_path) as img:
 | 
			
		||||
                        img.thumbnail((256, 256))
 | 
			
		||||
                        img.save(dest_path)
 | 
			
		||||
                    
 | 
			
		||||
                    # Store the relative path to the image
 | 
			
		||||
                    image_path_reference = os.path.join("macro_images", unique_filename)
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    print(f"Error processing image: {e}")
 | 
			
		||||
 | 
			
		||||
            # Create macro with modifier keys
 | 
			
		||||
            self.macros[macro_id] = {
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "type": macro_type,
 | 
			
		||||
                "command": command,
 | 
			
		||||
                "image_path": image_path_reference,
 | 
			
		||||
                "modifiers": {
 | 
			
		||||
                    "ctrl": ctrl_var.get(),
 | 
			
		||||
                    "alt": alt_var.get(),
 | 
			
		||||
                    "shift": shift_var.get(),
 | 
			
		||||
                    "enter": enter_var.get()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.save_macros()
 | 
			
		||||
            self.display_macros()
 | 
			
		||||
            dialog.destroy()
 | 
			
		||||
 | 
			
		||||
        tk.Button(dialog, text="Save", command=save_macro, **button_style).grid(row=6, column=0, padx=5, pady=20)
 | 
			
		||||
        tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20)
 | 
			
		||||
 | 
			
		||||
    def edit_macro(self):
 | 
			
		||||
        # First, show a dialog to select which macro to edit
 | 
			
		||||
        if not self.macros:
 | 
			
		||||
            tk.messagebox.showinfo("No Macros", "There are no macros to edit.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        dialog = tk.Toplevel(self.root)
 | 
			
		||||
        dialog.title("Select Macro to Edit")
 | 
			
		||||
        dialog.geometry("200x340")
 | 
			
		||||
        dialog.transient(self.root)
 | 
			
		||||
        dialog.configure(bg=self.bg_color)
 | 
			
		||||
 | 
			
		||||
        # Create a listbox to show available macros
 | 
			
		||||
        tk.Label(dialog, text="Select a macro to edit:", bg=self.bg_color, fg=self.fg_color).pack(pady=5)
 | 
			
		||||
 | 
			
		||||
        listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color)
 | 
			
		||||
        listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
 | 
			
		||||
 | 
			
		||||
        # Populate the listbox with macro names
 | 
			
		||||
        macro_ids = []
 | 
			
		||||
        for macro_id, macro in self.macros.items():
 | 
			
		||||
            listbox.insert(tk.END, macro["name"])
 | 
			
		||||
            macro_ids.append(macro_id)
 | 
			
		||||
 | 
			
		||||
        def on_select():
 | 
			
		||||
            if not listbox.curselection():
 | 
			
		||||
                tk.messagebox.showwarning("No Selection", "Please select a macro to edit.")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            idx = listbox.curselection()[0]
 | 
			
		||||
            selected_macro_id = macro_ids[idx]
 | 
			
		||||
            dialog.destroy()
 | 
			
		||||
            self.open_edit_dialog(selected_macro_id)
 | 
			
		||||
 | 
			
		||||
        button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
 | 
			
		||||
                        'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
 | 
			
		||||
 | 
			
		||||
        tk.Button(dialog, text="Edit Selected", command=on_select, **button_style).pack(pady=10)
 | 
			
		||||
        tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5)
 | 
			
		||||
 | 
			
		||||
    def open_edit_dialog(self, macro_id):
 | 
			
		||||
        # Open dialog to edit the selected macro
 | 
			
		||||
        macro = self.macros[macro_id]
 | 
			
		||||
 | 
			
		||||
        dialog = tk.Toplevel(self.root)
 | 
			
		||||
        dialog.title("Edit Macro")
 | 
			
		||||
        dialog.geometry("450x350")  # Increased height for additional options
 | 
			
		||||
        dialog.transient(self.root)
 | 
			
		||||
        dialog.configure(bg=self.bg_color)
 | 
			
		||||
 | 
			
		||||
        # Apply dark theme to dialog
 | 
			
		||||
        tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
 | 
			
		||||
        name_entry.grid(row=0, column=1, padx=5, pady=5)
 | 
			
		||||
        name_entry.insert(0, macro["name"])
 | 
			
		||||
 | 
			
		||||
        tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        type_var = tk.StringVar(value=macro["type"])
 | 
			
		||||
 | 
			
		||||
        radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color}
 | 
			
		||||
        tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w")
 | 
			
		||||
        tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w")
 | 
			
		||||
 | 
			
		||||
        tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
 | 
			
		||||
        command_text.grid(row=3, column=1, padx=5, pady=5)
 | 
			
		||||
        command_text.insert("1.0", macro["command"])
 | 
			
		||||
 | 
			
		||||
        # Modifiers frame
 | 
			
		||||
        mod_frame = tk.Frame(dialog, bg=self.bg_color)
 | 
			
		||||
        mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
 | 
			
		||||
        # Get existing modifiers or set defaults
 | 
			
		||||
        modifiers = macro.get("modifiers", {})
 | 
			
		||||
 | 
			
		||||
        # Add checkboxes for modifiers
 | 
			
		||||
        ctrl_var = tk.BooleanVar(value=modifiers.get("ctrl", False))
 | 
			
		||||
        alt_var = tk.BooleanVar(value=modifiers.get("alt", False))
 | 
			
		||||
        shift_var = tk.BooleanVar(value=modifiers.get("shift", False))
 | 
			
		||||
        enter_var = tk.BooleanVar(value=modifiers.get("enter", False))
 | 
			
		||||
 | 
			
		||||
        checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color,
 | 
			
		||||
                        'activebackground': self.bg_color, 'activeforeground': self.fg_color}
 | 
			
		||||
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
        tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
 | 
			
		||||
 | 
			
		||||
        image_path = tk.StringVar()
 | 
			
		||||
        tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w")
 | 
			
		||||
        image_entry = tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color)
 | 
			
		||||
        image_entry.grid(row=5, column=1, padx=5, pady=5)
 | 
			
		||||
 | 
			
		||||
        button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
 | 
			
		||||
                        'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
 | 
			
		||||
 | 
			
		||||
        tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename(
 | 
			
		||||
            filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2)
 | 
			
		||||
 | 
			
		||||
        def save_edited_macro():
 | 
			
		||||
            name = name_entry.get().strip()
 | 
			
		||||
            if not name:
 | 
			
		||||
                tk.messagebox.showerror("Error", "Macro name is required")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            new_type = type_var.get()
 | 
			
		||||
            command = command_text.get("1.0", tk.END).strip()
 | 
			
		||||
 | 
			
		||||
            # Keep the old image or update with new one
 | 
			
		||||
            image_path_reference = macro.get("image_path", "")
 | 
			
		||||
            img_path = image_path.get()
 | 
			
		||||
            if img_path:
 | 
			
		||||
                try:
 | 
			
		||||
                    # Generate unique filename for the image
 | 
			
		||||
                    file_ext = os.path.splitext(img_path)[1].lower()
 | 
			
		||||
                    unique_filename = f"{uuid.uuid4().hex}{file_ext}"
 | 
			
		||||
                    dest_path = os.path.join(self.images_dir, unique_filename)
 | 
			
		||||
                    
 | 
			
		||||
                    # Resize image to max 256x256
 | 
			
		||||
                    with Image.open(img_path) as img:
 | 
			
		||||
                        img.thumbnail((256, 256))
 | 
			
		||||
                        img.save(dest_path)
 | 
			
		||||
                    
 | 
			
		||||
                    # Store the relative path to the image
 | 
			
		||||
                    image_path_reference = os.path.join("macro_images", unique_filename)
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    print(f"Error processing image: {e}")
 | 
			
		||||
 | 
			
		||||
            # Update macro with modifiers
 | 
			
		||||
            self.macros[macro_id] = {
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "type": new_type,
 | 
			
		||||
                "command": command,
 | 
			
		||||
                "image_path": image_path_reference,
 | 
			
		||||
                "modifiers": {
 | 
			
		||||
                    "ctrl": ctrl_var.get(),
 | 
			
		||||
                    "alt": alt_var.get(),
 | 
			
		||||
                    "shift": shift_var.get(),
 | 
			
		||||
                    "enter": enter_var.get()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.save_macros()
 | 
			
		||||
            self.display_macros()
 | 
			
		||||
            dialog.destroy()
 | 
			
		||||
 | 
			
		||||
        tk.Button(dialog, text="Save", command=save_edited_macro, **button_style).grid(row=6, column=0, padx=5, pady=20)
 | 
			
		||||
        tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20)
 | 
			
		||||
 | 
			
		||||
    def delete_macro(self):
 | 
			
		||||
        # Show a dialog to select which macro to delete
 | 
			
		||||
        if not self.macros:
 | 
			
		||||
            tk.messagebox.showinfo("No Macros", "There are no macros to delete.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        dialog = tk.Toplevel(self.root)
 | 
			
		||||
        dialog.title("Delete Macro")
 | 
			
		||||
        dialog.geometry("200x340")
 | 
			
		||||
        dialog.transient(self.root)
 | 
			
		||||
        dialog.configure(bg=self.bg_color)
 | 
			
		||||
 | 
			
		||||
        # Create a listbox to show available macros
 | 
			
		||||
        tk.Label(dialog, text="Select a macro to delete:", bg=self.bg_color, fg=self.fg_color).pack(pady=5)
 | 
			
		||||
 | 
			
		||||
        listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color)
 | 
			
		||||
        listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
 | 
			
		||||
 | 
			
		||||
        # Populate the listbox with macro names
 | 
			
		||||
        macro_ids = []
 | 
			
		||||
        for macro_id, macro in self.macros.items():
 | 
			
		||||
            listbox.insert(tk.END, macro["name"])
 | 
			
		||||
            macro_ids.append(macro_id)
 | 
			
		||||
 | 
			
		||||
        def on_delete():
 | 
			
		||||
            if not listbox.curselection():
 | 
			
		||||
                tk.messagebox.showwarning("No Selection", "Please select a macro to delete.")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            idx = listbox.curselection()[0]
 | 
			
		||||
            selected_macro_id = macro_ids[idx]
 | 
			
		||||
            selected_name = self.macros[selected_macro_id]["name"]
 | 
			
		||||
 | 
			
		||||
            # Confirm deletion
 | 
			
		||||
            if tk.messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{selected_name}'?"):
 | 
			
		||||
                # Delete associated image file if it exists
 | 
			
		||||
                macro = self.macros[selected_macro_id]
 | 
			
		||||
                if "image_path" in macro and macro["image_path"]:
 | 
			
		||||
                    try:
 | 
			
		||||
                        img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"])
 | 
			
		||||
                        if os.path.exists(img_path):
 | 
			
		||||
                            os.remove(img_path)
 | 
			
		||||
                    except Exception as e:
 | 
			
		||||
                        print(f"Error removing image file: {e}")
 | 
			
		||||
                del self.macros[selected_macro_id]
 | 
			
		||||
                self.save_macros()
 | 
			
		||||
                self.display_macros()
 | 
			
		||||
                dialog.destroy()
 | 
			
		||||
 | 
			
		||||
        button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color,
 | 
			
		||||
                        'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT}
 | 
			
		||||
 | 
			
		||||
        tk.Button(dialog, text="Delete Selected", command=on_delete, **button_style).pack(pady=10)
 | 
			
		||||
        tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5)
 | 
			
		||||
 | 
			
		||||
    def execute_macro(self, macro_id):
 | 
			
		||||
        if macro_id not in self.macros:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        macro = self.macros[macro_id]
 | 
			
		||||
        try:
 | 
			
		||||
            if macro["type"] == "text":
 | 
			
		||||
                # Handle key modifiers
 | 
			
		||||
                modifiers = macro.get("modifiers", {})
 | 
			
		||||
 | 
			
		||||
                # Add modifier keys if enabled
 | 
			
		||||
                if modifiers.get("ctrl", False):
 | 
			
		||||
                    pyautogui.keyDown('ctrl')
 | 
			
		||||
                if modifiers.get("alt", False):
 | 
			
		||||
                    pyautogui.keyDown('alt')
 | 
			
		||||
                if modifiers.get("shift", False):
 | 
			
		||||
                    pyautogui.keyDown('shift')
 | 
			
		||||
                if str(macro["command"]) and len(str(macro["command"])) == 1:
 | 
			
		||||
                    pyautogui.keyDown(macro["command"])
 | 
			
		||||
                    time.sleep(0.5)
 | 
			
		||||
                    pyautogui.keyUp(macro["command"])
 | 
			
		||||
                else:
 | 
			
		||||
                    pyautogui.typewrite(macro["command"], interval=0.02)
 | 
			
		||||
                # Release modifier keys in reverse order
 | 
			
		||||
                if modifiers.get("shift", False):
 | 
			
		||||
                    pyautogui.keyUp('shift')
 | 
			
		||||
                if modifiers.get("alt", False):
 | 
			
		||||
                    pyautogui.keyUp('alt')
 | 
			
		||||
                if modifiers.get("ctrl", False):
 | 
			
		||||
                    pyautogui.keyUp('ctrl')
 | 
			
		||||
 | 
			
		||||
                # Add Enter/Return if requested
 | 
			
		||||
                if modifiers.get("enter", False):
 | 
			
		||||
                    pyautogui.press('enter')
 | 
			
		||||
 | 
			
		||||
            elif macro["type"] == "app":
 | 
			
		||||
                subprocess.Popen(macro["command"], shell=True)
 | 
			
		||||
            return True
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error executing macro: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def create_tray_icon(self):
 | 
			
		||||
        # Create tray icon
 | 
			
		||||
        icon_image = Image.new("RGB", (64, 64), self.accent_color)
 | 
			
		||||
        menu = (
 | 
			
		||||
            pystray.MenuItem('Show', self.show_window),
 | 
			
		||||
            pystray.MenuItem('Exit', self.exit_app)
 | 
			
		||||
        )
 | 
			
		||||
        self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu)
 | 
			
		||||
        self.tray_icon.run_detached()
 | 
			
		||||
 | 
			
		||||
    def show_window(self, icon=None, item=None):
 | 
			
		||||
        # Show the window from tray
 | 
			
		||||
        self.root.deiconify()
 | 
			
		||||
        self.root.state('normal')
 | 
			
		||||
        self.root.lift()
 | 
			
		||||
        self.root.focus_force()
 | 
			
		||||
 | 
			
		||||
    def exit_app(self, icon=None, item=None):
 | 
			
		||||
        # Actually exit the app
 | 
			
		||||
        self.stop_server()
 | 
			
		||||
        self.server_running = False
 | 
			
		||||
        if hasattr(self, 'tray_icon'):
 | 
			
		||||
            self.tray_icon.stop()
 | 
			
		||||
        self.root.quit()
 | 
			
		||||
 | 
			
		||||
    def minimize_to_tray(self):
 | 
			
		||||
        # Create tray icon if it doesn't exist
 | 
			
		||||
        if not hasattr(self, 'tray_icon'):
 | 
			
		||||
            self.create_tray_icon()
 | 
			
		||||
        self.root.withdraw()
 | 
			
		||||
 | 
			
		||||
    def on_closing(self):
 | 
			
		||||
        # When the window is closed, minimize to tray instead of closing
 | 
			
		||||
        self.minimize_to_tray()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    root = tk.Tk()
 | 
			
		||||
    app = MacroPadServer(root)
 | 
			
		||||
    root.mainloop()
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
0.5.4
 | 
			
		||||
0.7.5
 | 
			
		||||
		Reference in New Issue
	
	Block a user