MP-Server/mp-server-v2.py

970 lines
37 KiB
Python

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import json
import os
import uuid
import pyautogui
import subprocess
import threading
import pystray
from PIL import Image, ImageTk
from flask import Flask, render_template_string, request, jsonify, send_file
import webbrowser
from waitress import serve
import logging
import socket
import qrcode
import sys
import time
class MacroPadServer:
def __init__(self, root):
self.root = root
self.root.title("MacroPad Server")
self.root.geometry("800x600")
self.configure_styles()
# Set Version Str
self.version_str = "0.5.3 Beta"
# 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__))
self.data_file = os.path.join(base_dir, "macros.json")
self.images_dir = os.path.join(base_dir, "macro_images")
self.app_dir = base_dir
os.makedirs(self.images_dir, exist_ok=True)
self.macros = {}
self.load_macros()
self.image_cache = {} # Cache for images
# Set up server
self.server_running = False
self.web_app = None
self.flask_thread = None
self.server_port = 40000
# Create UI
self.create_ui()
# Set up window close handler
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def configure_styles(self):
# Dark theme
self.bg_color = "#2e2e2e"
self.fg_color = "#ffffff"
self.highlight_color = "#3e3e3e"
self.accent_color = "#007acc"
self.button_bg = "#505050"
self.button_fg = "#ffffff"
self.root.configure(bg=self.bg_color)
# Configure ttk styles
style = ttk.Style()
style.theme_use("clam")
style.configure("TButton", background=self.button_bg, foreground=self.button_fg)
style.map("TButton", background=[("active", self.accent_color)])
style.configure("TFrame", background=self.bg_color)
style.configure("TLabel", background=self.bg_color, foreground=self.fg_color)
def create_ui(self):
# 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 buttons
left_frame = ttk.Frame(main_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Macro list
self.listbox_frame = ttk.Frame(left_frame)
self.listbox_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Right side: Details and server controls
right_frame = ttk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
#Set version details
version_label = tk.Label(self.root, text=self.version_str,
bg=self.bg_color, fg=self.fg_color,
font=('Helvetica', 8)) # Using smaller font for version text
version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
# Button container
button_frame = ttk.Frame(left_frame)
button_frame.pack(fill=tk.X, pady=(0, 10))
button_style = {'style': 'TButton'}
# Add buttons
ttk.Button(button_frame, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=2)
# Server controls
server_frame = ttk.Frame(right_frame)
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 placeholder
self.qr_label = ttk.Label(right_frame)
self.qr_label.pack(pady=10)
# Server URL display
self.url_var = tk.StringVar(value="")
self.url_label = ttk.Label(right_frame, textvariable=self.url_var)
self.url_label.pack(pady=5)
# Open in browser button
self.browser_button = ttk.Button(right_frame, text="Open in Browser",
command=self.open_in_browser, state=tk.DISABLED)
self.browser_button.pack(pady=5)
# Display any existing macros
self.display_macros()
def load_macros(self):
try:
if os.path.exists(self.data_file):
with open(self.data_file, "r") as file:
self.macros = json.load(file)
except Exception as e:
print(f"Error loading macros: {e}")
self.macros = {}
def save_macros(self):
try:
with open(self.data_file, "w") as file:
json.dump(self.macros, file, indent=4)
except Exception as e:
print(f"Error saving macros: {e}")
def display_macros(self):
# Clear the previous listbox
for widget in self.listbox_frame.winfo_children():
widget.destroy()
# Create scrollable canvas for macros
canvas = tk.Canvas(self.listbox_frame, bg=self.bg_color, highlightthickness=0)
scrollbar = ttk.Scrollbar(self.listbox_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
# Configure canvas and scrollable frame
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Pack canvas and scrollbar
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Display macros
row = 0
for macro_id, macro in self.macros.items():
frame = ttk.Frame(scrollable_frame)
frame.pack(fill="x", pady=5, padx=5)
# Create button with image if available
button_text = macro["name"]
button = tk.Button(
frame, text=button_text, bg=self.button_bg, fg=self.button_fg,
activebackground=self.accent_color, activeforeground=self.button_fg,
relief=tk.RAISED, bd=2, pady=8, command=lambda mid=macro_id: self.execute_macro(mid)
)
# Try to display image on button
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.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 a reference
except Exception as e:
print(f"Error loading image for {macro['name']}: {e}")
button.pack(side=tk.LEFT, fill=tk.X, expand=True)
row += 1
# Display message if no macros
if not self.macros:
label = tk.Label(scrollable_frame, text="No macros defined",
bg=self.bg_color, fg=self.fg_color)
label.pack(pady=20)
def toggle_server(self):
if self.server_running:
self.stop_server()
else:
self.start_server()
def start_server(self):
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 the systems internal IP address
ip_address = self.get_ip_addresses()
if ip_address:
# Set the URL display
url = f"http://{ip_address}:{self.server_port}"
url_text = "Web UI available at:\n" + "\n" + url
self.url_var.set(url_text)
# Enable browser button
self.browser_button.config(state=tk.NORMAL)
# Generate and display QR code for the first IP
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):
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)
# Clear QR code
self.qr_label.config(image="")
# The Flask server will be stopped on the next request
def get_ip_addresses(self):
"""Get the primary internal IPv4 address of the machine."""
try:
# Create a socket to connect to an external server
# This helps determine which network interface is used for outbound connections
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# We don't need to actually send data - just configure the socket
s.connect(("8.8.8.8", 80))
# Get the IP address that would be used for this connection
ip = s.getsockname()[0]
s.close()
return ip
except Exception as e:
print(f"Error getting IP address: {e}")
# Fallback method if the above doesn't work
try:
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
# Don't return localhost address
if ip.startswith("127."):
for addr_info in socket.getaddrinfo(hostname, None):
potential_ip = addr_info[4][0]
# If IPv4 and not localhost
if '.' in potential_ip and not potential_ip.startswith("127."):
return potential_ip
else:
return ip
except:
return "127.0.0.1" # Last resort fallback
def generate_qr_code(self, url):
try:
# Try to import qrcode
import qrcode
# Generate QR code
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)
# Create an image from the QR code
qr_img = qr.make_image(fill_color="black", back_color="white")
# Convert to PhotoImage for display
qr_photoimg = ImageTk.PhotoImage(qr_img)
# Update label
self.qr_label.config(image=qr_photoimg)
self.qr_label.image = qr_photoimg # Keep a reference
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 the default browser
if self.server_running:
webbrowser.open(f"http://localhost:{self.server_port}")
def create_web_app(self):
app = Flask(__name__)
# Disable Flask's logging except for errors
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
# Define HTML templates
index_html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MacroPad Web Interface</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #2e2e2e;
color: #ffffff;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
h1 {
color: #007acc;
margin: 0;
}
.refresh-button {
background-color: #505050;
color: white;
border: none;
border-radius: 8px;
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
}
.refresh-button:hover {
background-color: #007acc;
}
.refresh-button svg {
margin-right: 5px;
}
.macro-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.macro-button {
background-color: #505050;
color: white;
border: none;
border-radius: 8px;
padding: 15px 10px;
text-align: center;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100px;
}
.macro-button:hover, .macro-button:active {
background-color: #007acc;
}
.macro-button img {
max-width: 64px;
max-height: 64px;
margin-bottom: 10px;
}
.status {
text-align: center;
margin: 20px 0;
padding: 10px;
border-radius: 5px;
}
.success {
background-color: #4CAF50;
color: white;
display: none;
}
.error {
background-color: #f44336;
color: white;
display: none;
}
@media (max-width: 600px) {
.macro-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.macro-button {
padding: 10px 5px;
font-size: 14px;
}
h1 {
font-size: 24px;
}
.refresh-button {
padding: 8px 12px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="header-container">
<h1>MacroPad Web Interface</h1>
<button class="refresh-button" onclick="loadMacros()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
Refresh
</button>
</div>
<div class="status success" id="success-status">Macro executed successfully!</div>
<div class="status error" id="error-status">Failed to execute macro</div>
<div class="macro-grid" id="macro-grid">
<!-- Macros will be loaded here -->
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadMacros();
});
function loadMacros() {
fetch('/api/macros')
.then(response => response.json())
.then(macros => {
const macroGrid = document.getElementById('macro-grid');
macroGrid.innerHTML = '';
if (Object.keys(macros).length === 0) {
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros defined.</p>';
return;
}
for (const [macroId, macro] of Object.entries(macros)) {
const button = document.createElement('button');
button.className = 'macro-button';
button.onclick = function() { executeMacro(macroId); };
// Add image if available
if (macro.image_path) {
const img = document.createElement('img');
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
img.alt = macro.name;
img.onerror = function() {
this.style.display = 'none';
};
button.appendChild(img);
}
const text = document.createTextNode(macro.name);
button.appendChild(text);
macroGrid.appendChild(button);
}
})
.catch(error => {
console.error('Error loading macros:', error);
});
}
function executeMacro(macroId) {
fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ macro_id: macroId })
})
.then(response => response.json())
.then(data => {
const successStatus = document.getElementById('success-status');
const errorStatus = document.getElementById('error-status');
if (data.success) {
successStatus.style.display = 'block';
errorStatus.style.display = 'none';
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
} else {
errorStatus.style.display = 'block';
successStatus.style.display = 'none';
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
}
})
.catch(error => {
console.error('Error executing macro:', error);
const errorStatus = document.getElementById('error-status');
errorStatus.style.display = 'block';
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
});
}
</script>
</body>
</html>
'''
@app.route('/')
def index():
return render_template_string(index_html)
@app.route('/api/macros')
def get_macros():
return jsonify(self.macros)
@app.route('/api/image/<path:image_path>')
def get_image(image_path):
try:
# Using os.path.join would translate slashes incorrectly on Windows
# Use normpath instead
image_path = os.path.join(self.app_dir, image_path)
return send_file(image_path)
except Exception as e:
return str(e), 404
@app.route('/api/execute', methods=['POST'])
def execute_macro():
if not self.server_running:
return jsonify({"success": False, "error": "Server is shutting down"})
data = request.get_json()
if not data or 'macro_id' not in data:
return jsonify({"success": False, "error": "Invalid request"})
macro_id = data['macro_id']
success = self.execute_macro(macro_id)
return jsonify({"success": success})
return app
def run_web_server(self):
# Create Flask app
self.web_app = self.create_web_app()
# Update the status in the GUI
self.status_var.set(f"Web server running on port {self.server_port}")
# Run the Flask app with waitress for production-ready serving
try:
serve(self.web_app, host='0.0.0.0', port=self.server_port, threads=4)
except Exception as e:
self.status_var.set(f"Web server error: {e}")
self.server_running = False
def add_macro(self):
dialog = tk.Toplevel(self.root)
dialog.title("Add 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)
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")
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)
# 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:", 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_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()
macro_id = str(uuid.uuid4())
# Process image if provided
image_path_reference = ""
img_path = image_path.get()
if img_path:
try:
# Generate unique filename for the image
file_ext = os.path.splitext(img_path)[1].lower()
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
dest_path = os.path.join(self.images_dir, unique_filename)
# Resize image to max 256x256
with Image.open(img_path) as img:
img.thumbnail((256, 256))
img.save(dest_path)
# Store the relative path to the image
image_path_reference = os.path.join("macro_images", unique_filename)
except Exception as e:
print(f"Error processing image: {e}")
# Create macro with modifier keys
self.macros[macro_id] = {
"name": name,
"type": macro_type,
"command": command,
"image_path": image_path_reference,
"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, **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):
# 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_path_reference = macro.get("image_path", "")
img_path = image_path.get()
if img_path:
try:
# Generate unique filename for the image
file_ext = os.path.splitext(img_path)[1].lower()
unique_filename = f"{uuid.uuid4().hex}{file_ext}"
dest_path = os.path.join(self.images_dir, unique_filename)
# Resize image to max 256x256
with Image.open(img_path) as img:
img.thumbnail((256, 256))
img.save(dest_path)
# Store the relative path to the image
image_path_reference = os.path.join("macro_images", unique_filename)
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_path": image_path_reference,
"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):
# 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}'?"):
# Delete associated image file if it exists
macro = self.macros[selected_macro_id]
if "image_path" in macro and macro["image_path"]:
try:
img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"])
if os.path.exists(img_path):
os.remove(img_path)
except Exception as e:
print(f"Error removing image file: {e}")
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:
return False
macro = self.macros[macro_id]
try:
if macro["type"] == "text":
# Handle key modifiers
modifiers = macro.get("modifiers", {})
# Add modifier keys if enabled
if modifiers.get("ctrl", False):
pyautogui.keyDown('ctrl')
if modifiers.get("alt", False):
pyautogui.keyDown('alt')
if modifiers.get("shift", False):
pyautogui.keyDown('shift')
if str(macro["command"]) and len(str(macro["command"])) == 1:
pyautogui.keyDown(macro["command"])
time.sleep(0.5)
else:
pyautogui.typewrite(macro["command"], interval=0.02)
# Release modifier keys in reverse order
if modifiers.get("shift", False):
pyautogui.keyUp('shift')
if modifiers.get("alt", False):
pyautogui.keyUp('alt')
if modifiers.get("ctrl", False):
pyautogui.keyUp('ctrl')
# 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
except Exception as e:
print(f"Error executing macro: {e}")
return False
def create_tray_icon(self):
# Create tray icon
icon_image = Image.new("RGB", (64, 64), self.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 the 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):
# Actually exit the app
self.stop_server()
self.server_running = False
if hasattr(self, 'tray_icon'):
self.tray_icon.stop()
self.root.quit()
def minimize_to_tray(self):
# Create tray icon if it doesn't exist
if not hasattr(self, 'tray_icon'):
self.create_tray_icon()
self.root.withdraw()
def on_closing(self):
# When the window is closed, minimize to tray instead of closing
self.minimize_to_tray()
if __name__ == "__main__":
root = tk.Tk()
app = MacroPadServer(root)
root.mainloop()