updating main app to resolve previous issues
Tabs are working correctly, and the app that minimizes to the tray
This commit is contained in:
parent
9f3484dff2
commit
896875ce90
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@ macros.json
|
||||
macro_images/
|
||||
macro_images/*
|
||||
build/
|
||||
.venv
|
||||
__pycache__
|
@ -1,6 +1,6 @@
|
||||
# Configuration and constants for MacroPad Server
|
||||
|
||||
VERSION = "0.5.4 Beta"
|
||||
VERSION = "0.7.5 Beta"
|
||||
DEFAULT_PORT = 40000
|
||||
|
||||
# UI Theme colors
|
||||
|
BIN
dist/mp-server-v2.exe → dist/main.exe
vendored
BIN
dist/mp-server-v2.exe → dist/main.exe
vendored
Binary file not shown.
86
main.py
86
main.py
@ -9,7 +9,7 @@ import socket
|
||||
import qrcode
|
||||
import webbrowser
|
||||
import pystray
|
||||
from PIL import Image, ImageTk
|
||||
from PIL import Image, ImageTk, ImageDraw, ImageFont
|
||||
|
||||
from config import VERSION, DEFAULT_PORT, THEME
|
||||
from macro_manager import MacroManager
|
||||
@ -47,11 +47,19 @@ class MacroPadServer:
|
||||
self.server_running = False
|
||||
self.flask_thread = None
|
||||
|
||||
# Tray state
|
||||
self.tray_icon = None
|
||||
self.is_closing = False
|
||||
|
||||
# Create UI
|
||||
self.create_ui()
|
||||
|
||||
# Set up window close handler
|
||||
# Set up window event handlers
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
self.root.bind('<Unmap>', self.on_minimize)
|
||||
|
||||
# Initialize tray icon
|
||||
self.create_tray_icon()
|
||||
|
||||
def configure_styles(self):
|
||||
"""Configure the dark theme styles"""
|
||||
@ -406,15 +414,53 @@ class MacroPadServer:
|
||||
if self.server_running:
|
||||
webbrowser.open(f"http://localhost:{DEFAULT_PORT}")
|
||||
|
||||
def on_minimize(self, event):
|
||||
"""Handle window minimize event"""
|
||||
# Only minimize to tray if the window is being iconified, not just unmapped
|
||||
if event.widget == self.root and self.root.state() == 'iconic':
|
||||
self.root.withdraw() # Hide window
|
||||
|
||||
def create_tray_icon(self):
|
||||
"""Create system tray icon"""
|
||||
icon_image = Image.new("RGB", (64, 64), THEME['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()
|
||||
try:
|
||||
# Create a simple icon image with M letter
|
||||
icon_image = Image.new("RGB", (64, 64), THEME['accent_color'])
|
||||
draw = ImageDraw.Draw(icon_image)
|
||||
|
||||
try:
|
||||
# Try to use a system font
|
||||
font = ImageFont.truetype("arial.ttf", 40)
|
||||
except:
|
||||
try:
|
||||
# Try other common fonts
|
||||
font = ImageFont.truetype("calibri.ttf", 40)
|
||||
except:
|
||||
# Fall back to default font
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Draw "M" in the center
|
||||
text = "M"
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
x = (64 - text_width) // 2
|
||||
y = (64 - text_height) // 2
|
||||
draw.text((x, y), text, fill="white", font=font)
|
||||
|
||||
menu = (
|
||||
pystray.MenuItem('Show', self.show_window),
|
||||
pystray.MenuItem('Exit', self.exit_app)
|
||||
)
|
||||
|
||||
self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu)
|
||||
|
||||
# Run tray icon in a separate thread
|
||||
tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
|
||||
tray_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating tray icon: {e}")
|
||||
# Tray icon is optional, continue without it
|
||||
|
||||
def show_window(self, icon=None, item=None):
|
||||
"""Show window from tray"""
|
||||
@ -425,14 +471,26 @@ class MacroPadServer:
|
||||
|
||||
def exit_app(self, icon=None, item=None):
|
||||
"""Exit the application"""
|
||||
self.is_closing = True
|
||||
self.stop_server()
|
||||
if hasattr(self, 'tray_icon'):
|
||||
self.tray_icon.stop()
|
||||
self.root.quit()
|
||||
|
||||
if self.tray_icon:
|
||||
try:
|
||||
self.tray_icon.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.root.quit()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Force exit if needed
|
||||
import os
|
||||
os._exit(0)
|
||||
|
||||
def on_closing(self):
|
||||
"""Handle window close event"""
|
||||
# For now just exit, but could minimize to tray
|
||||
"""Handle window close event - exit the application"""
|
||||
self.exit_app()
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['mp-server-v2.py'],
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
@ -22,7 +22,7 @@ exe = EXE(
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='mp-server-v2',
|
||||
name='main',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
File diff suppressed because it is too large
Load Diff
1176
mp-server-v2-new.py
1176
mp-server-v2-new.py
File diff suppressed because it is too large
Load Diff
971
mp-server-v2.py
971
mp-server-v2.py
@ -1,971 +0,0 @@
|
||||
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.4 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)
|
||||
pyautogui.keyUp(macro["command"])
|
||||
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()
|
@ -1 +1 @@
|
||||
0.5.4
|
||||
0.7.5
|
Loading…
x
Reference in New Issue
Block a user