Modernize application to v0.9.0 with PySide6, FastAPI, and PWA support
## Major Changes ### Build System - Replace requirements.txt with pyproject.toml for modern dependency management - Support for uv package manager alongside pip - Update PyInstaller spec files for new dependencies and structure ### Desktop GUI (Tkinter → PySide6) - Complete rewrite of UI using PySide6/Qt6 - New modular structure in gui/ directory: - main_window.py: Main application window - macro_editor.py: Macro creation/editing dialog - command_builder.py: Visual command sequence builder - Modern dark theme with consistent styling - System tray integration ### Web Server (Flask → FastAPI) - Migrate from Flask/Waitress to FastAPI/Uvicorn - Add WebSocket support for real-time updates - Full CRUD API for macro management - Image upload endpoint ### Web Interface → PWA - New web/ directory with standalone static files - PWA manifest and service worker for installability - Offline caching support - Full macro editing from web interface - Responsive mobile-first design - Command builder UI matching desktop functionality ### Macro System Enhancement - New command sequence model replacing simple text/app types - Command types: text, key, hotkey, wait, app - Support for delays between commands (wait in ms) - Support for key presses between commands (enter, tab, etc.) - Automatic migration of existing macros to new format - Backward compatibility maintained ### Files Added - pyproject.toml - gui/__init__.py, main_window.py, macro_editor.py, command_builder.py - gui/widgets/__init__.py - web/index.html, manifest.json, service-worker.js - web/css/styles.css, web/js/app.js - web/icons/icon-192.png, icon-512.png ### Files Removed - requirements.txt (replaced by pyproject.toml) - ui_components.py (replaced by gui/ modules) - web_templates.py (replaced by web/ static files) - main.spec (consolidated into platform-specific specs) ### Files Modified - main.py: Simplified entry point for PySide6 - macro_manager.py: Command sequence model and migration - web_server.py: FastAPI implementation - config.py: Version bump to 0.9.0 - All .spec files: Updated for PySide6 and new structure - README.md: Complete rewrite for v0.9.0 - .gitea/workflows/release.yml: Disabled pending build testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
520
main.py
520
main.py
@@ -1,502 +1,52 @@
|
||||
# Main application file for MacroPad Server
|
||||
# PySide6 version
|
||||
|
||||
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
|
||||
import multiprocessing
|
||||
|
||||
|
||||
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)
|
||||
def get_app_dir():
|
||||
"""Get the application directory."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as compiled executable
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# Running as script
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 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 main():
|
||||
"""Main entry point."""
|
||||
# Required for multiprocessing on Windows
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
def configure_styles(self):
|
||||
"""Configure the dark theme styles"""
|
||||
self.root.configure(bg=THEME['bg_color'])
|
||||
# Get app directory
|
||||
app_dir = get_app_dir()
|
||||
|
||||
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'])],
|
||||
padding=[("selected", [12, 8])]) # Keep same padding when selected
|
||||
# Import PySide6 after freeze_support
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
|
||||
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)
|
||||
# Create application
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("MacroPad Server")
|
||||
app.setOrganizationName("MacroPad")
|
||||
|
||||
# Left side: Macro list and controls
|
||||
left_frame = ttk.Frame(main_frame)
|
||||
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
# Set application icon
|
||||
icon_path = os.path.join(app_dir, "Macro Pad.png")
|
||||
if os.path.exists(icon_path):
|
||||
app.setWindowIcon(QIcon(icon_path))
|
||||
|
||||
# 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)
|
||||
# Import and create main window
|
||||
from gui.main_window import MainWindow
|
||||
window = MainWindow(app_dir)
|
||||
window.show()
|
||||
|
||||
# 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 "MP" in the center
|
||||
text = "MP"
|
||||
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()
|
||||
# Run application
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = MacroPadServer(root)
|
||||
root.mainloop()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user