MP-Server/main.py

500 lines
18 KiB
Python
Raw Normal View History

# Main application file for MacroPad Server
import tkinter as tk
from tkinter import ttk, messagebox
import os
import sys
import threading
import socket
import qrcode
import webbrowser
import pystray
from PIL import Image, ImageTk, ImageDraw, ImageFont
from config import VERSION, DEFAULT_PORT, THEME
from macro_manager import MacroManager
from web_server import WebServer
from ui_components import MacroDialog, MacroSelector, TabManager
class MacroPadServer:
def __init__(self, root):
self.root = root
self.root.title("MacroPad Server")
self.root.geometry("800x600")
self.configure_styles()
# 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__))
data_file = os.path.join(base_dir, "macros.json")
images_dir = os.path.join(base_dir, "macro_images")
os.makedirs(images_dir, exist_ok=True)
# Initialize components
self.macro_manager = MacroManager(data_file, images_dir, base_dir)
self.web_server = WebServer(self.macro_manager, base_dir, DEFAULT_PORT)
# UI state
self.current_sort = "name"
self.current_tab = "All"
self.image_cache = {}
# Server state
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 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"""
self.root.configure(bg=THEME['bg_color'])
style = ttk.Style()
style.theme_use("clam")
style.configure("TButton", background=THEME['button_bg'], foreground=THEME['button_fg'])
style.map("TButton", background=[("active", THEME['accent_color'])])
style.configure("TFrame", background=THEME['bg_color'])
style.configure("TLabel", background=THEME['bg_color'], foreground=THEME['fg_color'])
# Configure notebook (tabs) style
style.configure("TNotebook", background=THEME['bg_color'], borderwidth=0)
style.configure("TNotebook.Tab", background=THEME['tab_bg'], foreground=THEME['fg_color'],
padding=[12, 8], borderwidth=0)
style.map("TNotebook.Tab", background=[("selected", THEME['tab_selected'])],
foreground=[("selected", THEME['fg_color'])])
def create_ui(self):
"""Create the main user interface"""
# 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 controls
left_frame = ttk.Frame(main_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Sort controls
self._create_sort_controls(left_frame)
# Create notebook for tabs
self.notebook = ttk.Notebook(left_frame)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.notebook.bind("<<NotebookTabChanged>>", self.on_tab_change)
# Button controls
self._create_macro_buttons(left_frame)
# Right side: Server controls
right_frame = ttk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
self._create_server_controls(right_frame)
# Version label
version_label = tk.Label(self.root, text=VERSION,
bg=THEME['bg_color'], fg=THEME['fg_color'],
font=('Helvetica', 8))
version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
# Initialize display
self.setup_tabs()
self.display_macros()
def _create_sort_controls(self, parent):
"""Create sorting controls"""
sort_frame = ttk.Frame(parent)
sort_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(sort_frame, text="Sort by:").pack(side=tk.LEFT, padx=(0, 5))
self.sort_var = tk.StringVar(value=self.current_sort)
sort_combo = ttk.Combobox(sort_frame, textvariable=self.sort_var,
values=["name", "type", "recent"],
state="readonly", width=10)
sort_combo.pack(side=tk.LEFT, padx=(0, 10))
sort_combo.bind("<<ComboboxSelected>>", self.on_sort_change)
# Tab management button
ttk.Button(sort_frame, text="Manage Tabs",
command=self.manage_tabs).pack(side=tk.RIGHT, padx=(5, 0))
def _create_macro_buttons(self, parent):
"""Create macro management buttons"""
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(button_frame, text="Add Macro", command=self.add_macro).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro).pack(side=tk.LEFT, padx=2)
def _create_server_controls(self, parent):
"""Create web server controls"""
server_frame = ttk.Frame(parent)
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 display
self.qr_label = ttk.Label(parent)
self.qr_label.pack(pady=10)
# URL display
self.url_var = tk.StringVar(value="")
self.url_label = ttk.Label(parent, textvariable=self.url_var)
self.url_label.pack(pady=5)
# Browser button
self.browser_button = ttk.Button(parent, text="Open in Browser",
command=self.open_in_browser, state=tk.DISABLED)
self.browser_button.pack(pady=5)
def setup_tabs(self):
"""Initialize tabs based on macro categories"""
# Clear existing tabs
for tab in self.notebook.tabs():
self.notebook.forget(tab)
# Get unique tabs from macro manager
tabs = self.macro_manager.get_unique_tabs()
for tab_name in tabs:
frame = ttk.Frame(self.notebook)
self.notebook.add(frame, text=tab_name)
def get_current_tab_name(self):
"""Get the name of the currently selected tab"""
try:
current_tab_id = self.notebook.select()
return self.notebook.tab(current_tab_id, "text")
except:
return "All"
def on_tab_change(self, event=None):
"""Handle tab change event"""
self.current_tab = self.get_current_tab_name()
self.display_macros()
def on_sort_change(self, event=None):
"""Handle sort option change"""
self.current_sort = self.sort_var.get()
self.display_macros()
def display_macros(self):
"""Display macros in the current tab"""
# Get current tab frame
try:
current_tab_id = self.notebook.select()
current_frame = self.notebook.nametowidget(current_tab_id)
except:
return
# Clear previous content
for widget in current_frame.winfo_children():
widget.destroy()
# Create scrollable canvas
canvas = tk.Canvas(current_frame, bg=THEME['bg_color'], highlightthickness=0)
scrollbar = ttk.Scrollbar(current_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
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)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Get sorted and filtered macros
sorted_macros = self.macro_manager.get_sorted_macros(self.current_sort)
filtered_macros = self.macro_manager.filter_macros_by_tab(sorted_macros, self.current_tab)
# Display macros
for macro_id, macro in filtered_macros:
self._create_macro_button(scrollable_frame, macro_id, macro)
# Display message if no macros
if not filtered_macros:
label = tk.Label(scrollable_frame, text="No macros in this category",
bg=THEME['bg_color'], fg=THEME['fg_color'])
label.pack(pady=20)
def _create_macro_button(self, parent, macro_id, macro):
"""Create a button for a single macro"""
frame = ttk.Frame(parent)
frame.pack(fill="x", pady=5, padx=5)
button = tk.Button(
frame, text=macro["name"],
bg=THEME['button_bg'], fg=THEME['button_fg'],
activebackground=THEME['accent_color'], activeforeground=THEME['button_fg'],
relief=tk.RAISED, bd=2, pady=8,
command=lambda: self.macro_manager.execute_macro(macro_id)
)
# Add image if available
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.macro_manager.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 reference
except Exception as e:
print(f"Error loading image for {macro['name']}: {e}")
button.pack(side=tk.LEFT, fill=tk.X, expand=True)
def manage_tabs(self):
"""Open tab management dialog"""
tab_manager = TabManager(self.root, self.macro_manager)
tab_manager.show()
self.setup_tabs()
self.display_macros()
def add_macro(self):
"""Add a new macro"""
dialog = MacroDialog(self.root, self.macro_manager)
result = dialog.show()
if result:
self.setup_tabs()
self.display_macros()
def edit_macro(self):
"""Edit an existing macro"""
selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Edit")
macro_id = selector.show()
if macro_id:
dialog = MacroDialog(self.root, self.macro_manager, macro_id)
result = dialog.show()
if result:
self.setup_tabs()
self.display_macros()
def delete_macro(self):
"""Delete a macro"""
selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Delete")
macro_id = selector.show()
if macro_id:
macro_name = self.macro_manager.macros[macro_id]["name"]
if messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{macro_name}'?"):
self.macro_manager.delete_macro(macro_id)
self.setup_tabs()
self.display_macros()
def toggle_server(self):
"""Toggle web server on/off"""
if self.server_running:
self.stop_server()
else:
self.start_server()
def start_server(self):
"""Start the web server"""
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 IP address and display info
ip_address = self.get_ip_address()
if ip_address:
url = f"http://{ip_address}:{DEFAULT_PORT}"
url_text = f"Web UI available at:\n{url}"
self.url_var.set(url_text)
self.browser_button.config(state=tk.NORMAL)
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):
"""Stop the web server"""
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)
self.qr_label.config(image="")
def run_web_server(self):
"""Run the web server in a separate thread"""
self.status_var.set(f"Web server running on port {DEFAULT_PORT}")
try:
self.web_server.run()
except Exception as e:
self.status_var.set(f"Web server error: {e}")
self.server_running = False
def get_ip_address(self):
"""Get the primary internal IPv4 address"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception as e:
print(f"Error getting IP address: {e}")
try:
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
if not ip.startswith("127."):
return ip
for addr_info in socket.getaddrinfo(hostname, None):
potential_ip = addr_info[4][0]
if '.' in potential_ip and not potential_ip.startswith("127."):
return potential_ip
except:
pass
return "127.0.0.1"
def generate_qr_code(self, url):
"""Generate and display QR code for the URL"""
try:
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)
qr_img = qr.make_image(fill_color="black", back_color="white")
qr_photoimg = ImageTk.PhotoImage(qr_img)
self.qr_label.config(image=qr_photoimg)
self.qr_label.image = qr_photoimg
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 browser"""
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"""
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"""
self.root.deiconify()
self.root.state('normal')
self.root.lift()
self.root.focus_force()
def exit_app(self, icon=None, item=None):
"""Exit the application"""
self.is_closing = True
self.stop_server()
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 - exit the application"""
self.exit_app()
if __name__ == "__main__":
root = tk.Tk()
app = MacroPadServer(root)
root.mainloop()