""" Preset Controls GUI Interface for saving, loading, and managing presets. """ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QListWidget, QPushButton, QLineEdit, QLabel, QFileDialog, QMessageBox, QListWidgetItem, QInputDialog, QFrame) from PyQt5.QtCore import Qt, pyqtSlot import json import os class PresetControls(QWidget): """Control panel for preset management""" def __init__(self, arpeggiator, channel_manager, volume_engine): super().__init__() self.arpeggiator = arpeggiator self.channel_manager = channel_manager self.volume_engine = volume_engine # Preset storage self.presets = {} self.current_preset = None self.presets_directory = "presets" # Ensure presets directory exists os.makedirs(self.presets_directory, exist_ok=True) self.setup_ui() self.load_presets_from_directory() def setup_ui(self): """Set up the user interface""" layout = QVBoxLayout(self) # Preset list preset_group = self.create_preset_list() layout.addWidget(preset_group) # Preset operations operations_group = self.create_operations() layout.addWidget(operations_group) # File operations file_group = self.create_file_operations() layout.addWidget(file_group) def create_preset_list(self) -> QGroupBox: """Create preset list display""" group = QGroupBox("Presets") layout = QVBoxLayout(group) self.preset_list = QListWidget() self.preset_list.setMaximumHeight(200) self.preset_list.itemClicked.connect(self.on_preset_selected) self.preset_list.itemDoubleClicked.connect(self.on_preset_double_clicked) layout.addWidget(self.preset_list) # Current preset indicator current_layout = QHBoxLayout() current_layout.addWidget(QLabel("Current:")) self.current_preset_label = QLabel("None") self.current_preset_label.setStyleSheet("font-weight: bold; color: #00aa00;") current_layout.addWidget(self.current_preset_label) current_layout.addStretch() layout.addLayout(current_layout) return group def create_operations(self) -> QGroupBox: """Create preset operation buttons""" group = QGroupBox("Operations") layout = QGridLayout(group) # Load preset self.load_button = QPushButton("Load Preset") self.load_button.setEnabled(False) self.load_button.clicked.connect(self.load_selected_preset) layout.addWidget(self.load_button, 0, 0) # Save current as new preset self.save_new_button = QPushButton("Save as New...") self.save_new_button.clicked.connect(self.save_new_preset) layout.addWidget(self.save_new_button, 0, 1) # Update selected preset self.update_button = QPushButton("Update Selected") self.update_button.setEnabled(False) self.update_button.clicked.connect(self.update_selected_preset) layout.addWidget(self.update_button, 1, 0) # Delete preset self.delete_button = QPushButton("Delete Selected") self.delete_button.setEnabled(False) self.delete_button.clicked.connect(self.delete_selected_preset) self.delete_button.setStyleSheet("color: #aa6666;") layout.addWidget(self.delete_button, 1, 1) # Rename preset self.rename_button = QPushButton("Rename Selected") self.rename_button.setEnabled(False) self.rename_button.clicked.connect(self.rename_selected_preset) layout.addWidget(self.rename_button, 2, 0) # Duplicate preset self.duplicate_button = QPushButton("Duplicate Selected") self.duplicate_button.setEnabled(False) self.duplicate_button.clicked.connect(self.duplicate_selected_preset) layout.addWidget(self.duplicate_button, 2, 1) return group def create_file_operations(self) -> QGroupBox: """Create file operation buttons""" group = QGroupBox("File Operations") layout = QHBoxLayout(group) # Import preset self.import_button = QPushButton("Import Preset...") self.import_button.clicked.connect(self.import_preset) layout.addWidget(self.import_button) # Export preset self.export_button = QPushButton("Export Selected...") self.export_button.setEnabled(False) self.export_button.clicked.connect(self.export_selected_preset) layout.addWidget(self.export_button) return group def capture_current_settings(self) -> dict: """Capture current settings into a preset dictionary""" preset = { "version": "1.0", "timestamp": None, # Will be set when saving # Arpeggiator settings "arpeggiator": { "root_note": self.arpeggiator.root_note, "scale": self.arpeggiator.scale, "pattern_type": self.arpeggiator.pattern_type, "octave_range": self.arpeggiator.octave_range, "note_speed": self.arpeggiator.note_speed, "gate": self.arpeggiator.gate, "swing": self.arpeggiator.swing, "velocity": self.arpeggiator.velocity, "tempo": self.arpeggiator.tempo }, # Channel settings "channels": { "active_synth_count": self.channel_manager.active_synth_count, "channel_instruments": self.channel_manager.channel_instruments.copy() }, # Volume pattern settings "volume_patterns": { "current_pattern": self.volume_engine.current_pattern, "pattern_speed": self.volume_engine.pattern_speed, "pattern_intensity": self.volume_engine.pattern_intensity, "global_volume_range": self.volume_engine.global_volume_range, "global_velocity_range": self.volume_engine.global_velocity_range, "channel_volume_ranges": self.volume_engine.channel_volume_ranges.copy(), "velocity_ranges": self.volume_engine.velocity_ranges.copy() } } return preset def apply_preset_settings(self, preset: dict): """Apply preset settings to the system""" try: # Apply arpeggiator settings arp_settings = preset.get("arpeggiator", {}) self.arpeggiator.set_root_note(arp_settings.get("root_note", 60)) self.arpeggiator.set_scale(arp_settings.get("scale", "major")) self.arpeggiator.set_pattern_type(arp_settings.get("pattern_type", "up")) self.arpeggiator.set_octave_range(arp_settings.get("octave_range", 1)) self.arpeggiator.set_note_speed(arp_settings.get("note_speed", "1/8")) self.arpeggiator.set_gate(arp_settings.get("gate", 1.0)) self.arpeggiator.set_swing(arp_settings.get("swing", 0.0)) self.arpeggiator.set_velocity(arp_settings.get("velocity", 80)) self.arpeggiator.set_tempo(arp_settings.get("tempo", 120.0)) # Apply channel settings channel_settings = preset.get("channels", {}) self.channel_manager.set_active_synth_count( channel_settings.get("active_synth_count", 8) ) # Apply instruments instruments = channel_settings.get("channel_instruments", {}) for channel_str, program in instruments.items(): channel = int(channel_str) self.channel_manager.set_channel_instrument(channel, program) # Apply volume pattern settings volume_settings = preset.get("volume_patterns", {}) self.volume_engine.set_pattern(volume_settings.get("current_pattern", "static")) self.volume_engine.set_pattern_speed(volume_settings.get("pattern_speed", 1.0)) self.volume_engine.set_pattern_intensity(volume_settings.get("pattern_intensity", 1.0)) # Apply global ranges global_vol = volume_settings.get("global_volume_range", (0.2, 1.0)) global_vel = volume_settings.get("global_velocity_range", (40, 127)) self.volume_engine.set_global_ranges( global_vol[0], global_vol[1], global_vel[0], global_vel[1] ) # Apply individual channel ranges ch_vol_ranges = volume_settings.get("channel_volume_ranges", {}) for channel_str, range_tuple in ch_vol_ranges.items(): channel = int(channel_str) self.volume_engine.set_channel_volume_range(channel, range_tuple[0], range_tuple[1]) vel_ranges = volume_settings.get("velocity_ranges", {}) for channel_str, range_tuple in vel_ranges.items(): channel = int(channel_str) self.volume_engine.set_velocity_range(channel, range_tuple[0], range_tuple[1]) except Exception as e: QMessageBox.warning(self, "Preset Error", f"Error applying preset: {str(e)}") @pyqtSlot(QListWidgetItem) def on_preset_selected(self, item): """Handle preset selection""" preset_name = item.text() # Enable/disable buttons based on selection has_selection = preset_name is not None self.load_button.setEnabled(has_selection) self.update_button.setEnabled(has_selection) self.delete_button.setEnabled(has_selection) self.rename_button.setEnabled(has_selection) self.duplicate_button.setEnabled(has_selection) self.export_button.setEnabled(has_selection) @pyqtSlot(QListWidgetItem) def on_preset_double_clicked(self, item): """Handle preset double-click (load preset)""" self.load_selected_preset() @pyqtSlot() def load_selected_preset(self): """Load the selected preset""" current_item = self.preset_list.currentItem() if not current_item: return preset_name = current_item.text() if preset_name in self.presets: self.apply_preset_settings(self.presets[preset_name]) self.current_preset = preset_name self.current_preset_label.setText(preset_name) # Visual feedback current_item.setBackground(Qt.darkGreen) for i in range(self.preset_list.count()): item = self.preset_list.item(i) if item != current_item: item.setBackground(Qt.transparent) @pyqtSlot() def save_new_preset(self): """Save current settings as a new preset""" name, ok = QInputDialog.getText(self, "New Preset", "Enter preset name:") if ok and name: if name in self.presets: reply = QMessageBox.question( self, "Overwrite Preset", f"Preset '{name}' already exists. Overwrite?", QMessageBox.Yes | QMessageBox.No ) if reply != QMessageBox.Yes: return # Capture current settings preset_data = self.capture_current_settings() preset_data["timestamp"] = self.get_current_timestamp() # Save preset self.presets[name] = preset_data self.save_preset_to_file(name, preset_data) # Update list self.refresh_preset_list() # Select the new preset items = self.preset_list.findItems(name, Qt.MatchExactly) if items: self.preset_list.setCurrentItem(items[0]) @pyqtSlot() def update_selected_preset(self): """Update the selected preset with current settings""" current_item = self.preset_list.currentItem() if not current_item: return preset_name = current_item.text() reply = QMessageBox.question( self, "Update Preset", f"Update preset '{preset_name}' with current settings?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: preset_data = self.capture_current_settings() preset_data["timestamp"] = self.get_current_timestamp() self.presets[preset_name] = preset_data self.save_preset_to_file(preset_name, preset_data) @pyqtSlot() def delete_selected_preset(self): """Delete the selected preset""" current_item = self.preset_list.currentItem() if not current_item: return preset_name = current_item.text() reply = QMessageBox.question( self, "Delete Preset", f"Delete preset '{preset_name}'? This cannot be undone.", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: # Remove from memory and file if preset_name in self.presets: del self.presets[preset_name] preset_file = os.path.join(self.presets_directory, f"{preset_name}.json") if os.path.exists(preset_file): os.remove(preset_file) # Update list self.refresh_preset_list() # Clear current if it was deleted if self.current_preset == preset_name: self.current_preset = None self.current_preset_label.setText("None") @pyqtSlot() def rename_selected_preset(self): """Rename the selected preset""" current_item = self.preset_list.currentItem() if not current_item: return old_name = current_item.text() new_name, ok = QInputDialog.getText(self, "Rename Preset", "Enter new name:", text=old_name) if ok and new_name and new_name != old_name: if new_name in self.presets: QMessageBox.warning(self, "Rename Error", f"Preset '{new_name}' already exists.") return # Move preset data self.presets[new_name] = self.presets[old_name] del self.presets[old_name] # Handle files old_file = os.path.join(self.presets_directory, f"{old_name}.json") new_file = os.path.join(self.presets_directory, f"{new_name}.json") if os.path.exists(old_file): os.rename(old_file, new_file) # Update current preset reference if self.current_preset == old_name: self.current_preset = new_name self.current_preset_label.setText(new_name) # Refresh list self.refresh_preset_list() @pyqtSlot() def duplicate_selected_preset(self): """Duplicate the selected preset""" current_item = self.preset_list.currentItem() if not current_item: return source_name = current_item.text() new_name, ok = QInputDialog.getText(self, "Duplicate Preset", "Enter name for copy:", text=f"{source_name} Copy") if ok and new_name: if new_name in self.presets: QMessageBox.warning(self, "Duplicate Error", f"Preset '{new_name}' already exists.") return # Copy preset data self.presets[new_name] = self.presets[source_name].copy() self.save_preset_to_file(new_name, self.presets[new_name]) # Refresh list and select new preset self.refresh_preset_list() items = self.preset_list.findItems(new_name, Qt.MatchExactly) if items: self.preset_list.setCurrentItem(items[0]) @pyqtSlot() def import_preset(self): """Import a preset from file""" file_path, _ = QFileDialog.getOpenFileName( self, "Import Preset", "", "JSON Files (*.json);;All Files (*)" ) if file_path: try: with open(file_path, 'r') as f: preset_data = json.load(f) # Get name from user default_name = os.path.splitext(os.path.basename(file_path))[0] name, ok = QInputDialog.getText(self, "Import Preset", "Preset name:", text=default_name) if ok and name: self.presets[name] = preset_data self.save_preset_to_file(name, preset_data) self.refresh_preset_list() except Exception as e: QMessageBox.critical(self, "Import Error", f"Error importing preset: {str(e)}") @pyqtSlot() def export_selected_preset(self): """Export the selected preset to file""" current_item = self.preset_list.currentItem() if not current_item: return preset_name = current_item.text() file_path, _ = QFileDialog.getSaveFileName( self, "Export Preset", f"{preset_name}.json", "JSON Files (*.json);;All Files (*)" ) if file_path: try: with open(file_path, 'w') as f: json.dump(self.presets[preset_name], f, indent=2) except Exception as e: QMessageBox.critical(self, "Export Error", f"Error exporting preset: {str(e)}") def load_presets_from_directory(self): """Load all presets from the presets directory""" if not os.path.exists(self.presets_directory): return for filename in os.listdir(self.presets_directory): if filename.endswith('.json'): preset_name = os.path.splitext(filename)[0] file_path = os.path.join(self.presets_directory, filename) try: with open(file_path, 'r') as f: preset_data = json.load(f) self.presets[preset_name] = preset_data except Exception as e: print(f"Error loading preset {filename}: {e}") self.refresh_preset_list() def save_preset_to_file(self, name: str, preset_data: dict): """Save a preset to file""" file_path = os.path.join(self.presets_directory, f"{name}.json") try: with open(file_path, 'w') as f: json.dump(preset_data, f, indent=2) except Exception as e: QMessageBox.critical(self, "Save Error", f"Error saving preset: {str(e)}") def refresh_preset_list(self): """Refresh the preset list display""" self.preset_list.clear() for name in sorted(self.presets.keys()): item = QListWidgetItem(name) if name == self.current_preset: item.setBackground(Qt.darkGreen) self.preset_list.addItem(item) def get_current_timestamp(self) -> str: """Get current timestamp string""" from datetime import datetime return datetime.now().isoformat() def new_preset(self): """Create a new preset (for menu action)""" self.save_new_preset() def load_preset(self): """Load preset (for menu action)""" if self.preset_list.currentItem(): self.load_selected_preset() def save_preset(self): """Save preset (for menu action)""" if self.preset_list.currentItem(): self.update_selected_preset() else: self.save_new_preset()