# Main application file for MacroPad Server import tkinter as tk from tkinter import ttk, messagebox import os import sys import threading import socket import qrcode import webbrowser import pystray from PIL import Image, ImageTk, ImageDraw, ImageFont from config import VERSION, DEFAULT_PORT, THEME from macro_manager import MacroManager from web_server import WebServer from ui_components import MacroDialog, MacroSelector, TabManager class MacroPadServer: def __init__(self, root): self.root = root self.root.title("MacroPad Server") self.root.geometry("800x600") self.configure_styles() # Set up directories if getattr(sys, 'frozen', False): base_dir = os.path.dirname(sys.executable) else: base_dir = os.path.dirname(os.path.abspath(__file__)) data_file = os.path.join(base_dir, "macros.json") images_dir = os.path.join(base_dir, "macro_images") os.makedirs(images_dir, exist_ok=True) # Initialize components self.macro_manager = MacroManager(data_file, images_dir, base_dir) self.web_server = WebServer(self.macro_manager, base_dir, DEFAULT_PORT) # UI state self.current_sort = "name" self.current_tab = "All" self.image_cache = {} # Server state self.server_running = False self.flask_thread = None # Tray state self.tray_icon = None self.is_closing = False # Create UI self.create_ui() # Set up window event handlers self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.root.bind('', self.on_minimize) # Initialize tray icon self.create_tray_icon() def configure_styles(self): """Configure the dark theme styles""" self.root.configure(bg=THEME['bg_color']) style = ttk.Style() style.theme_use("clam") style.configure("TButton", background=THEME['button_bg'], foreground=THEME['button_fg']) style.map("TButton", background=[("active", THEME['accent_color'])]) style.configure("TFrame", background=THEME['bg_color']) style.configure("TLabel", background=THEME['bg_color'], foreground=THEME['fg_color']) # Configure notebook (tabs) style style.configure("TNotebook", background=THEME['bg_color'], borderwidth=0) style.configure("TNotebook.Tab", background=THEME['tab_bg'], foreground=THEME['fg_color'], padding=[12, 8], borderwidth=0) style.map("TNotebook.Tab", background=[("selected", THEME['tab_selected'])], foreground=[("selected", THEME['fg_color'])]) def create_ui(self): """Create the main user interface""" # Create main container main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Left side: Macro list and controls left_frame = ttk.Frame(main_frame) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Sort controls self._create_sort_controls(left_frame) # Create notebook for tabs self.notebook = ttk.Notebook(left_frame) self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) self.notebook.bind("<>", self.on_tab_change) # Button controls self._create_macro_buttons(left_frame) # Right side: Server controls right_frame = ttk.Frame(main_frame) right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0)) self._create_server_controls(right_frame) # Version label version_label = tk.Label(self.root, text=VERSION, bg=THEME['bg_color'], fg=THEME['fg_color'], font=('Helvetica', 8)) version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2)) # Initialize display self.setup_tabs() self.display_macros() def _create_sort_controls(self, parent): """Create sorting controls""" sort_frame = ttk.Frame(parent) sort_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(sort_frame, text="Sort by:").pack(side=tk.LEFT, padx=(0, 5)) self.sort_var = tk.StringVar(value=self.current_sort) sort_combo = ttk.Combobox(sort_frame, textvariable=self.sort_var, values=["name", "type", "recent"], state="readonly", width=10) sort_combo.pack(side=tk.LEFT, padx=(0, 10)) sort_combo.bind("<>", self.on_sort_change) # Tab management button ttk.Button(sort_frame, text="Manage Tabs", command=self.manage_tabs).pack(side=tk.RIGHT, padx=(5, 0)) def _create_macro_buttons(self, parent): """Create macro management buttons""" button_frame = ttk.Frame(parent) button_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(button_frame, text="Add Macro", command=self.add_macro).pack(side=tk.LEFT, padx=2) ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro).pack(side=tk.LEFT, padx=2) ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro).pack(side=tk.LEFT, padx=2) def _create_server_controls(self, parent): """Create web server controls""" server_frame = ttk.Frame(parent) server_frame.pack(fill=tk.X, pady=10) ttk.Label(server_frame, text="Web Server Control:").grid(row=0, column=0, sticky="w", pady=5) self.server_button = ttk.Button(server_frame, text="Start Web Server", command=self.toggle_server) self.server_button.grid(row=0, column=1, padx=5, pady=5) # Status display self.status_var = tk.StringVar(value="Web server not running") ttk.Label(server_frame, textvariable=self.status_var).grid(row=1, column=0, columnspan=2, sticky="w", pady=5) # QR code display self.qr_label = ttk.Label(parent) self.qr_label.pack(pady=10) # URL display self.url_var = tk.StringVar(value="") self.url_label = ttk.Label(parent, textvariable=self.url_var) self.url_label.pack(pady=5) # Browser button self.browser_button = ttk.Button(parent, text="Open in Browser", command=self.open_in_browser, state=tk.DISABLED) self.browser_button.pack(pady=5) def setup_tabs(self): """Initialize tabs based on macro categories""" # Clear existing tabs for tab in self.notebook.tabs(): self.notebook.forget(tab) # Get unique tabs from macro manager tabs = self.macro_manager.get_unique_tabs() for tab_name in tabs: frame = ttk.Frame(self.notebook) self.notebook.add(frame, text=tab_name) def get_current_tab_name(self): """Get the name of the currently selected tab""" try: current_tab_id = self.notebook.select() return self.notebook.tab(current_tab_id, "text") except: return "All" def on_tab_change(self, event=None): """Handle tab change event""" self.current_tab = self.get_current_tab_name() self.display_macros() def on_sort_change(self, event=None): """Handle sort option change""" self.current_sort = self.sort_var.get() self.display_macros() def display_macros(self): """Display macros in the current tab""" # Get current tab frame try: current_tab_id = self.notebook.select() current_frame = self.notebook.nametowidget(current_tab_id) except: return # Clear previous content for widget in current_frame.winfo_children(): widget.destroy() # Create scrollable canvas canvas = tk.Canvas(current_frame, bg=THEME['bg_color'], highlightthickness=0) scrollbar = ttk.Scrollbar(current_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Get sorted and filtered macros sorted_macros = self.macro_manager.get_sorted_macros(self.current_sort) filtered_macros = self.macro_manager.filter_macros_by_tab(sorted_macros, self.current_tab) # Display macros for macro_id, macro in filtered_macros: self._create_macro_button(scrollable_frame, macro_id, macro) # Display message if no macros if not filtered_macros: label = tk.Label(scrollable_frame, text="No macros in this category", bg=THEME['bg_color'], fg=THEME['fg_color']) label.pack(pady=20) def _create_macro_button(self, parent, macro_id, macro): """Create a button for a single macro""" frame = ttk.Frame(parent) frame.pack(fill="x", pady=5, padx=5) button = tk.Button( frame, text=macro["name"], bg=THEME['button_bg'], fg=THEME['button_fg'], activebackground=THEME['accent_color'], activeforeground=THEME['button_fg'], relief=tk.RAISED, bd=2, pady=8, command=lambda: self.macro_manager.execute_macro(macro_id) ) # Add image if available if "image_path" in macro and macro["image_path"]: try: if macro["image_path"] in self.image_cache: button_image = self.image_cache[macro["image_path"]] else: img_path = os.path.join(self.macro_manager.app_dir, macro["image_path"]) img = Image.open(img_path) img = img.resize((32, 32)) button_image = ImageTk.PhotoImage(img) self.image_cache[macro["image_path"]] = button_image button.config(image=button_image, compound=tk.LEFT) button.image = button_image # Keep reference except Exception as e: print(f"Error loading image for {macro['name']}: {e}") button.pack(side=tk.LEFT, fill=tk.X, expand=True) def manage_tabs(self): """Open tab management dialog""" tab_manager = TabManager(self.root, self.macro_manager) tab_manager.show() self.setup_tabs() self.display_macros() def add_macro(self): """Add a new macro""" dialog = MacroDialog(self.root, self.macro_manager) result = dialog.show() if result: self.setup_tabs() self.display_macros() def edit_macro(self): """Edit an existing macro""" selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Edit") macro_id = selector.show() if macro_id: dialog = MacroDialog(self.root, self.macro_manager, macro_id) result = dialog.show() if result: self.setup_tabs() self.display_macros() def delete_macro(self): """Delete a macro""" selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Delete") macro_id = selector.show() if macro_id: macro_name = self.macro_manager.macros[macro_id]["name"] if messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{macro_name}'?"): self.macro_manager.delete_macro(macro_id) self.setup_tabs() self.display_macros() def toggle_server(self): """Toggle web server on/off""" if self.server_running: self.stop_server() else: self.start_server() def start_server(self): """Start the web server""" try: if not self.server_running: self.server_running = True self.flask_thread = threading.Thread(target=self.run_web_server) self.flask_thread.daemon = True self.flask_thread.start() self.server_button.config(text="Stop Web Server") # Get IP address and display info ip_address = self.get_ip_address() if ip_address: url = f"http://{ip_address}:{DEFAULT_PORT}" url_text = f"Web UI available at:\n{url}" self.url_var.set(url_text) self.browser_button.config(state=tk.NORMAL) self.generate_qr_code(url) else: self.url_var.set("No network interfaces found") except Exception as e: self.status_var.set(f"Error starting server: {e}") self.server_running = False def stop_server(self): """Stop the web server""" if self.server_running: self.server_running = False self.status_var.set("Web server stopped") self.server_button.config(text="Start Web Server") self.url_var.set("") self.browser_button.config(state=tk.DISABLED) self.qr_label.config(image="") def run_web_server(self): """Run the web server in a separate thread""" self.status_var.set(f"Web server running on port {DEFAULT_PORT}") try: self.web_server.run() except Exception as e: self.status_var.set(f"Web server error: {e}") self.server_running = False def get_ip_address(self): """Get the primary internal IPv4 address""" try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception as e: print(f"Error getting IP address: {e}") try: hostname = socket.gethostname() ip = socket.gethostbyname(hostname) if not ip.startswith("127."): return ip for addr_info in socket.getaddrinfo(hostname, None): potential_ip = addr_info[4][0] if '.' in potential_ip and not potential_ip.startswith("127."): return potential_ip except: pass return "127.0.0.1" def generate_qr_code(self, url): """Generate and display QR code for the URL""" try: qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, ) qr.add_data(url) qr.make(fit=True) qr_img = qr.make_image(fill_color="black", back_color="white") qr_photoimg = ImageTk.PhotoImage(qr_img) self.qr_label.config(image=qr_photoimg) self.qr_label.image = qr_photoimg except ImportError: self.qr_label.config(text="QR code generation requires 'qrcode' package") except Exception as e: print(f"Error generating QR code: {e}") self.qr_label.config(text="Error generating QR code") def open_in_browser(self): """Open the web interface in browser""" if self.server_running: webbrowser.open(f"http://localhost:{DEFAULT_PORT}") def on_minimize(self, event): """Handle window minimize event""" # Only minimize to tray if the window is being iconified, not just unmapped if event.widget == self.root and self.root.state() == 'iconic': self.root.withdraw() # Hide window def create_tray_icon(self): """Create system tray icon""" try: # Create a simple icon image with M letter icon_image = Image.new("RGB", (64, 64), THEME['accent_color']) draw = ImageDraw.Draw(icon_image) try: # Try to use a system font font = ImageFont.truetype("arial.ttf", 40) except: try: # Try other common fonts font = ImageFont.truetype("calibri.ttf", 40) except: # Fall back to default font font = ImageFont.load_default() # Draw "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""" self.root.deiconify() self.root.state('normal') self.root.lift() self.root.focus_force() def exit_app(self, icon=None, item=None): """Exit the application""" self.is_closing = True self.stop_server() if self.tray_icon: try: self.tray_icon.stop() except: pass try: self.root.quit() except: pass # Force exit if needed import os os._exit(0) def on_closing(self): """Handle window close event - exit the application""" self.exit_app() if __name__ == "__main__": root = tk.Tk() app = MacroPadServer(root) root.mainloop()