@ -31,6 +31,10 @@ class PresetControls(QWidget):
self . setup_ui ( )
self . load_presets_from_directory ( )
self . load_first_preset_on_startup ( )
# Connect to armed state changes
self . arpeggiator . armed_state_changed . connect ( self . on_armed_state_changed )
def setup_ui ( self ) :
""" Set up the user interface """
@ -111,6 +115,17 @@ class PresetControls(QWidget):
self . duplicate_button . clicked . connect ( self . duplicate_selected_preset )
layout . addWidget ( self . duplicate_button , 2 , 1 )
# Safety controls
self . force_apply_button = QPushButton ( " Force Apply Armed " )
self . force_apply_button . clicked . connect ( self . force_apply_armed )
self . force_apply_button . setStyleSheet ( " color: #ffaa00; font-weight: bold; " )
layout . addWidget ( self . force_apply_button , 3 , 0 )
self . clear_armed_button = QPushButton ( " Clear Armed " )
self . clear_armed_button . clicked . connect ( self . clear_armed )
self . clear_armed_button . setStyleSheet ( " color: #aa6666; " )
layout . addWidget ( self . clear_armed_button , 3 , 1 )
return group
def create_file_operations ( self ) - > QGroupBox :
@ -147,7 +162,13 @@ class PresetControls(QWidget):
" gate " : self . arpeggiator . gate ,
" swing " : self . arpeggiator . swing ,
" velocity " : self . arpeggiator . velocity ,
" tempo " : self . arpeggiator . tempo
" tempo " : self . arpeggiator . tempo ,
" pattern_length " : getattr ( self . arpeggiator , ' pattern_length ' , 8 ) ,
" channel_distribution " : self . arpeggiator . channel_distribution ,
" delay_enabled " : self . arpeggiator . delay_enabled ,
" delay_length " : self . arpeggiator . delay_length ,
" delay_timing " : self . arpeggiator . delay_timing ,
" delay_fade " : self . arpeggiator . delay_fade
} ,
# Channel settings
@ -173,6 +194,12 @@ class PresetControls(QWidget):
def apply_preset_settings ( self , preset : dict ) :
""" Apply preset settings to the system """
try :
# Find the preset name for this data (for tracking current preset)
preset_name = None
for name , data in self . presets . items ( ) :
if data == preset :
preset_name = name
break
# Apply arpeggiator settings
arp_settings = preset . get ( " arpeggiator " , { } )
self . arpeggiator . set_root_note ( arp_settings . get ( " root_note " , 60 ) )
@ -185,6 +212,20 @@ class PresetControls(QWidget):
self . arpeggiator . set_velocity ( arp_settings . get ( " velocity " , 80 ) )
self . arpeggiator . set_tempo ( arp_settings . get ( " tempo " , 120.0 ) )
# Apply pattern length if available
if " pattern_length " in arp_settings :
if hasattr ( self . arpeggiator , ' set_pattern_length ' ) :
self . arpeggiator . set_pattern_length ( arp_settings [ " pattern_length " ] )
# Apply channel distribution
self . arpeggiator . set_channel_distribution ( arp_settings . get ( " channel_distribution " , " up " ) )
# Apply delay settings
self . arpeggiator . set_delay_enabled ( arp_settings . get ( " delay_enabled " , False ) )
self . arpeggiator . set_delay_length ( arp_settings . get ( " delay_length " , 3 ) )
self . arpeggiator . set_delay_timing ( arp_settings . get ( " delay_timing " , " 1/4 " ) )
self . arpeggiator . set_delay_fade ( arp_settings . get ( " delay_fade " , 0.3 ) )
# Apply channel settings
channel_settings = preset . get ( " channels " , { } )
self . channel_manager . set_active_synth_count (
@ -220,6 +261,13 @@ class PresetControls(QWidget):
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 ] )
# Update UI if preset was found
if preset_name :
self . current_preset = preset_name
self . current_preset_label . setText ( preset_name )
# Update colors without refreshing the entire list
self . update_preset_list_colors ( )
except Exception as e :
QMessageBox . warning ( self , " Preset Error " , f " Error applying preset: {str(e)} " )
@ -227,16 +275,28 @@ class PresetControls(QWidget):
@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 )
try :
if not item :
return
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 )
except RuntimeError :
# Item was deleted, disable all buttons
self . load_button . setEnabled ( False )
self . update_button . setEnabled ( False )
self . delete_button . setEnabled ( False )
self . rename_button . setEnabled ( False )
self . duplicate_button . setEnabled ( False )
self . export_button . setEnabled ( False )
@pyqtSlot ( QListWidgetItem )
def on_preset_double_clicked ( self , item ) :
@ -245,23 +305,30 @@ class PresetControls(QWidget):
@pyqtSlot ( )
def load_selected_preset ( self ) :
""" Load the selected preset """
""" Arm the selected preset for loading at pattern end """
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 )
try :
preset_name = current_item . text ( )
if preset_name in self . presets :
# Arm the preset instead of immediately applying it
self . arpeggiator . arm_preset ( self . presets [ preset_name ] )
# Visual feedback - orange for armed preset
current_item . setBackground ( Qt . darkYellow )
for i in range ( self . preset_list . count ( ) ) :
item = self . preset_list . item ( i )
if item and item != current_item :
# Keep current preset green, others transparent
if item . text ( ) == self . current_preset :
item . setBackground ( Qt . darkGreen )
else :
item . setBackground ( Qt . transparent )
except RuntimeError :
# Item was deleted, ignore
pass
@pyqtSlot ( )
def save_new_preset ( self ) :
@ -470,6 +537,18 @@ class PresetControls(QWidget):
self . refresh_preset_list ( )
def load_first_preset_on_startup ( self ) :
""" Automatically load the first preset on startup if available """
if self . preset_list . count ( ) > 0 :
# Get the first item (presets are sorted alphabetically)
first_item = self . preset_list . item ( 0 )
if first_item :
preset_name = first_item . text ( )
if preset_name in self . presets :
# Apply immediately on startup (not armed)
self . apply_preset_settings ( self . presets [ preset_name ] )
self . preset_list . setCurrentItem ( first_item )
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 " )
@ -488,6 +567,21 @@ class PresetControls(QWidget):
item . setBackground ( Qt . darkGreen )
self . preset_list . addItem ( item )
def update_preset_list_colors ( self ) :
""" Update preset list colors without recreating items """
try :
for i in range ( self . preset_list . count ( ) ) :
item = self . preset_list . item ( i )
if item and not item . isHidden ( ) :
preset_name = item . text ( )
if preset_name == self . current_preset :
item . setBackground ( Qt . darkGreen )
else :
item . setBackground ( Qt . transparent )
except RuntimeError :
# If items were deleted, just refresh the whole list
self . refresh_preset_list ( )
def get_current_timestamp ( self ) - > str :
""" Get current timestamp string """
from datetime import datetime
@ -507,4 +601,22 @@ class PresetControls(QWidget):
if self . preset_list . currentItem ( ) :
self . update_selected_preset ( )
else :
self . save_new_preset ( )
self . save_new_preset ( )
@pyqtSlot ( )
def force_apply_armed ( self ) :
""" Force apply any armed changes immediately """
self . arpeggiator . force_apply_armed_changes ( )
@pyqtSlot ( )
def clear_armed ( self ) :
""" Clear all armed changes without applying them """
self . arpeggiator . clear_all_armed_changes ( )
# Update UI to remove orange highlighting
self . update_preset_list_colors ( )
@pyqtSlot ( )
def on_armed_state_changed ( self ) :
""" Handle armed state changes """
# Update UI colors when armed state changes
self . update_preset_list_colors ( )