Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ui/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
200 changes: 200 additions & 0 deletions src/ui/dialogs/preset_load_dialog.py
Original file line number Diff line number Diff line change
@@ -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("<b>Name</b>")
self._name_label = QLabel("—")
self._name_label.setWordWrap(True)
self._name_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

desc_header = QLabel("<b>Description</b>")
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()
31 changes: 30 additions & 1 deletion src/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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"""
Expand Down
13 changes: 13 additions & 0 deletions src/ui/tabs/project_info_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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)
Expand Down
Loading