You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

510 lines
20 KiB

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