# Settings Dialog for MacroPad Server from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QLabel, QLineEdit, QCheckBox, QPushButton, QGroupBox, QFormLayout, QMessageBox ) from PySide6.QtCore import Qt, Signal from config import THEME class SettingsDialog(QDialog): """Settings dialog for application preferences.""" relay_settings_changed = Signal() def __init__(self, settings_manager, parent=None): super().__init__(parent) self.settings_manager = settings_manager self.setup_ui() self.load_settings() def setup_ui(self): """Setup the dialog UI.""" self.setWindowTitle("Settings") self.setMinimumSize(500, 400) self.setStyleSheet(f""" QDialog {{ background-color: {THEME['bg_color']}; color: {THEME['fg_color']}; }} QTabWidget::pane {{ border: 1px solid {THEME['highlight_color']}; background: {THEME['bg_color']}; }} QTabBar::tab {{ background: {THEME['tab_bg']}; color: {THEME['fg_color']}; padding: 8px 16px; margin-right: 2px; }} QTabBar::tab:selected {{ background: {THEME['tab_selected']}; }} QGroupBox {{ border: 1px solid {THEME['highlight_color']}; border-radius: 4px; margin-top: 12px; padding-top: 8px; color: {THEME['fg_color']}; }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} QLabel {{ color: {THEME['fg_color']}; }} QLineEdit {{ background-color: {THEME['highlight_color']}; color: {THEME['fg_color']}; border: 1px solid {THEME['button_bg']}; border-radius: 4px; padding: 6px; }} QLineEdit:focus {{ border-color: {THEME['accent_color']}; }} QLineEdit:read-only {{ background-color: {THEME['bg_color']}; }} QCheckBox {{ color: {THEME['fg_color']}; }} QCheckBox::indicator {{ width: 18px; height: 18px; }} QPushButton {{ background-color: {THEME['button_bg']}; color: {THEME['fg_color']}; border: none; padding: 8px 16px; border-radius: 4px; }} QPushButton:hover {{ background-color: {THEME['highlight_color']}; }} QPushButton:pressed {{ background-color: {THEME['accent_color']}; }} """) layout = QVBoxLayout(self) # Tab widget self.tab_widget = QTabWidget() # General tab general_tab = self.create_general_tab() self.tab_widget.addTab(general_tab, "General") # Relay Server tab relay_tab = self.create_relay_tab() self.tab_widget.addTab(relay_tab, "Relay Server") layout.addWidget(self.tab_widget) # Buttons button_layout = QHBoxLayout() button_layout.addStretch() cancel_btn = QPushButton("Cancel") cancel_btn.clicked.connect(self.reject) button_layout.addWidget(cancel_btn) save_btn = QPushButton("Save") save_btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['accent_color']}; color: white; }} QPushButton:hover {{ background-color: #0096ff; }} """) save_btn.clicked.connect(self.save_settings) button_layout.addWidget(save_btn) layout.addLayout(button_layout) def create_general_tab(self) -> QWidget: """Create the general settings tab.""" tab = QWidget() layout = QVBoxLayout(tab) # Behavior group behavior_group = QGroupBox("Behavior") behavior_layout = QVBoxLayout(behavior_group) self.minimize_to_tray_cb = QCheckBox("Minimize to system tray") self.minimize_to_tray_cb.setToolTip( "When enabled, minimizing the window will hide it to the system tray\n" "instead of the taskbar. Double-click the tray icon to restore." ) behavior_layout.addWidget(self.minimize_to_tray_cb) layout.addWidget(behavior_group) layout.addStretch() return tab def create_relay_tab(self) -> QWidget: """Create the relay server settings tab.""" tab = QWidget() layout = QVBoxLayout(tab) # Info label info_label = QLabel( "The relay server allows you to access MacroPad from anywhere\n" "via a secure HTTPS connection. Your macros will be accessible\n" "through a unique URL that you can share with your devices." ) info_label.setWordWrap(True) info_label.setStyleSheet(f"color: #aaa; margin-bottom: 10px;") layout.addWidget(info_label) # Connection group connection_group = QGroupBox("Connection") connection_layout = QFormLayout(connection_group) self.relay_enabled_cb = QCheckBox("Enable relay server") self.relay_enabled_cb.stateChanged.connect(self.on_relay_enabled_changed) connection_layout.addRow("", self.relay_enabled_cb) self.relay_url_edit = QLineEdit() self.relay_url_edit.setPlaceholderText("wss://relay.example.com") connection_layout.addRow("Server URL:", self.relay_url_edit) layout.addWidget(connection_group) # Authentication group auth_group = QGroupBox("Authentication") auth_layout = QFormLayout(auth_group) self.relay_password_edit = QLineEdit() self.relay_password_edit.setEchoMode(QLineEdit.Password) self.relay_password_edit.setPlaceholderText("Required - minimum 8 characters") auth_layout.addRow("Password:", self.relay_password_edit) # Show password toggle show_password_cb = QCheckBox("Show password") show_password_cb.stateChanged.connect( lambda state: self.relay_password_edit.setEchoMode( QLineEdit.Normal if state else QLineEdit.Password ) ) auth_layout.addRow("", show_password_cb) layout.addWidget(auth_group) # Status group status_group = QGroupBox("Status") status_layout = QFormLayout(status_group) self.relay_session_id_edit = QLineEdit() self.relay_session_id_edit.setReadOnly(True) self.relay_session_id_edit.setPlaceholderText("Not connected") status_layout.addRow("Session ID:", self.relay_session_id_edit) self.relay_full_url_edit = QLineEdit() self.relay_full_url_edit.setReadOnly(True) self.relay_full_url_edit.setPlaceholderText("Connect to see your URL") status_layout.addRow("Your URL:", self.relay_full_url_edit) # Regenerate button regen_btn = QPushButton("Generate New URL") regen_btn.setToolTip("Generate a new unique URL (invalidates the old one)") regen_btn.clicked.connect(self.regenerate_session_id) status_layout.addRow("", regen_btn) layout.addWidget(status_group) layout.addStretch() return tab def load_settings(self): """Load current settings into the UI.""" # General self.minimize_to_tray_cb.setChecked( self.settings_manager.get_minimize_to_tray() ) # Relay self.relay_enabled_cb.setChecked( self.settings_manager.get_relay_enabled() ) self.relay_url_edit.setText( self.settings_manager.get_relay_url() ) self.relay_password_edit.setText( self.settings_manager.get_relay_password() ) session_id = self.settings_manager.get_relay_session_id() if session_id: self.relay_session_id_edit.setText(session_id) base_url = self.relay_url_edit.text().replace('wss://', 'https://').replace('ws://', 'http://') base_url = base_url.replace('/desktop', '') self.relay_full_url_edit.setText(f"{base_url}/{session_id}") self.on_relay_enabled_changed() def on_relay_enabled_changed(self): """Handle relay enabled checkbox change.""" enabled = self.relay_enabled_cb.isChecked() self.relay_url_edit.setEnabled(enabled) self.relay_password_edit.setEnabled(enabled) def validate_settings(self) -> bool: """Validate settings before saving.""" if self.relay_enabled_cb.isChecked(): url = self.relay_url_edit.text().strip() password = self.relay_password_edit.text() if not url: QMessageBox.warning( self, "Validation Error", "Relay server URL is required when relay is enabled." ) return False if not url.startswith(('ws://', 'wss://')): QMessageBox.warning( self, "Validation Error", "Relay server URL must start with ws:// or wss://" ) return False if len(password) < 8: QMessageBox.warning( self, "Validation Error", "Password must be at least 8 characters." ) return False return True def save_settings(self): """Save settings and close dialog.""" if not self.validate_settings(): return # General self.settings_manager.set( 'minimize_to_tray', self.minimize_to_tray_cb.isChecked() ) # Relay - check if settings changed old_enabled = self.settings_manager.get_relay_enabled() old_url = self.settings_manager.get_relay_url() old_password = self.settings_manager.get_relay_password() new_enabled = self.relay_enabled_cb.isChecked() new_url = self.relay_url_edit.text().strip() new_password = self.relay_password_edit.text() relay_changed = ( old_enabled != new_enabled or old_url != new_url or old_password != new_password ) self.settings_manager.set('relay.enabled', new_enabled) self.settings_manager.set('relay.server_url', new_url) self.settings_manager.set('relay.password', new_password) self.settings_manager.save() if relay_changed: self.relay_settings_changed.emit() self.accept() def regenerate_session_id(self): """Clear session ID to force regeneration on next connect.""" reply = QMessageBox.question( self, "Regenerate URL", "This will generate a new URL. The old URL will stop working.\n\n" "Are you sure you want to continue?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.settings_manager.set('relay.session_id', None) self.relay_session_id_edit.setText("") self.relay_full_url_edit.setText("") self.settings_manager.save() QMessageBox.information( self, "URL Regenerated", "A new URL will be generated when you reconnect to the relay server." )