added minimize to system tray

This commit is contained in:
jknapp 2025-03-14 22:48:34 -07:00
parent c897195083
commit d74bedfa28
2 changed files with 100 additions and 50 deletions

View File

@ -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("<Unmap>", 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(
"<Configure>",
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()

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
pillow
pyautogui
pystray