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()