# 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 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 # Create UI self.create_ui() # Set up window close handler self.root.protocol("WM_DELETE_WINDOW", self.on_closing) 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 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() 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.stop_server() if hasattr(self, 'tray_icon'): self.tray_icon.stop() self.root.quit() def on_closing(self): """Handle window close event""" # For now just exit, but could minimize to tray self.exit_app() if __name__ == "__main__": root = tk.Tk() app = MacroPadServer(root) root.mainloop()