""" Settings dialog. Built dynamically from app.config.SETTINGS_SCHEMA, grouped by section (Database, JIRA, ...). When a new service adds settings to that schema, they show up here automatically - no UI changes needed. The number of settings has grown a lot, so the group content lives in a QScrollArea and the dialog's height is capped to the actual screen's available space - Save/Cancel stay outside the scroll area, pinned at the bottom, so they're always reachable no matter how tall the content gets or how small the screen is. """ from __future__ import annotations from collections import defaultdict from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QFormLayout, QLineEdit, QPushButton, QHBoxLayout, QGroupBox, QLabel, QMessageBox, QScrollArea, QWidget, QApplication, ) from app import config class SettingsDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Settings") self._fields: dict[str, QLineEdit] = {} current_values = config.load_settings() # Group schema entries by their group label, preserving order groups: dict[str, list[tuple[str, str, bool]]] = defaultdict(list) for key, (label, group, is_secret) in config.SETTINGS_SCHEMA.items(): groups[group].append((key, label, is_secret)) outer_layout = QVBoxLayout(self) outer_layout.addWidget( QLabel("Changes are saved to your .env file and applied immediately.") ) # All the group boxes live inside a scrollable area, since the # full list no longer reliably fits on smaller screens. scroll_content = QWidget() content_layout = QVBoxLayout(scroll_content) for group_name, entries in groups.items(): box = QGroupBox(group_name) form = QFormLayout(box) for key, label, is_secret in entries: field = QLineEdit(current_values.get(key, "")) if is_secret: field.setEchoMode(QLineEdit.EchoMode.Password) form.addRow(label, field) self._fields[key] = field content_layout.addWidget(box) content_layout.addStretch() scroll_area = QScrollArea() scroll_area.setWidget(scroll_content) scroll_area.setWidgetResizable(True) outer_layout.addWidget(scroll_area, stretch=1) button_row = QHBoxLayout() save_button = QPushButton("Save") cancel_button = QPushButton("Cancel") save_button.clicked.connect(self._on_save) cancel_button.clicked.connect(self.reject) button_row.addStretch() button_row.addWidget(cancel_button) button_row.addWidget(save_button) outer_layout.addLayout(button_row) self._size_to_fit_screen() def _size_to_fit_screen(self) -> None: """Start at a comfortable size, but never taller/wider than the actual screen has room for - whatever's left just scrolls.""" width, height = 560, 720 screen = self.screen() or QApplication.primaryScreen() if screen is not None: available = screen.availableGeometry() # Leave a little breathing room so window chrome/taskbars don't # push Save off-screen either. height = min(height, available.height() - 80) width = min(width, available.width() - 80) self.resize(max(width, 400), max(height, 300)) def _on_save(self) -> None: values = {key: field.text().strip() for key, field in self._fields.items()} try: config.save_settings(values) except OSError as exc: QMessageBox.critical(self, "Couldn't save settings", str(exc)) return self.accept()