added minimize to system tray
This commit is contained in:
parent
c897195083
commit
d74bedfa28
147
mp-server.py
147
mp-server.py
@ -8,62 +8,106 @@ import os
|
|||||||
import pyautogui
|
import pyautogui
|
||||||
import subprocess
|
import subprocess
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
import pystray # Add this import for system tray functionality
|
||||||
|
import io
|
||||||
|
|
||||||
class MacroServer:
|
class MacroServer:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("MacroPad Server")
|
self.root.title("MacroPad Server")
|
||||||
self.root.geometry("800x600")
|
self.root.geometry("800x600")
|
||||||
|
|
||||||
self.macros = {}
|
self.macros = {}
|
||||||
self.load_macros()
|
self.load_macros()
|
||||||
|
|
||||||
# Create GUI
|
# Create GUI
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
# Start server thread
|
# Start server thread
|
||||||
self.server_running = True
|
self.server_running = True
|
||||||
self.server_thread = threading.Thread(target=self.run_server)
|
self.server_thread = threading.Thread(target=self.run_server)
|
||||||
self.server_thread.daemon = True
|
self.server_thread.daemon = True
|
||||||
self.server_thread.start()
|
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):
|
def create_widgets(self):
|
||||||
# Toolbar
|
# Toolbar
|
||||||
toolbar = tk.Frame(self.root)
|
toolbar = tk.Frame(self.root)
|
||||||
toolbar.pack(side=tk.TOP, fill=tk.X)
|
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="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="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="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
|
# Macro list frame
|
||||||
list_frame = tk.Frame(self.root)
|
list_frame = tk.Frame(self.root)
|
||||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
# Scrollable canvas for macros
|
# Scrollable canvas for macros
|
||||||
self.canvas = tk.Canvas(list_frame)
|
self.canvas = tk.Canvas(list_frame)
|
||||||
scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=self.canvas.yview)
|
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)
|
||||||
|
|
||||||
self.scrollable_frame.bind(
|
self.scrollable_frame.bind(
|
||||||
"<Configure>",
|
"<Configure>",
|
||||||
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||||
)
|
)
|
||||||
|
|
||||||
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||||
self.canvas.configure(yscrollcommand=scrollbar.set)
|
self.canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
self.canvas.pack(side="left", fill="both", expand=True)
|
self.canvas.pack(side="left", fill="both", expand=True)
|
||||||
scrollbar.pack(side="right", fill="y")
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
self.display_macros()
|
self.display_macros()
|
||||||
|
|
||||||
# Status bar
|
# Status bar
|
||||||
self.status_var = tk.StringVar()
|
self.status_var = tk.StringVar()
|
||||||
self.status_var.set("Server ready. Waiting for connections...")
|
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)
|
||||||
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
||||||
|
|
||||||
def load_macros(self):
|
def load_macros(self):
|
||||||
try:
|
try:
|
||||||
if os.path.exists("macros.json"):
|
if os.path.exists("macros.json"):
|
||||||
@ -72,27 +116,27 @@ class MacroServer:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading macros: {e}")
|
print(f"Error loading macros: {e}")
|
||||||
self.macros = {}
|
self.macros = {}
|
||||||
|
|
||||||
def save_macros(self):
|
def save_macros(self):
|
||||||
try:
|
try:
|
||||||
with open("macros.json", "w") as f:
|
with open("macros.json", "w") as f:
|
||||||
json.dump(self.macros, f)
|
json.dump(self.macros, f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving macros: {e}")
|
print(f"Error saving macros: {e}")
|
||||||
|
|
||||||
def display_macros(self):
|
def display_macros(self):
|
||||||
# Clear existing macros
|
# Clear existing macros
|
||||||
for widget in self.scrollable_frame.winfo_children():
|
for widget in self.scrollable_frame.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
|
|
||||||
# Display macros in a grid
|
# Display macros in a grid
|
||||||
row, col = 0, 0
|
row, col = 0, 0
|
||||||
max_cols = 3
|
max_cols = 3
|
||||||
|
|
||||||
for macro_id, macro in self.macros.items():
|
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)
|
||||||
frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew")
|
frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew")
|
||||||
|
|
||||||
# Load image if exists
|
# Load image if exists
|
||||||
if "image_data" in macro and macro["image_data"]:
|
if "image_data" in macro and macro["image_data"]:
|
||||||
try:
|
try:
|
||||||
@ -108,52 +152,52 @@ class MacroServer:
|
|||||||
tk.Label(frame, text="[No Image]", width=8, height=4).pack()
|
tk.Label(frame, text="[No Image]", width=8, height=4).pack()
|
||||||
else:
|
else:
|
||||||
tk.Label(frame, text="[No Image]", width=8, height=4).pack()
|
tk.Label(frame, text="[No Image]", width=8, height=4).pack()
|
||||||
|
|
||||||
tk.Label(frame, text=macro["name"]).pack()
|
tk.Label(frame, text=macro["name"]).pack()
|
||||||
|
|
||||||
col += 1
|
col += 1
|
||||||
if col >= max_cols:
|
if col >= max_cols:
|
||||||
col = 0
|
col = 0
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
def add_macro(self):
|
def add_macro(self):
|
||||||
# Create a dialog to add a new macro
|
# Create a dialog to add a new macro
|
||||||
dialog = tk.Toplevel(self.root)
|
dialog = tk.Toplevel(self.root)
|
||||||
dialog.title("Add Macro")
|
dialog.title("Add Macro")
|
||||||
dialog.geometry("400x300")
|
dialog.geometry("400x300")
|
||||||
dialog.transient(self.root)
|
dialog.transient(self.root)
|
||||||
|
|
||||||
tk.Label(dialog, text="Macro Name:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
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 = tk.Entry(dialog, width=30)
|
||||||
name_entry.grid(row=0, column=1, padx=5, pady=5)
|
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:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
||||||
type_var = tk.StringVar(value="text")
|
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="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.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")
|
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 = tk.Text(dialog, width=30, height=5)
|
||||||
command_text.grid(row=3, column=1, padx=5, pady=5)
|
command_text.grid(row=3, column=1, padx=5, pady=5)
|
||||||
|
|
||||||
image_path = tk.StringVar()
|
image_path = tk.StringVar()
|
||||||
tk.Label(dialog, text="Image:").grid(row=4, column=0, padx=5, pady=5, sticky="w")
|
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.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(
|
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")]))).grid(row=4, column=2)
|
||||||
|
|
||||||
def save_macro():
|
def save_macro():
|
||||||
name = name_entry.get().strip()
|
name = name_entry.get().strip()
|
||||||
if not name:
|
if not name:
|
||||||
tk.messagebox.showerror("Error", "Macro name is required")
|
tk.messagebox.showerror("Error", "Macro name is required")
|
||||||
return
|
return
|
||||||
|
|
||||||
macro_type = type_var.get()
|
macro_type = type_var.get()
|
||||||
command = command_text.get("1.0", tk.END).strip()
|
command = command_text.get("1.0", tk.END).strip()
|
||||||
|
|
||||||
# Generate a unique ID
|
# Generate a unique ID
|
||||||
macro_id = str(len(self.macros) + 1)
|
macro_id = str(len(self.macros) + 1)
|
||||||
|
|
||||||
# Process image
|
# Process image
|
||||||
image_data = ""
|
image_data = ""
|
||||||
img_path = image_path.get()
|
img_path = image_path.get()
|
||||||
@ -163,7 +207,7 @@ class MacroServer:
|
|||||||
image_data = base64.b64encode(img_file.read()).decode('utf-8')
|
image_data = base64.b64encode(img_file.read()).decode('utf-8')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing image: {e}")
|
print(f"Error processing image: {e}")
|
||||||
|
|
||||||
# Create macro
|
# Create macro
|
||||||
self.macros[macro_id] = {
|
self.macros[macro_id] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
@ -171,26 +215,26 @@ class MacroServer:
|
|||||||
"command": command,
|
"command": command,
|
||||||
"image_data": image_data
|
"image_data": image_data
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save_macros()
|
self.save_macros()
|
||||||
self.display_macros()
|
self.display_macros()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
|
||||||
tk.Button(dialog, text="Save", command=save_macro).grid(row=5, column=0, padx=5, pady=20)
|
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="Cancel", command=dialog.destroy).grid(row=5, column=1, padx=5, pady=20)
|
||||||
|
|
||||||
def edit_macro(self):
|
def edit_macro(self):
|
||||||
# To be implemented - similar to add_macro but pre-fills fields with existing data
|
# To be implemented - similar to add_macro but pre-fills fields with existing data
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def delete_macro(self):
|
def delete_macro(self):
|
||||||
# To be implemented - offers a selection and deletes the chosen macro
|
# To be implemented - offers a selection and deletes the chosen macro
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def execute_macro(self, macro_id):
|
def execute_macro(self, macro_id):
|
||||||
if macro_id not in self.macros:
|
if macro_id not in self.macros:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
macro = self.macros[macro_id]
|
macro = self.macros[macro_id]
|
||||||
try:
|
try:
|
||||||
if macro["type"] == "text":
|
if macro["type"] == "text":
|
||||||
@ -201,35 +245,35 @@ class MacroServer:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error executing macro: {e}")
|
print(f"Error executing macro: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run_server(self):
|
def run_server(self):
|
||||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server_socket.bind(('0.0.0.0', 40000))
|
server_socket.bind(('0.0.0.0', 40000))
|
||||||
server_socket.listen(5)
|
server_socket.listen(5)
|
||||||
|
|
||||||
while self.server_running:
|
while self.server_running:
|
||||||
client_socket, address = server_socket.accept()
|
client_socket, address = server_socket.accept()
|
||||||
self.status_var.set(f"Client connected from {address}")
|
self.status_var.set(f"Client connected from {address}")
|
||||||
|
|
||||||
client_thread = threading.Thread(target=self.handle_client, args=(client_socket,))
|
client_thread = threading.Thread(target=self.handle_client, args=(client_socket,))
|
||||||
client_thread.daemon = True
|
client_thread.daemon = True
|
||||||
client_thread.start()
|
client_thread.start()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Server error: {e}")
|
print(f"Server error: {e}")
|
||||||
finally:
|
finally:
|
||||||
server_socket.close()
|
server_socket.close()
|
||||||
|
|
||||||
def handle_client(self, client_socket):
|
def handle_client(self, client_socket):
|
||||||
try:
|
try:
|
||||||
while self.server_running:
|
while self.server_running:
|
||||||
data = client_socket.recv(1024).decode('utf-8')
|
data = client_socket.recv(1024).decode('utf-8')
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request = json.loads(data)
|
request = json.loads(data)
|
||||||
if request['action'] == 'get_macros':
|
if request['action'] == 'get_macros':
|
||||||
@ -241,20 +285,23 @@ class MacroServer:
|
|||||||
client_socket.send(json.dumps({'success': success}).encode('utf-8'))
|
client_socket.send(json.dumps({'success': success}).encode('utf-8'))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("Invalid JSON received")
|
print("Invalid JSON received")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error handling client: {e}")
|
print(f"Error handling client: {e}")
|
||||||
finally:
|
finally:
|
||||||
client_socket.close()
|
client_socket.close()
|
||||||
|
|
||||||
def on_closing(self):
|
def on_closing(self):
|
||||||
self.server_running = False
|
# Instead of directly closing, ask if user wants to minimize to tray
|
||||||
self.save_macros()
|
if tk.messagebox.askyesno("Minimize to Tray", "Do you want to minimize to the system tray?"):
|
||||||
self.root.destroy()
|
self.minimize_to_tray()
|
||||||
|
else:
|
||||||
|
self.exit_app()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import io # Added for BytesIO
|
import io # Added for BytesIO
|
||||||
|
import tkinter.messagebox # Add this for message boxes
|
||||||
|
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
app = MacroServer(root)
|
app = MacroServer(root)
|
||||||
root.protocol("WM_DELETE_WINDOW", app.on_closing)
|
root.mainloop()
|
||||||
root.mainloop()
|
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pillow
|
||||||
|
pyautogui
|
||||||
|
pystray
|
Loading…
x
Reference in New Issue
Block a user