# Main window for MacroPad Server (PySide6) import os import sys import threading from typing import Optional from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTabWidget, QGridLayout, QScrollArea, QFrame, QMenu, QMenuBar, QStatusBar, QMessageBox, QApplication, QSystemTrayIcon ) from PySide6.QtCore import Qt, Signal, QTimer, QSize from PySide6.QtGui import QIcon, QPixmap, QAction, QFont from config import VERSION, THEME, DEFAULT_PORT from macro_manager import MacroManager from web_server import WebServer class MacroButton(QPushButton): """Custom button widget for displaying a macro.""" def __init__(self, macro_id: str, macro: dict, parent=None): super().__init__(parent) self.macro_id = macro_id self.macro = macro self.setFixedSize(120, 100) self.setCursor(Qt.PointingHandCursor) self.setStyleSheet(f""" QPushButton {{ background-color: {THEME['button_bg']}; color: {THEME['fg_color']}; border: none; border-radius: 8px; padding: 8px; text-align: center; }} QPushButton:hover {{ background-color: {THEME['highlight_color']}; }} QPushButton:pressed {{ background-color: {THEME['accent_color']}; }} """) # Layout layout = QVBoxLayout(self) layout.setSpacing(4) layout.setContentsMargins(4, 4, 4, 4) # Image or placeholder image_label = QLabel() image_label.setFixedSize(48, 48) image_label.setAlignment(Qt.AlignCenter) if macro.get("image_path"): pixmap = QPixmap(macro["image_path"]) if not pixmap.isNull(): pixmap = pixmap.scaled(48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation) image_label.setPixmap(pixmap) else: self._set_placeholder(image_label, macro["name"]) else: self._set_placeholder(image_label, macro["name"]) layout.addWidget(image_label, alignment=Qt.AlignCenter) # Name label name_label = QLabel(macro["name"]) name_label.setAlignment(Qt.AlignCenter) name_label.setWordWrap(True) name_label.setStyleSheet(f"color: {THEME['fg_color']}; font-size: 11px;") layout.addWidget(name_label) def _set_placeholder(self, label: QLabel, name: str): """Set a placeholder with the first letter of the name.""" label.setStyleSheet(f""" background-color: {THEME['highlight_color']}; border-radius: 8px; font-size: 20px; font-weight: bold; color: {THEME['fg_color']}; """) label.setText(name[0].upper() if name else "?") def contextMenuEvent(self, event): """Show context menu on right-click.""" menu = QMenu(self) edit_action = menu.addAction("Edit") delete_action = menu.addAction("Delete") action = menu.exec_(event.globalPos()) if action == edit_action: self.parent().parent().parent().parent().edit_macro(self.macro_id) elif action == delete_action: self.parent().parent().parent().parent().delete_macro(self.macro_id) class MainWindow(QMainWindow): """Main application window.""" macros_changed = Signal() def __init__(self, app_dir: str): super().__init__() self.app_dir = app_dir self.current_tab = "All" self.sort_by = "name" # Initialize macro manager data_file = os.path.join(app_dir, "macros.json") images_dir = os.path.join(app_dir, "macro_images") os.makedirs(images_dir, exist_ok=True) self.macro_manager = MacroManager(data_file, images_dir, app_dir) # Initialize web server self.web_server = WebServer(self.macro_manager, app_dir, DEFAULT_PORT) self.server_thread = None # Setup UI self.setup_ui() self.setup_menu() self.setup_tray() # Start web server self.start_server() # Connect signals self.macros_changed.connect(self.refresh_macros) # Load initial data self.refresh_tabs() self.refresh_macros() def setup_ui(self): """Setup the main UI components.""" self.setWindowTitle(f"MacroPad Server v{VERSION}") self.setMinimumSize(600, 400) self.setStyleSheet(f"background-color: {THEME['bg_color']};") # Central widget central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) # Toolbar toolbar = QWidget() toolbar.setStyleSheet(f"background-color: {THEME['highlight_color']};") toolbar_layout = QHBoxLayout(toolbar) toolbar_layout.setContentsMargins(10, 10, 10, 10) add_btn = QPushButton("+ Add Macro") add_btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['accent_color']}; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; }} QPushButton:hover {{ background-color: #0096ff; }} """) add_btn.clicked.connect(self.add_macro) toolbar_layout.addWidget(add_btn) toolbar_layout.addStretch() # IP address label self.ip_label = QLabel() self.ip_label.setStyleSheet(f"color: {THEME['fg_color']};") self.update_ip_label() toolbar_layout.addWidget(self.ip_label) qr_btn = QPushButton("QR Code") qr_btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['button_bg']}; color: white; border: none; padding: 8px 16px; border-radius: 4px; }} QPushButton:hover {{ background-color: {THEME['highlight_color']}; }} """) qr_btn.clicked.connect(self.show_qr_code) toolbar_layout.addWidget(qr_btn) layout.addWidget(toolbar) # Tab widget self.tab_widget = QTabWidget() self.tab_widget.setStyleSheet(f""" QTabWidget::pane {{ border: none; background: {THEME['bg_color']}; }} QTabBar::tab {{ background: {THEME['tab_bg']}; color: {THEME['fg_color']}; padding: 8px 16px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; }} QTabBar::tab:selected {{ background: {THEME['tab_selected']}; }} QTabBar::tab:hover {{ background: {THEME['highlight_color']}; }} """) self.tab_widget.currentChanged.connect(self.on_tab_changed) layout.addWidget(self.tab_widget) # Status bar self.status_bar = QStatusBar() self.status_bar.setStyleSheet(f""" background-color: {THEME['highlight_color']}; color: {THEME['fg_color']}; """) self.setStatusBar(self.status_bar) self.status_bar.showMessage("Ready") def setup_menu(self): """Setup the menu bar.""" menubar = self.menuBar() menubar.setStyleSheet(f""" QMenuBar {{ background-color: {THEME['highlight_color']}; color: {THEME['fg_color']}; }} QMenuBar::item:selected {{ background-color: {THEME['accent_color']}; }} QMenu {{ background-color: {THEME['highlight_color']}; color: {THEME['fg_color']}; }} QMenu::item:selected {{ background-color: {THEME['accent_color']}; }} """) # File menu file_menu = menubar.addMenu("File") add_action = QAction("Add Macro", self) add_action.setShortcut("Ctrl+N") add_action.triggered.connect(self.add_macro) file_menu.addAction(add_action) file_menu.addSeparator() quit_action = QAction("Quit", self) quit_action.setShortcut("Ctrl+Q") quit_action.triggered.connect(self.close) file_menu.addAction(quit_action) # View menu view_menu = menubar.addMenu("View") refresh_action = QAction("Refresh", self) refresh_action.setShortcut("F5") refresh_action.triggered.connect(self.refresh_all) view_menu.addAction(refresh_action) # Sort submenu sort_menu = view_menu.addMenu("Sort By") for sort_option in [("Name", "name"), ("Type", "type"), ("Recent", "recent")]: action = QAction(sort_option[0], self) action.triggered.connect(lambda checked, s=sort_option[1]: self.set_sort(s)) sort_menu.addAction(action) # Help menu help_menu = menubar.addMenu("Help") about_action = QAction("About", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def setup_tray(self): """Setup system tray icon.""" self.tray_icon = QSystemTrayIcon(self) # Load icon icon_path = os.path.join(self.app_dir, "Macro Pad.png") if os.path.exists(icon_path): self.tray_icon.setIcon(QIcon(icon_path)) self.setWindowIcon(QIcon(icon_path)) else: self.tray_icon.setIcon(self.style().standardIcon(self.style().SP_ComputerIcon)) # Tray menu tray_menu = QMenu() show_action = tray_menu.addAction("Show") show_action.triggered.connect(self.show) quit_action = tray_menu.addAction("Quit") quit_action.triggered.connect(self.close) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.activated.connect(self.on_tray_activated) self.tray_icon.show() def on_tray_activated(self, reason): """Handle tray icon activation.""" if reason == QSystemTrayIcon.DoubleClick: self.show() self.activateWindow() def start_server(self): """Start the web server in a background thread.""" self.web_server.create_app() def run(): self.web_server.run() self.server_thread = threading.Thread(target=run, daemon=True) self.server_thread.start() self.status_bar.showMessage(f"Server running on port {DEFAULT_PORT}") def update_ip_label(self): """Update the IP address label.""" try: import netifaces for iface in netifaces.interfaces(): addrs = netifaces.ifaddresses(iface) if netifaces.AF_INET in addrs: for addr in addrs[netifaces.AF_INET]: ip = addr.get('addr', '') if ip and not ip.startswith('127.'): self.ip_label.setText(f"http://{ip}:{DEFAULT_PORT}") return except Exception: pass self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}") def refresh_tabs(self): """Refresh the tab widget.""" self.tab_widget.blockSignals(True) self.tab_widget.clear() tabs = self.macro_manager.get_unique_tabs() for tab_name in tabs: scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet(f"background-color: {THEME['bg_color']}; border: none;") container = QWidget() container.setStyleSheet(f"background-color: {THEME['bg_color']};") scroll.setWidget(container) self.tab_widget.addTab(scroll, tab_name) self.tab_widget.blockSignals(False) def refresh_macros(self): """Refresh the macro grid.""" current_index = self.tab_widget.currentIndex() if current_index < 0: return scroll = self.tab_widget.widget(current_index) container = scroll.widget() # Clear existing layout if container.layout(): while container.layout().count(): item = container.layout().takeAt(0) if item.widget(): item.widget().deleteLater() else: container.setLayout(QGridLayout()) layout = container.layout() layout.setSpacing(10) layout.setContentsMargins(10, 10, 10, 10) layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) # Get macros for current tab tab_name = self.tab_widget.tabText(current_index) macro_list = self.macro_manager.get_sorted_macros(self.sort_by) filtered = self.macro_manager.filter_macros_by_tab(macro_list, tab_name) # Add macro buttons cols = max(1, (self.width() - 40) // 130) for i, (macro_id, macro) in enumerate(filtered): btn = MacroButton(macro_id, macro) btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid)) layout.addWidget(btn, i // cols, i % cols) def on_tab_changed(self, index): """Handle tab change.""" self.refresh_macros() def execute_macro(self, macro_id: str): """Execute a macro.""" success = self.macro_manager.execute_macro(macro_id) if success: self.status_bar.showMessage("Macro executed", 2000) else: self.status_bar.showMessage("Macro execution failed", 2000) def add_macro(self): """Open dialog to add a new macro.""" from .macro_editor import MacroEditorDialog dialog = MacroEditorDialog(self.macro_manager, parent=self) if dialog.exec_(): self.refresh_tabs() self.refresh_macros() def edit_macro(self, macro_id: str): """Open dialog to edit a macro.""" from .macro_editor import MacroEditorDialog dialog = MacroEditorDialog(self.macro_manager, macro_id=macro_id, parent=self) if dialog.exec_(): self.refresh_tabs() self.refresh_macros() def delete_macro(self, macro_id: str): """Delete a macro with confirmation.""" reply = QMessageBox.question( self, "Delete Macro", "Are you sure you want to delete this macro?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.macro_manager.delete_macro(macro_id) self.refresh_tabs() self.refresh_macros() def set_sort(self, sort_by: str): """Set the sort order.""" self.sort_by = sort_by self.refresh_macros() def refresh_all(self): """Refresh tabs and macros.""" self.refresh_tabs() self.refresh_macros() def show_qr_code(self): """Show QR code dialog.""" try: import qrcode from PySide6.QtWidgets import QDialog, QVBoxLayout from io import BytesIO url = self.ip_label.text() qr = qrcode.QRCode(version=1, box_size=10, border=4) qr.add_data(url) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") # Convert to QPixmap buffer = BytesIO() img.save(buffer, format="PNG") buffer.seek(0) pixmap = QPixmap() pixmap.loadFromData(buffer.read()) # Show dialog dialog = QDialog(self) dialog.setWindowTitle("QR Code") layout = QVBoxLayout(dialog) label = QLabel() label.setPixmap(pixmap.scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)) layout.addWidget(label) url_label = QLabel(url) url_label.setStyleSheet(f"color: {THEME['fg_color']};") url_label.setAlignment(Qt.AlignCenter) layout.addWidget(url_label) dialog.exec_() except ImportError: QMessageBox.warning(self, "Error", "QR code library not available") def show_about(self): """Show about dialog.""" QMessageBox.about( self, "About MacroPad Server", f"MacroPad Server v{VERSION}\n\n" "A cross-platform macro management application\n" "with desktop and web interfaces.\n\n" "PWA-enabled for mobile devices." ) def closeEvent(self, event): """Handle window close.""" self.tray_icon.hide() event.accept() def resizeEvent(self, event): """Handle window resize.""" super().resizeEvent(event) self.refresh_macros()