From d7a291a58fefe32387a9361d58dbb1cf9037eb34 Mon Sep 17 00:00:00 2001 From: jknapp Date: Sat, 15 Mar 2025 17:03:51 -0700 Subject: [PATCH] updating mp-server.py --- mp-server.py | 397 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 344 insertions(+), 53 deletions(-) diff --git a/mp-server.py b/mp-server.py index faee9f9..3a08eb2 100644 --- a/mp-server.py +++ b/mp-server.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import filedialog, simpledialog +from tkinter import filedialog, simpledialog, ttk import json import socket import threading @@ -10,13 +10,35 @@ import subprocess from PIL import Image, ImageTk import pystray # Add this import for system tray functionality import io +import time -class MacroServer: +class MacroPadServer: def __init__(self, root): self.root = root self.root.title("MacroPad Server") self.root.geometry("800x600") + # Set dark theme colors + self.bg_color = "#2E2E2E" + self.fg_color = "#FFFFFF" + self.highlight_color = "#3D3D3D" + self.accent_color = "#007BFF" + self.button_bg = "#444444" + self.button_fg = "#FFFFFF" + + # Apply theme + self.root.configure(bg=self.bg_color) + self.style = ttk.Style() + self.style.theme_use('clam') + + # Configure styles + self.style.configure('TFrame', background=self.bg_color) + self.style.configure('TLabel', background=self.bg_color, foreground=self.fg_color) + self.style.configure('TButton', background=self.button_bg, foreground=self.button_fg, borderwidth=0) + self.style.map('TButton', + background=[('active', self.accent_color), ('disabled', self.highlight_color)], + foreground=[('active', self.button_fg), ('disabled', '#999999')]) + self.style.configure('TRadiobutton', background=self.bg_color, foreground=self.fg_color) self.macros = {} self.load_macros() @@ -36,9 +58,10 @@ class MacroServer: 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") + icon_image = Image.new("RGB", (64, 64), color=self.accent_color) # Menu for the tray icon menu = ( @@ -72,22 +95,26 @@ class MacroServer: def create_widgets(self): # Toolbar - toolbar = tk.Frame(self.root) + toolbar = tk.Frame(self.root, bg=self.highlight_color) 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) + # Custom button style + button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'padx': 10, + 'pady': 5, 'bd': 0, 'activebackground': self.accent_color, + 'activeforeground': self.button_fg, 'relief': tk.FLAT} + tk.Button(toolbar, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="Minimize to Tray", command=self.minimize_to_tray, **button_style).pack(side=tk.LEFT, padx=5, pady=5) # Macro list frame - list_frame = tk.Frame(self.root) + list_frame = tk.Frame(self.root, bg=self.bg_color) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Scrollable canvas for macros - self.canvas = tk.Canvas(list_frame) + self.canvas = tk.Canvas(list_frame, bg=self.bg_color, highlightthickness=0) scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=self.canvas.yview) - self.scrollable_frame = tk.Frame(self.canvas) + self.scrollable_frame = tk.Frame(self.canvas, bg=self.bg_color) self.scrollable_frame.bind( "", @@ -105,7 +132,8 @@ class MacroServer: # 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 = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, + anchor=tk.W, bg=self.highlight_color, fg=self.fg_color) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def load_macros(self): @@ -134,7 +162,8 @@ class MacroServer: 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 = tk.Frame(self.scrollable_frame, bd=2, relief=tk.RAISED, padx=5, pady=5, + bg=self.highlight_color) frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") # Load image if exists @@ -144,16 +173,16 @@ class MacroServer: image = Image.open(io.BytesIO(image_data)) image = image.resize((64, 64), Image.LANCZOS) photo = ImageTk.PhotoImage(image) - label = tk.Label(frame, image=photo) + label = tk.Label(frame, image=photo, bg=self.highlight_color) label.image = photo # Keep a reference label.pack() except Exception as e: print(f"Error displaying image: {e}") - tk.Label(frame, text="[No Image]", width=8, height=4).pack() + tk.Label(frame, text="[No Image]", width=8, height=4, bg=self.highlight_color, fg=self.fg_color).pack() else: - tk.Label(frame, text="[No Image]", width=8, height=4).pack() + tk.Label(frame, text="[No Image]", width=8, height=4, bg=self.highlight_color, fg=self.fg_color).pack() - tk.Label(frame, text=macro["name"]).pack() + tk.Label(frame, text=macro["name"], bg=self.highlight_color, fg=self.fg_color).pack() col += 1 if col >= max_cols: @@ -164,27 +193,55 @@ class MacroServer: # Create a dialog to add a new macro dialog = tk.Toplevel(self.root) dialog.title("Add Macro") - dialog.geometry("400x300") + dialog.geometry("450x350") # Increased height for additional options dialog.transient(self.root) + dialog.configure(bg=self.bg_color) - tk.Label(dialog, text="Macro Name:").grid(row=0, column=0, padx=5, pady=5, sticky="w") - name_entry = tk.Entry(dialog, width=30) + # 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:").grid(row=1, column=0, padx=5, pady=5, sticky="w") + 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") - 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) + 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) + # Key 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:").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.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w") + tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color).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")]))).grid(row=4, column=2) + filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2) def save_macro(): name = name_entry.get().strip() @@ -208,28 +265,214 @@ class MacroServer: except Exception as e: print(f"Error processing image: {e}") - # Create macro + # Create macro with modifier keys self.macros[macro_id] = { "name": name, "type": macro_type, "command": command, - "image_data": image_data + "image_data": image_data, + "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).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) + 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): - # To be implemented - similar to add_macro but pre-fills fields with existing data - pass + # 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_data = macro.get("image_data", "") + img_path = image_path.get() + if img_path: + try: + with open(img_path, "rb") as img_file: + image_data = base64.b64encode(img_file.read()).decode('utf-8') + 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_data": image_data, + "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): - # To be implemented - offers a selection and deletes the chosen macro - pass + # 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}'?"): + 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: @@ -238,7 +481,39 @@ class MacroServer: macro = self.macros[macro_id] try: if macro["type"] == "text": - pyautogui.typewrite(macro["command"]) + # Handle key modifiers + modifiers = macro.get("modifiers", {}) + keys_to_press = [] + + # Add modifier keys if enabled + if modifiers.get("ctrl", False): + keys_to_press.append('ctrl') + if modifiers.get("alt", False): + keys_to_press.append('alt') + if modifiers.get("shift", False): + keys_to_press.append('shift') + + # If there are modifier keys, use hotkey functionality + if keys_to_press: + # For single characters with modifiers, use hotkey + if len(macro["command"]) == 1: + keys_to_press.append(macro["command"], interval=0.02) + pyautogui.hotkey(*keys_to_press) + else: + # For longer text, press modifiers, type text, then release + for key in keys_to_press: + pyautogui.keyDown(key) + pyautogui.typewrite(macro["command"], interval=0.02) + for key in reversed(keys_to_press): + pyautogui.keyUp(key) + else: + # No modifiers, just type the text + pyautogui.typewrite(macro["command"], interval=0.02) + + # 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 @@ -270,38 +545,54 @@ class MacroServer: def handle_client(self, client_socket): try: while self.server_running: - data = client_socket.recv(1024).decode('utf-8') + data = b"" + while True: + chunk = client_socket.recv(4096) + if not chunk: + break + data += chunk + try: + # Try to see if we've received a complete JSON object + json_data = json.loads(data.decode('utf-8')) + break # If successful, break out of the inner loop + except json.JSONDecodeError: + # If it's not a complete JSON object yet, keep reading + continue + if not data: break try: - request = json.loads(data) + request = json_data if request['action'] == 'get_macros': # Send all macros to client client_socket.send(json.dumps(self.macros).encode('utf-8')) elif request['action'] == 'execute': # Execute the specified macro success = self.execute_macro(request['macro_id']) - client_socket.send(json.dumps({'success': success}).encode('utf-8')) - except json.JSONDecodeError: - print("Invalid JSON received") + response = {'success': success} + client_socket.send(json.dumps(response).encode('utf-8')) + else: + client_socket.send(json.dumps({'error': 'Unknown action'}).encode('utf-8')) + except (json.JSONDecodeError, KeyError, TypeError) as e: + error_msg = {'error': f'Invalid request: {str(e)}'} + client_socket.send(json.dumps(error_msg).encode('utf-8')) + print(f"JSON processing error: {e}, Data received: {data[:100]}") except Exception as e: - print(f"Error handling client: {e}") + print(f"Client handling error: {e}") finally: client_socket.close() + self.status_var.set("Client disconnected. Waiting for connections...") def on_closing(self): - # 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() + # When the window is closed, minimize to tray instead of closing + self.minimize_to_tray() + if __name__ == "__main__": - import io # Added for BytesIO - import tkinter.messagebox # Add this for message boxes - root = tk.Tk() - app = MacroServer(root) - root.mainloop() \ No newline at end of file + app = MacroPadServer(root) + root.mainloop() + +