From 0d8e18430da9afba258d92e8c7081a24260c7d8d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 3 Apr 2026 18:43:51 +0000
Subject: [PATCH 1/2] Initial plan
From ce46f49fe376cc36185c0c0ea6a9006d1e702777 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 3 Apr 2026 18:54:30 +0000
Subject: [PATCH 2/2] feat: implement Preset Load Functionality
- Add PresetLoadDialog in src/ui/dialogs/preset_load_dialog.py
- Lists all available presets with metadata display panel
- Load button enabled only when a preset is selected
- Double-click to load; validates before accepting
- Shows warning on invalid/corrupt presets
- Export PresetLoadDialog from src/ui/dialogs/__init__.py
- Add Load Preset button to ProjectInfoTab Quick Start section
- Add Load Preset (Ctrl+L) to MainWindow File menu
- Add MainWindow.show_load_preset_dialog() slot that loads
and applies preset config to all tabs
- Add 18 tests in tests/test_preset_load_dialog.py covering
init, list population, metadata display, accept/reject, and
MainWindow integration
Agent-Logs-Url: https://github.com/DirektDSP/PluginConfiguratorApp/sessions/38d99418-2b8e-4e75-8fe4-c5a754db74e2
Co-authored-by: SeamusMullan <43112447+SeamusMullan@users.noreply.github.com>
---
src/ui/dialogs/__init__.py | 3 +-
src/ui/dialogs/preset_load_dialog.py | 200 ++++++++++++++
src/ui/main_window.py | 31 ++-
src/ui/tabs/project_info_tab.py | 13 +
tests/test_preset_load_dialog.py | 374 +++++++++++++++++++++++++++
5 files changed, 619 insertions(+), 2 deletions(-)
create mode 100644 src/ui/dialogs/preset_load_dialog.py
create mode 100644 tests/test_preset_load_dialog.py
diff --git a/src/ui/dialogs/__init__.py b/src/ui/dialogs/__init__.py
index 70a311f..e9b66e3 100644
--- a/src/ui/dialogs/__init__.py
+++ b/src/ui/dialogs/__init__.py
@@ -1,6 +1,7 @@
"""Dialog components for the Plugin Configurator application."""
+from ui.dialogs.preset_load_dialog import PresetLoadDialog
from ui.dialogs.preset_management_dialog import PresetManagementDialog
from ui.dialogs.success_dialog import SuccessDialog
-__all__ = ["PresetManagementDialog", "SuccessDialog"]
+__all__ = ["PresetLoadDialog", "PresetManagementDialog", "SuccessDialog"]
diff --git a/src/ui/dialogs/preset_load_dialog.py b/src/ui/dialogs/preset_load_dialog.py
new file mode 100644
index 0000000..c2fa467
--- /dev/null
+++ b/src/ui/dialogs/preset_load_dialog.py
@@ -0,0 +1,200 @@
+"""Preset load dialog for selecting and loading an available preset."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from PySide6.QtCore import Qt, Slot
+from PySide6.QtWidgets import (
+ QDialog,
+ QDialogButtonBox,
+ QGroupBox,
+ QHBoxLayout,
+ QLabel,
+ QListWidget,
+ QListWidgetItem,
+ QMessageBox,
+ QSizePolicy,
+ QTextEdit,
+ QVBoxLayout,
+ QWidget,
+)
+
+if TYPE_CHECKING:
+ from core.config_manager import ConfigManager
+
+
+class PresetLoadDialog(QDialog):
+ """Dialog for browsing available presets and loading one into the configurator."""
+
+ def __init__(self, config_manager: ConfigManager, parent: QWidget | None = None) -> None:
+ super().__init__(parent)
+ self._config_manager = config_manager
+ self._selected_preset: str | None = None
+
+ self.setWindowTitle("Load Preset")
+ self.setMinimumSize(600, 400)
+ self.resize(700, 450)
+
+ self._setup_ui()
+ self._refresh_preset_list()
+
+ # ------------------------------------------------------------------
+ # Public API
+ # ------------------------------------------------------------------
+
+ def selected_preset_name(self) -> str | None:
+ """Return the name of the preset chosen by the user, or ``None``."""
+ return self._selected_preset
+
+ # ------------------------------------------------------------------
+ # UI construction
+ # ------------------------------------------------------------------
+
+ def _setup_ui(self) -> None:
+ """Build the dialog layout."""
+ main_layout = QHBoxLayout(self)
+
+ # ── Left panel: preset list ───────────────────────────────────
+ left_panel = QVBoxLayout()
+
+ presets_group = QGroupBox("Available Presets")
+ presets_group_layout = QVBoxLayout(presets_group)
+
+ self._preset_list = QListWidget()
+ self._preset_list.setAlternatingRowColors(True)
+ self._preset_list.currentRowChanged.connect(self._on_preset_selected)
+ self._preset_list.itemDoubleClicked.connect(self._on_load)
+ presets_group_layout.addWidget(self._preset_list)
+
+ left_panel.addWidget(presets_group)
+
+ # ── Right panel: metadata display ─────────────────────────────
+ right_panel = QVBoxLayout()
+
+ metadata_group = QGroupBox("Preset Details")
+ metadata_layout = QVBoxLayout(metadata_group)
+
+ name_header = QLabel("Name")
+ self._name_label = QLabel("—")
+ self._name_label.setWordWrap(True)
+ self._name_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
+
+ desc_header = QLabel("Description")
+ self._desc_text = QTextEdit()
+ self._desc_text.setReadOnly(True)
+ self._desc_text.setPlaceholderText("No description available.")
+ self._desc_text.setMaximumHeight(120)
+
+ metadata_layout.addWidget(name_header)
+ metadata_layout.addWidget(self._name_label)
+ metadata_layout.addSpacing(4)
+ metadata_layout.addWidget(desc_header)
+ metadata_layout.addWidget(self._desc_text)
+ metadata_layout.addStretch()
+
+ right_panel.addWidget(metadata_group)
+
+ # ── Dialog buttons ────────────────────────────────────────────
+ self._button_box = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+ )
+ self._load_btn = self._button_box.button(QDialogButtonBox.StandardButton.Ok)
+ self._load_btn.setText("Load")
+ self._load_btn.setEnabled(False)
+ self._button_box.accepted.connect(self._on_load)
+ self._button_box.rejected.connect(self.reject)
+
+ right_panel.addWidget(self._button_box)
+
+ # ── Assemble panels ───────────────────────────────────────────
+ left_widget = QWidget()
+ left_widget.setLayout(left_panel)
+ left_widget.setMinimumWidth(260)
+
+ right_widget = QWidget()
+ right_widget.setLayout(right_panel)
+
+ main_layout.addWidget(left_widget, 2)
+ main_layout.addWidget(right_widget, 3)
+
+ # ------------------------------------------------------------------
+ # Internal helpers
+ # ------------------------------------------------------------------
+
+ def _refresh_preset_list(self) -> None:
+ """Reload the preset list from disk.
+
+ The first preset in the sorted list is automatically selected so that
+ the metadata panel is populated immediately on dialog open.
+ """
+ self._preset_list.clear()
+ for preset_name in self._config_manager.get_available_presets():
+ self._preset_list.addItem(QListWidgetItem(preset_name))
+
+ # Auto-select the first item so the metadata panel is populated right away.
+ if self._preset_list.count() > 0:
+ self._preset_list.setCurrentRow(0)
+
+ self._update_load_button()
+
+ def _update_load_button(self) -> None:
+ """Enable the Load button only when a preset is selected."""
+ self._load_btn.setEnabled(self._preset_list.currentItem() is not None)
+
+ def _current_preset_name(self) -> str | None:
+ """Return the currently selected preset name, or ``None``."""
+ item = self._preset_list.currentItem()
+ return item.text() if item is not None else None
+
+ def _load_metadata(self, preset_name: str) -> dict:
+ """Return the ``meta`` section of *preset_name*, or an empty dict on error."""
+ try:
+ config = self._config_manager.load_preset(preset_name)
+ meta: dict = config.get("meta", {})
+ return meta
+ except (ValueError, OSError):
+ return {}
+
+ # ------------------------------------------------------------------
+ # Slots
+ # ------------------------------------------------------------------
+
+ @Slot(int)
+ def _on_preset_selected(self, row: int) -> None:
+ """Update the metadata panel when the user selects a preset."""
+ self._update_load_button()
+
+ if row < 0:
+ self._name_label.setText("—")
+ self._desc_text.setPlainText("")
+ return
+
+ preset_name = self._current_preset_name()
+ if preset_name is None:
+ return
+
+ meta = self._load_metadata(preset_name)
+ self._name_label.setText(meta.get("name") or preset_name)
+ self._desc_text.setPlainText(meta.get("description", ""))
+
+ @Slot()
+ def _on_load(self) -> None:
+ """Validate the selected preset and accept the dialog."""
+ preset_name = self._current_preset_name()
+ if preset_name is None:
+ return
+
+ is_valid, errors = self._config_manager.validate_preset_file(
+ self._config_manager.preset_dir / f"{preset_name}.xml"
+ )
+ if not is_valid:
+ QMessageBox.warning(
+ self,
+ "Invalid Preset",
+ "The selected preset is not valid:\n\n" + "\n".join(errors),
+ )
+ return
+
+ self._selected_preset = preset_name
+ self.accept()
diff --git a/src/ui/main_window.py b/src/ui/main_window.py
index 0169b2e..67d0652 100644
--- a/src/ui/main_window.py
+++ b/src/ui/main_window.py
@@ -14,7 +14,7 @@
from core.config_manager import ConfigManager
from ui.components import FileTreePreview
-from ui.dialogs import PresetManagementDialog
+from ui.dialogs import PresetLoadDialog, PresetManagementDialog
from ui.tabs.configuration_tab import ConfigurationTab
from ui.tabs.development_workflow_tab import DevelopmentWorkflowTab
from ui.tabs.generate_tab import GenerateTab
@@ -122,6 +122,10 @@ def setup_menu(self):
new_action.setShortcut("Ctrl+N")
new_action.triggered.connect(self.new_project)
+ load_preset_action = QAction("&Load Preset…", self)
+ load_preset_action.setShortcut("Ctrl+L")
+ load_preset_action.triggered.connect(self.show_load_preset_dialog)
+
save_preset_action = QAction("&Save as Preset", self)
save_preset_action.setShortcut("Ctrl+S")
save_preset_action.triggered.connect(self.save_current_as_preset)
@@ -140,6 +144,7 @@ def setup_menu(self):
# Add actions to file menu
file_menu.addAction(new_action)
+ file_menu.addAction(load_preset_action)
file_menu.addAction(save_preset_action)
file_menu.addAction(manage_presets_action)
file_menu.addAction(generate_action)
@@ -232,6 +237,30 @@ def show_preset_management(self):
dialog = PresetManagementDialog(self._config_manager, parent=self)
dialog.exec()
+ @Slot()
+ def show_load_preset_dialog(self):
+ """Open the preset load dialog and apply the chosen preset to all tabs."""
+ dialog = PresetLoadDialog(self._config_manager, parent=self)
+ if not dialog.exec():
+ return
+
+ preset_name = dialog.selected_preset_name()
+ if preset_name is None:
+ return
+
+ try:
+ config = self._config_manager.load_preset(preset_name)
+ except ValueError as exc:
+ QMessageBox.warning(
+ self,
+ "Load Preset Failed",
+ f'Could not load preset "{preset_name}":\n{exc}',
+ )
+ return
+
+ self.load_preset_to_all_tabs(config)
+ self.status_bar.showMessage(f'Preset "{preset_name}" loaded')
+
@Slot()
def generate_project(self):
"""Start the project generation process"""
diff --git a/src/ui/tabs/project_info_tab.py b/src/ui/tabs/project_info_tab.py
index f95ac85..6d476dd 100644
--- a/src/ui/tabs/project_info_tab.py
+++ b/src/ui/tabs/project_info_tab.py
@@ -300,8 +300,13 @@ def setup_ui(self):
"Skip the detailed steps and jump straight to Review & Generate with sensible defaults."
)
quick_actions_layout = QHBoxLayout()
+ self.load_preset_button = QPushButton("Load Preset…")
+ self.load_preset_button.setToolTip(
+ "Browse and load a saved preset to populate all configuration tabs."
+ )
self.review_generate_button = QPushButton("Review & Generate")
self.review_generate_button.setEnabled(False)
+ quick_actions_layout.addWidget(self.load_preset_button)
quick_actions_layout.addWidget(self.review_generate_button)
quick_actions_layout.addStretch()
quick_start_layout.addWidget(self.quick_start_checkbox)
@@ -378,6 +383,7 @@ def setup_connections(self):
self.project_name.textChanged.connect(self.update_file_tree)
self.quick_start_checkbox.toggled.connect(self._on_quick_start_toggled)
self.review_generate_button.clicked.connect(self._on_review_generate_clicked)
+ self.load_preset_button.clicked.connect(self._on_load_preset_clicked)
for spin in [self.fx_wet_dry, self.fx_latency, self.instrument_polyphony]:
spin.valueChanged.connect(self._on_plugin_type_option_changed)
for checkbox in [self.fx_sidechain, self.instrument_midi_input]:
@@ -856,6 +862,13 @@ def _on_review_generate_clicked(self):
main_window.quick_start_review_generate()
self._update_quick_start_button_state()
+ @Slot()
+ def _on_load_preset_clicked(self):
+ """Open the preset load dialog and apply the chosen preset to all tabs."""
+ main_window = self.window()
+ if hasattr(main_window, "show_load_preset_dialog"):
+ main_window.show_load_preset_dialog()
+
def _has_required_quick_start_data(self) -> bool:
"""Check required fields without showing dialogs for button state."""
return self._validate_required_fields(show_messages=False)
diff --git a/tests/test_preset_load_dialog.py b/tests/test_preset_load_dialog.py
new file mode 100644
index 0000000..365bd43
--- /dev/null
+++ b/tests/test_preset_load_dialog.py
@@ -0,0 +1,374 @@
+"""Tests for the PresetLoadDialog."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import pytest
+from PySide6.QtWidgets import QApplication, QMessageBox
+
+from core.config_manager import ConfigManager
+from ui.dialogs.preset_load_dialog import PresetLoadDialog
+
+
+@pytest.fixture(scope="module")
+def app():
+ """Create a single QApplication instance for all tests in this module."""
+ instance = QApplication.instance()
+ if not instance:
+ instance = QApplication(sys.argv)
+ yield instance
+
+
+@pytest.fixture
+def preset_dir(tmp_path: Path) -> Path:
+ return tmp_path / "presets"
+
+
+@pytest.fixture
+def config_manager(preset_dir: Path) -> ConfigManager:
+ return ConfigManager(preset_dir=preset_dir)
+
+
+@pytest.fixture
+def sample_config() -> dict:
+ return {
+ "meta": {"name": "My Test Preset", "description": "A preset for testing"},
+ "project_info": {
+ "template_name": "Audio FX Plugin",
+ "template_url": "https://github.com/SeamusMullan/PluginTemplate.git",
+ "project_name": "TestPlugin",
+ "product_name": "Test Plugin",
+ "version": "1.0.0",
+ "company_name": "TestCo",
+ "bundle_id": "com.test.plugin",
+ "manufacturer_code": "TST1",
+ "plugin_code": "TST1",
+ "output_directory": "/tmp/output",
+ },
+ "configuration": {
+ "standalone": False,
+ "vst3": True,
+ "au": False,
+ "auv3": False,
+ "clap": False,
+ "au_component_type": "aufx",
+ "au_component_subtype": "plug",
+ "au_component_manufacturer": "Ddsp",
+ "au_version": "1.0.0",
+ "clap_extensions": "note-ports,state",
+ "clap_features": "audio-effect",
+ "auv3_platform": "iOS",
+ "gui_width": 800,
+ "gui_height": 600,
+ "resizable": False,
+ "background_image": "",
+ "code_signing": False,
+ "installer": False,
+ "default_bypass": False,
+ "input_gain": False,
+ "output_gain": False,
+ },
+ "implementations": {
+ "moonbase_licensing": False,
+ "melatonin_inspector": False,
+ "custom_gui_framework": False,
+ "logging_framework": False,
+ "clap_builds": False,
+ "preset_management": False,
+ "preset_format": "",
+ "ab_comparison": False,
+ "state_management": False,
+ "gpu_audio": False,
+ },
+ "user_experience": {
+ "wizard": False,
+ "preview": False,
+ "preset_management": False,
+ },
+ "development_workflow": {
+ "vcs": False,
+ "testing": False,
+ "code_quality": False,
+ "validation_tools": False,
+ "scaffolding": False,
+ },
+ }
+
+
+# ---------------------------------------------------------------------------
+# Initialisation tests
+# ---------------------------------------------------------------------------
+
+
+class TestPresetLoadDialogInit:
+ def test_dialog_is_not_none(self, app, config_manager):
+ dlg = PresetLoadDialog(config_manager)
+ assert dlg is not None
+ dlg.deleteLater()
+
+ def test_dialog_window_title(self, app, config_manager):
+ dlg = PresetLoadDialog(config_manager)
+ assert dlg.windowTitle() == "Load Preset"
+ dlg.deleteLater()
+
+ def test_preset_list_is_present(self, app, config_manager):
+ dlg = PresetLoadDialog(config_manager)
+ assert dlg._preset_list is not None
+ dlg.deleteLater()
+
+ def test_load_button_disabled_without_presets(self, app, preset_dir, monkeypatch):
+ """When there are no presets the Load button must be disabled."""
+ empty_dir = preset_dir / "empty_load"
+ empty_dir.mkdir(parents=True, exist_ok=True)
+ cfg = ConfigManager.__new__(ConfigManager)
+ cfg.preset_dir = empty_dir
+ cfg.example_presets_dir = Path("/nonexistent")
+ monkeypatch.setattr(cfg, "_install_bundled_presets", lambda: None)
+ dlg = PresetLoadDialog(cfg)
+ assert not dlg._load_btn.isEnabled()
+ dlg.deleteLater()
+
+ def test_load_button_enabled_when_presets_available(self, app, config_manager, sample_config):
+ """The Load button must be enabled when at least one preset is available and auto-selected."""
+ config_manager.save_preset(sample_config, "btn_enable_preset")
+ dlg = PresetLoadDialog(config_manager)
+ # The list should be populated and the first row auto-selected
+ assert dlg._preset_list.count() > 0
+ assert dlg._preset_list.currentItem() is not None, "First preset should be auto-selected"
+ assert dlg._load_btn.isEnabled()
+ dlg.deleteLater()
+
+ def test_load_button_disabled_when_selection_cleared(self, app, config_manager):
+ """Deselecting all presets must disable the Load button."""
+ dlg = PresetLoadDialog(config_manager)
+ # Bundled presets ensure the list is non-empty
+ assert dlg._preset_list.count() > 0
+ dlg._preset_list.setCurrentRow(-1)
+ assert not dlg._load_btn.isEnabled()
+ dlg.deleteLater()
+
+ def test_bundled_presets_appear_in_list(self, app, config_manager):
+ """Bundled presets installed by ConfigManager must appear in the list."""
+ dlg = PresetLoadDialog(config_manager)
+ names = [dlg._preset_list.item(i).text() for i in range(dlg._preset_list.count())]
+ for bundled in ConfigManager.BUNDLED_PRESETS:
+ assert bundled in names
+ dlg.deleteLater()
+
+
+# ---------------------------------------------------------------------------
+# Preset list population
+# ---------------------------------------------------------------------------
+
+
+class TestPresetLoadDialogListPopulation:
+ def test_saved_preset_appears_in_list(self, app, config_manager, sample_config):
+ config_manager.save_preset(sample_config, "load_list_test")
+ dlg = PresetLoadDialog(config_manager)
+ names = [dlg._preset_list.item(i).text() for i in range(dlg._preset_list.count())]
+ assert "load_list_test" in names
+ dlg.deleteLater()
+
+ def test_list_is_sorted(self, app, config_manager, sample_config):
+ config_manager.save_preset(sample_config, "zzz_last")
+ config_manager.save_preset(sample_config, "aaa_first")
+ dlg = PresetLoadDialog(config_manager)
+ names = [dlg._preset_list.item(i).text() for i in range(dlg._preset_list.count())]
+ assert names == sorted(names)
+ dlg.deleteLater()
+
+
+# ---------------------------------------------------------------------------
+# Metadata display
+# ---------------------------------------------------------------------------
+
+
+class TestPresetLoadDialogMetadata:
+ def test_selecting_preset_shows_display_name(self, app, config_manager, sample_config):
+ config_manager.save_preset(sample_config, "meta_load_test")
+ dlg = PresetLoadDialog(config_manager)
+
+ for i in range(dlg._preset_list.count()):
+ if dlg._preset_list.item(i).text() == "meta_load_test":
+ dlg._preset_list.setCurrentRow(i)
+ break
+
+ assert dlg._name_label.text() == "My Test Preset"
+ dlg.deleteLater()
+
+ def test_selecting_preset_shows_description(self, app, config_manager, sample_config):
+ config_manager.save_preset(sample_config, "desc_load_test")
+ dlg = PresetLoadDialog(config_manager)
+
+ for i in range(dlg._preset_list.count()):
+ if dlg._preset_list.item(i).text() == "desc_load_test":
+ dlg._preset_list.setCurrentRow(i)
+ break
+
+ assert "A preset for testing" in dlg._desc_text.toPlainText()
+ dlg.deleteLater()
+
+ def test_no_selection_shows_dash(self, app, preset_dir, monkeypatch):
+ """When no preset is selected the name label should show a dash."""
+ empty_dir = preset_dir / "empty_meta"
+ empty_dir.mkdir(parents=True, exist_ok=True)
+ cfg = ConfigManager.__new__(ConfigManager)
+ cfg.preset_dir = empty_dir
+ cfg.example_presets_dir = Path("/nonexistent")
+ monkeypatch.setattr(cfg, "_install_bundled_presets", lambda: None)
+ dlg = PresetLoadDialog(cfg)
+ assert dlg._name_label.text() == "—"
+ dlg.deleteLater()
+
+
+# ---------------------------------------------------------------------------
+# Load / accept behaviour
+# ---------------------------------------------------------------------------
+
+
+class TestPresetLoadDialogAccept:
+ def test_selected_preset_name_is_none_before_accept(self, app, config_manager):
+ dlg = PresetLoadDialog(config_manager)
+ assert dlg.selected_preset_name() is None
+ dlg.deleteLater()
+
+ def test_on_load_stores_preset_name_and_accepts(self, app, config_manager, sample_config):
+ config_manager.save_preset(sample_config, "accept_test_preset")
+ dlg = PresetLoadDialog(config_manager)
+
+ for i in range(dlg._preset_list.count()):
+ if dlg._preset_list.item(i).text() == "accept_test_preset":
+ dlg._preset_list.setCurrentRow(i)
+ break
+
+ # Simulate pressing Load without actually opening the dialog event loop
+ dlg._on_load()
+
+ assert dlg.selected_preset_name() == "accept_test_preset"
+ dlg.deleteLater()
+
+ def test_on_load_does_nothing_without_selection(self, app, preset_dir, monkeypatch):
+ """Calling _on_load with no preset selected must not crash."""
+ empty_dir = preset_dir / "empty_load2"
+ empty_dir.mkdir(parents=True, exist_ok=True)
+ cfg = ConfigManager.__new__(ConfigManager)
+ cfg.preset_dir = empty_dir
+ cfg.example_presets_dir = Path("/nonexistent")
+ monkeypatch.setattr(cfg, "_install_bundled_presets", lambda: None)
+ dlg = PresetLoadDialog(cfg)
+ dlg._on_load() # Should not raise
+ assert dlg.selected_preset_name() is None
+ dlg.deleteLater()
+
+ def test_invalid_preset_shows_warning(self, app, config_manager, tmp_path, monkeypatch):
+ """Loading a corrupt preset file must display a warning."""
+ # Write a bad XML file directly to preset_dir
+ bad_file = config_manager.preset_dir / "corrupt_preset.xml"
+ bad_file.write_text("", encoding="utf-8")
+
+ dlg = PresetLoadDialog(config_manager)
+
+ for i in range(dlg._preset_list.count()):
+ if dlg._preset_list.item(i).text() == "corrupt_preset":
+ dlg._preset_list.setCurrentRow(i)
+ break
+
+ warnings = []
+ monkeypatch.setattr(
+ QMessageBox,
+ "warning",
+ staticmethod(lambda *a, **kw: warnings.append(True)),
+ )
+
+ dlg._on_load()
+ assert warnings, "Expected a warning dialog for invalid preset"
+ assert dlg.selected_preset_name() is None
+ dlg.deleteLater()
+
+
+# ---------------------------------------------------------------------------
+# MainWindow integration
+# ---------------------------------------------------------------------------
+
+
+class TestMainWindowLoadPreset:
+ """Verify that MainWindow.show_load_preset_dialog applies config to all tabs."""
+
+ def test_show_load_preset_dialog_applies_config(
+ self, app, config_manager, sample_config, monkeypatch
+ ):
+ from ui.main_window import MainWindow
+
+ window = MainWindow()
+
+ # Save a preset and monkey-patch the dialog to auto-accept it
+ config_manager.save_preset(sample_config, "integration_preset")
+
+ # Provide a config manager that has the preset
+ window._config_manager = config_manager
+
+ loaded_configs = []
+
+ original_load = window.load_preset_to_all_tabs
+
+ def capture_load(config):
+ loaded_configs.append(config)
+ original_load(config)
+
+ monkeypatch.setattr(window, "load_preset_to_all_tabs", capture_load)
+
+ # Simulate the dialog returning "accepted" with a preset selected
+ class _FakeDialog:
+ def __init__(self, *a, **kw):
+ pass
+
+ def exec(self):
+ return 1 # QDialog.DialogCode.Accepted
+
+ def selected_preset_name(self):
+ return "integration_preset"
+
+ monkeypatch.setattr(
+ "ui.main_window.PresetLoadDialog",
+ _FakeDialog,
+ )
+
+ window.show_load_preset_dialog()
+
+ assert loaded_configs, "load_preset_to_all_tabs was not called"
+ config = loaded_configs[0]
+ assert config["project_info"]["project_name"] == "TestPlugin"
+ window.close()
+
+ def test_show_load_preset_dialog_cancelled_does_nothing(
+ self, app, config_manager, monkeypatch
+ ):
+ from ui.main_window import MainWindow
+
+ window = MainWindow()
+
+ loaded = []
+
+ def capture_load(config):
+ loaded.append(config)
+
+ monkeypatch.setattr(window, "load_preset_to_all_tabs", capture_load)
+
+ class _CancelledDialog:
+ def __init__(self, *a, **kw):
+ pass
+
+ def exec(self):
+ return 0 # QDialog.DialogCode.Rejected
+
+ def selected_preset_name(self):
+ return None
+
+ monkeypatch.setattr("ui.main_window.PresetLoadDialog", _CancelledDialog)
+
+ window.show_load_preset_dialog()
+
+ assert not loaded, "load_preset_to_all_tabs should not be called on cancel"
+ window.close()