From d74bedfa285faf586994585524543141920ba349 Mon Sep 17 00:00:00 2001 From: jknapp Date: Fri, 14 Mar 2025 22:48:34 -0700 Subject: [PATCH] added minimize to system tray --- mp-server.py | 147 +++++++++++++++++++++++++++++++---------------- requirements.txt | 3 + 2 files changed, 100 insertions(+), 50 deletions(-) create mode 100644 requirements.txt diff --git a/mp-server.py b/mp-server.py index 9f50910..faee9f9 100644 --- a/mp-server.py +++ b/mp-server.py @@ -8,62 +8,106 @@ import os import pyautogui import subprocess from PIL import Image, ImageTk +import pystray # Add this import for system tray functionality +import io class MacroServer: def __init__(self, root): self.root = root self.root.title("MacroPad Server") self.root.geometry("800x600") - + self.macros = {} self.load_macros() - + # Create GUI self.create_widgets() - + # Start server thread self.server_running = True self.server_thread = threading.Thread(target=self.run_server) self.server_thread.daemon = True self.server_thread.start() - + + # Set up system tray icon + self.setup_tray_icon() + + # Capture the window close event + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + # Capture the window minimize event + self.root.bind("", lambda e: self.minimize_to_tray() if self.root.state() == 'iconic' else None) + def setup_tray_icon(self): + # Create a simple icon for the tray + icon_image = Image.new("RGB", (64, 64), color="blue") + + # Menu for the tray icon + menu = ( + pystray.MenuItem('Show', self.show_window), + pystray.MenuItem('Exit', self.exit_app) + ) + + self.icon = pystray.Icon("macropad_server", icon_image, "MacroPad Server", menu) + + def minimize_to_tray(self): + self.root.withdraw() # Hide window + if not self.icon.visible: + # Start the tray icon if it's not already running + self.icon_thread = threading.Thread(target=self.icon.run) + self.icon_thread.daemon = True + self.icon_thread.start() + + def show_window(self, icon=None, item=None): + self.root.deiconify() # Show window + self.root.lift() # Bring window to front + self.root.focus_force() # Focus the window + if self.icon.visible: + self.icon.stop() # Remove icon + + def exit_app(self, icon=None, item=None): + if self.icon.visible: + self.icon.stop() + self.server_running = False + self.save_macros() + self.root.destroy() + def create_widgets(self): # Toolbar toolbar = tk.Frame(self.root) toolbar.pack(side=tk.TOP, fill=tk.X) - + tk.Button(toolbar, text="Add Macro", command=self.add_macro).pack(side=tk.LEFT, padx=5, pady=5) tk.Button(toolbar, text="Edit Macro", command=self.edit_macro).pack(side=tk.LEFT, padx=5, pady=5) tk.Button(toolbar, text="Delete Macro", command=self.delete_macro).pack(side=tk.LEFT, padx=5, pady=5) - + tk.Button(toolbar, text="Minimize to Tray", command=self.minimize_to_tray).pack(side=tk.LEFT, padx=5, pady=5) + # Macro list frame list_frame = tk.Frame(self.root) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - + # Scrollable canvas for macros self.canvas = tk.Canvas(list_frame) scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=self.canvas.yview) self.scrollable_frame = tk.Frame(self.canvas) - + self.scrollable_frame.bind( "", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")) ) - + self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.canvas.configure(yscrollcommand=scrollbar.set) - + self.canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") - + self.display_macros() - + # Status bar self.status_var = tk.StringVar() self.status_var.set("Server ready. Waiting for connections...") status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) - + def load_macros(self): try: if os.path.exists("macros.json"): @@ -72,27 +116,27 @@ class MacroServer: except Exception as e: print(f"Error loading macros: {e}") self.macros = {} - + def save_macros(self): try: with open("macros.json", "w") as f: json.dump(self.macros, f) except Exception as e: print(f"Error saving macros: {e}") - + def display_macros(self): # Clear existing macros for widget in self.scrollable_frame.winfo_children(): widget.destroy() - + # Display macros in a grid row, col = 0, 0 max_cols = 3 - + for macro_id, macro in self.macros.items(): frame = tk.Frame(self.scrollable_frame, bd=2, relief=tk.RAISED, padx=5, pady=5) frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") - + # Load image if exists if "image_data" in macro and macro["image_data"]: try: @@ -108,52 +152,52 @@ class MacroServer: tk.Label(frame, text="[No Image]", width=8, height=4).pack() else: tk.Label(frame, text="[No Image]", width=8, height=4).pack() - + tk.Label(frame, text=macro["name"]).pack() - + col += 1 if col >= max_cols: col = 0 row += 1 - + def add_macro(self): # Create a dialog to add a new macro dialog = tk.Toplevel(self.root) dialog.title("Add Macro") dialog.geometry("400x300") dialog.transient(self.root) - + tk.Label(dialog, text="Macro Name:").grid(row=0, column=0, padx=5, pady=5, sticky="w") name_entry = tk.Entry(dialog, width=30) name_entry.grid(row=0, column=1, padx=5, pady=5) - + tk.Label(dialog, text="Type:").grid(row=1, column=0, padx=5, pady=5, sticky="w") type_var = tk.StringVar(value="text") tk.Radiobutton(dialog, text="Text", variable=type_var, value="text").grid(row=1, column=1, sticky="w") tk.Radiobutton(dialog, text="Application", variable=type_var, value="app").grid(row=2, column=1, sticky="w") - + tk.Label(dialog, text="Command/Text:").grid(row=3, column=0, padx=5, pady=5, sticky="w") command_text = tk.Text(dialog, width=30, height=5) command_text.grid(row=3, column=1, padx=5, pady=5) - + image_path = tk.StringVar() tk.Label(dialog, text="Image:").grid(row=4, column=0, padx=5, pady=5, sticky="w") tk.Entry(dialog, textvariable=image_path, width=30).grid(row=4, column=1, padx=5, pady=5) tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename( filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")]))).grid(row=4, 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() - + # Generate a unique ID macro_id = str(len(self.macros) + 1) - + # Process image image_data = "" img_path = image_path.get() @@ -163,7 +207,7 @@ class MacroServer: image_data = base64.b64encode(img_file.read()).decode('utf-8') except Exception as e: print(f"Error processing image: {e}") - + # Create macro self.macros[macro_id] = { "name": name, @@ -171,26 +215,26 @@ class MacroServer: "command": command, "image_data": image_data } - + self.save_macros() self.display_macros() dialog.destroy() - + tk.Button(dialog, text="Save", command=save_macro).grid(row=5, column=0, padx=5, pady=20) tk.Button(dialog, text="Cancel", command=dialog.destroy).grid(row=5, column=1, padx=5, pady=20) - + def edit_macro(self): # To be implemented - similar to add_macro but pre-fills fields with existing data pass - + def delete_macro(self): # To be implemented - offers a selection and deletes the chosen macro pass - + 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": @@ -201,35 +245,35 @@ class MacroServer: except Exception as e: print(f"Error executing macro: {e}") return False - + def run_server(self): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - + try: server_socket.bind(('0.0.0.0', 40000)) server_socket.listen(5) - + while self.server_running: client_socket, address = server_socket.accept() self.status_var.set(f"Client connected from {address}") - + client_thread = threading.Thread(target=self.handle_client, args=(client_socket,)) client_thread.daemon = True client_thread.start() - + except Exception as e: print(f"Server error: {e}") finally: server_socket.close() - + def handle_client(self, client_socket): try: while self.server_running: data = client_socket.recv(1024).decode('utf-8') if not data: break - + try: request = json.loads(data) if request['action'] == 'get_macros': @@ -241,20 +285,23 @@ class MacroServer: client_socket.send(json.dumps({'success': success}).encode('utf-8')) except json.JSONDecodeError: print("Invalid JSON received") - + except Exception as e: print(f"Error handling client: {e}") finally: client_socket.close() - + def on_closing(self): - self.server_running = False - self.save_macros() - self.root.destroy() + # Instead of directly closing, ask if user wants to minimize to tray + if tk.messagebox.askyesno("Minimize to Tray", "Do you want to minimize to the system tray?"): + self.minimize_to_tray() + else: + self.exit_app() if __name__ == "__main__": import io # Added for BytesIO + import tkinter.messagebox # Add this for message boxes + root = tk.Tk() app = MacroServer(root) - root.protocol("WM_DELETE_WINDOW", app.on_closing) - root.mainloop() + root.mainloop() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dd465c4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pillow +pyautogui +pystray \ No newline at end of file