""" Arpeggiator Controls - READABLE BUTTONS WITH PROPER SIZING """ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QComboBox, QSlider, QSpinBox, QLabel, QPushButton, QFrame, QSizePolicy) from PyQt5.QtCore import Qt, pyqtSlot class ArpeggiatorControls(QWidget): """Readable arpeggiator controls with properly sized buttons""" def __init__(self, arpeggiator, channel_manager, simulator=None): super().__init__() self.arpeggiator = arpeggiator self.channel_manager = channel_manager self.simulator = simulator # State tracking self.presets = {} self.current_preset = None self.root_note_buttons = {} self.octave_buttons = {} self.scale_buttons = {} self.scale_notes_buttons = {} self.pattern_buttons = {} self.distribution_buttons = {} self.speed_buttons = {} self.pattern_length_buttons = {} self.note_limit_buttons = {} # Scaling support self.scale_factor = 1.0 self.all_buttons = [] # Track all buttons for scaling updates self.current_root_note = 0 self.current_octave = 4 self.current_scale = "major" self.current_pattern = "up" self.current_distribution = "up" self.current_speed = "1/8" self.current_pattern_length = 8 self.current_note_limit = 7 # Default to 7 (full scale) self.current_delay_timing = "1/4" # Armed state tracking self.armed_root_note_button = None self.armed_octave_button = None self.armed_scale_button = None self.armed_scale_note_button = None self.armed_pattern_button = None self.armed_distribution_button = None self.armed_pattern_length_button = None self.armed_note_limit_button = None # Speed changes apply immediately - no armed state needed self.setup_ui() self.connect_signals() def create_scalable_button(self, text, base_width=40, base_height=22, base_font_size=12, checkable=False, style_type="normal"): """Create a button with scalable sizing and styling""" button = QPushButton(text) button.setCheckable(checkable) # Set size policy for expansion button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # Set minimum size based on scaled values min_width = max(30, int(base_width * self.scale_factor)) min_height = max(18, int(base_height * self.scale_factor)) button.setMinimumSize(min_width, min_height) # Apply initial styling self.apply_button_style(button, base_font_size, style_type) # Track button for scaling updates self.all_buttons.append(button) return button def apply_button_style(self, button, base_font_size=12, style_type="normal"): """Apply scalable styling to a button""" font_size = max(8, int(base_font_size * self.scale_factor)) padding = max(1, int(3 * self.scale_factor)) if style_type == "active": button.setStyleSheet(f""" QPushButton {{ background: #00aa44; color: white; font-size: {font_size}px; font-weight: bold; padding: {padding}px; border: 1px solid #00cc55; }} QPushButton:hover {{ background: #00cc66; border: 1px solid #00ee77; }} """) elif style_type == "orange": button.setStyleSheet(f""" QPushButton {{ background: #cc6600; color: white; font-size: {font_size}px; font-weight: bold; padding: {padding}px; border: 1px solid #ee8800; }} QPushButton:hover {{ background: #ee7700; border: 1px solid #ffaa00; }} """) elif style_type == "blue": button.setStyleSheet(f""" QPushButton {{ background: #0066cc; color: white; font-size: {font_size}px; font-weight: bold; padding: {padding}px; border: 1px solid #0088ee; }} QPushButton:hover {{ background: #0088ee; border: 1px solid #00aaff; }} """) else: # normal button.setStyleSheet(f""" QPushButton {{ background: #3a3a3a; color: #ffffff; font-size: {font_size}px; font-weight: bold; padding: {padding}px; border: 1px solid #555555; }} QPushButton:hover {{ background: #505050; border: 1px solid #777777; }} """) def apply_scaling(self, scale_factor): """Apply new scaling factor to all buttons""" self.scale_factor = scale_factor # Update all tracked buttons for button in self.all_buttons: # Update minimum size current_size = button.minimumSize() new_width = max(30, int(40 * scale_factor)) # Base width 40 new_height = max(18, int(22 * scale_factor)) # Base height 22 button.setMinimumSize(new_width, new_height) # Re-apply styling with new scale # Determine button type by checking current style current_style = button.styleSheet() if "#00aa44" in current_style: self.apply_button_style(button, 12, "active") elif "#cc6600" in current_style: self.apply_button_style(button, 12, "orange") elif "#0066cc" in current_style: self.apply_button_style(button, 12, "blue") else: self.apply_button_style(button, 12, "normal") def setup_ui(self): """Clean quadrant layout with readable buttons""" # Main grid with better spacing and expansion main = QGridLayout(self) main.setSpacing(12) main.setContentsMargins(12, 12, 12, 12) # Equal quadrants with size policies basic_quad = self.basic_quadrant() basic_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) main.addWidget(basic_quad, 0, 0) dist_quad = self.distribution_quadrant() dist_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) main.addWidget(dist_quad, 0, 1) pattern_quad = self.pattern_quadrant() pattern_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) main.addWidget(pattern_quad, 1, 0) timing_quad = self.timing_quadrant() timing_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) main.addWidget(timing_quad, 1, 1) # Equal stretch for all quadrants main.setRowStretch(0, 1) main.setRowStretch(1, 1) main.setColumnStretch(0, 1) main.setColumnStretch(1, 1) def basic_quadrant(self): """Basic settings with readable buttons""" group = QGroupBox("Basic Settings") layout = QVBoxLayout(group) layout.setSpacing(6) layout.setContentsMargins(8, 8, 8, 8) # Root notes - 12 buttons in horizontal row, NO spacing between buttons layout.addWidget(QLabel("Root Note:")) notes_widget = QWidget() notes_layout = QHBoxLayout(notes_widget) notes_layout.setSpacing(0) # NO spacing between buttons notes_layout.setContentsMargins(0, 0, 0, 0) notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] for i, note in enumerate(notes): btn = self.create_scalable_button(note, 40, 22, 12, checkable=True, style_type="active" if i == 0 else "normal") btn.clicked.connect(lambda checked, n=i: self.on_root_note_clicked(n)) if i == 0: btn.setChecked(True) self.root_note_buttons[i] = btn notes_layout.addWidget(btn) layout.addWidget(notes_widget) # Octaves - 6 buttons in horizontal row, NO spacing between buttons layout.addWidget(QLabel("Octave:")) octave_widget = QWidget() octave_layout = QHBoxLayout(octave_widget) octave_layout.setSpacing(0) # NO spacing between buttons octave_layout.setContentsMargins(0, 0, 0, 0) for octave in range(3, 9): # C3 to C8 btn = self.create_scalable_button(f"C{octave}", 50, 22, 12, checkable=True, style_type="active" if octave == 4 else "normal") btn.clicked.connect(lambda checked, o=octave: self.on_octave_clicked(o)) if octave == 4: btn.setChecked(True) self.octave_buttons[octave] = btn octave_layout.addWidget(btn) layout.addWidget(octave_widget) # Scales - 2 rows of 4, minimal vertical spacing layout.addWidget(QLabel("Scale:")) scales_widget = QWidget() scales_layout = QGridLayout(scales_widget) scales_layout.setSpacing(0) # NO horizontal spacing scales_layout.setVerticalSpacing(2) # Minimal vertical spacing scales_layout.setContentsMargins(0, 0, 0, 0) main_scales = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "pentatonic_major", "pentatonic_minor"] for i, scale in enumerate(main_scales): display_name = scale.replace("_", " ").title() if len(display_name) > 10: display_name = display_name[:10] btn = self.create_scalable_button(display_name, 120, 22, 12, checkable=True, style_type="active" if scale == "major" else "normal") btn.clicked.connect(lambda checked, s=scale: self.on_scale_clicked(s)) if scale == "major": btn.setChecked(True) self.scale_buttons[scale] = btn scales_layout.addWidget(btn, i // 4, i % 4) layout.addWidget(scales_widget) # Scale notes selection layout.addWidget(QLabel("Scale Notes:")) scale_notes_widget = QWidget() self.scale_notes_layout = QGridLayout(scale_notes_widget) self.scale_notes_layout.setSpacing(2) self.scale_notes_buttons = {} self.current_scale_note_index = 0 # Start from root by default # Initially populate with major scale notes self.update_scale_notes_display() layout.addWidget(scale_notes_widget) # Octave range dropdown layout.addWidget(QLabel("Octave Range:")) self.octave_range_combo = QComboBox() self.octave_range_combo.setFixedHeight(30) for i in range(1, 5): self.octave_range_combo.addItem(f"{i} octave{'s' if i > 1 else ''}") layout.addWidget(self.octave_range_combo) return group def distribution_quadrant(self): """Distribution with readable buttons and simulator display""" group = QGroupBox("Channel Distribution") layout = QVBoxLayout(group) layout.setSpacing(6) layout.setContentsMargins(8, 8, 8, 8) layout.addWidget(QLabel("Distribution Pattern:")) # 2 rows of 4 distribution buttons dist_widget = QWidget() dist_layout = QGridLayout(dist_widget) dist_layout.setSpacing(0) # NO horizontal spacing dist_layout.setVerticalSpacing(2) # Minimal vertical spacing dist_layout.setContentsMargins(0, 0, 0, 0) patterns = ["up", "down", "up_down", "bounce", "random", "cycle", "alternating", "single_channel"] for i, pattern in enumerate(patterns): display_name = pattern.replace("_", " ").title() if len(display_name) > 12: display_name = display_name[:12] btn = QPushButton(display_name) btn.setFixedSize(120, 22) # Taller buttons for better readability btn.setCheckable(True) btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") btn.clicked.connect(lambda checked, p=pattern: self.on_distribution_clicked(p)) if pattern == "up": btn.setChecked(True) btn.setStyleSheet("background: #0066cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #0088ee;") self.distribution_buttons[pattern] = btn dist_layout.addWidget(btn, i // 4, i % 4) layout.addWidget(dist_widget) # Description self.dist_desc = QLabel("Channels: 1 → 2 → 3 → 4 → 5 → 6...") self.dist_desc.setStyleSheet("font-size: 10px; color: gray;") layout.addWidget(self.dist_desc) # Simulator display if self.simulator: from .simulator_display import SimulatorDisplay self.simulator_display = SimulatorDisplay(self.simulator, self.channel_manager) layout.addWidget(self.simulator_display) else: # Create placeholder for now placeholder = QLabel("Simulator display will appear here") placeholder.setStyleSheet("font-size: 10px; color: gray; text-align: center;") placeholder.setAlignment(Qt.AlignCenter) layout.addWidget(placeholder) return group def pattern_quadrant(self): """Pattern with readable buttons""" group = QGroupBox("Pattern Settings") layout = QVBoxLayout(group) layout.setSpacing(6) layout.setContentsMargins(8, 8, 8, 8) layout.addWidget(QLabel("Arpeggio Pattern:")) # 2 rows of 4 pattern buttons pattern_widget = QWidget() pattern_layout = QGridLayout(pattern_widget) pattern_layout.setSpacing(0) # NO horizontal spacing pattern_layout.setVerticalSpacing(2) # Minimal vertical spacing pattern_layout.setContentsMargins(0, 0, 0, 0) patterns = ["up", "down", "up_down", "down_up", "random", "chord", "note_order", "custom"] for i, pattern in enumerate(patterns): display_name = pattern.replace("_", " ").title() if len(display_name) > 12: display_name = display_name[:12] btn = QPushButton(display_name) btn.setFixedSize(120, 22) # Taller buttons for better readability btn.setCheckable(True) btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") btn.clicked.connect(lambda checked, p=pattern: self.on_pattern_clicked(p)) if pattern == "up": btn.setChecked(True) btn.setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;") self.pattern_buttons[pattern] = btn pattern_layout.addWidget(btn, i // 4, i % 4) layout.addWidget(pattern_widget) # Pattern length buttons layout.addWidget(QLabel("Pattern Length:")) length_widget = QWidget() length_layout = QGridLayout(length_widget) length_layout.setSpacing(0) # NO horizontal spacing length_layout.setVerticalSpacing(2) # Minimal vertical spacing length_layout.setContentsMargins(0, 0, 0, 0) self.pattern_length_buttons = {} for i in range(1, 17): # 1-16 pattern lengths btn = QPushButton(str(i)) btn.setFixedSize(30, 22) # Smaller buttons for numbers btn.setCheckable(True) btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") btn.clicked.connect(lambda checked, length=i: self.on_pattern_length_clicked(length)) if i == 8: # Default to 8 btn.setChecked(True) btn.setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;") self.pattern_length_buttons[i] = btn length_layout.addWidget(btn, i // 8, i % 8) # 2 rows of 8 layout.addWidget(length_widget) # Note limit buttons layout.addWidget(QLabel("Note Limit:")) note_limit_widget = QWidget() note_limit_layout = QGridLayout(note_limit_widget) note_limit_layout.setSpacing(0) # NO horizontal spacing note_limit_layout.setVerticalSpacing(2) # Minimal vertical spacing note_limit_layout.setContentsMargins(0, 0, 0, 0) self.note_limit_buttons = {} for i in range(1, 8): # 1-7 note limits (1-7 notes from scale) btn = self.create_scalable_button(str(i), 30, 22, 12, checkable=True, style_type="blue" if i == 7 else "normal") btn.clicked.connect(lambda checked, limit=i: self.on_note_limit_clicked(limit)) if i == 7: # Default to 7 (full scale) btn.setChecked(True) self.note_limit_buttons[i] = btn note_limit_layout.addWidget(btn, 0, i-1) # Single row layout.addWidget(note_limit_widget) return group def timing_quadrant(self): """Timing with readable controls""" group = QGroupBox("Timing Settings") layout = QVBoxLayout(group) layout.setSpacing(6) layout.setContentsMargins(8, 8, 8, 8) # Tempo tempo_layout = QHBoxLayout() tempo_layout.addWidget(QLabel("Tempo:")) self.tempo_spin = QSpinBox() self.tempo_spin.setRange(40, 200) self.tempo_spin.setValue(120) self.tempo_spin.setSuffix(" BPM") self.tempo_spin.setFixedHeight(30) tempo_layout.addWidget(self.tempo_spin) layout.addLayout(tempo_layout) # Speed buttons layout.addWidget(QLabel("Note Speed:")) speed_widget = QWidget() speed_layout = QHBoxLayout(speed_widget) speed_layout.setSpacing(0) # NO spacing between buttons speed_layout.setContentsMargins(0, 0, 0, 0) self.speed_buttons = {} speeds = ["1/32", "1/16", "1/8", "1/4", "1/2", "1/1"] for speed in speeds: btn = QPushButton(speed) btn.setFixedSize(50, 22) # Taller buttons for better readability btn.setCheckable(True) btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") btn.clicked.connect(lambda checked, s=speed: self.on_speed_clicked(s)) if speed == "1/8": btn.setChecked(True) btn.setStyleSheet("background: #9933cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;") self.speed_buttons[speed] = btn speed_layout.addWidget(btn) layout.addWidget(speed_widget) # Gate gate_layout = QHBoxLayout() gate_layout.addWidget(QLabel("Gate:")) self.gate_slider = QSlider(Qt.Horizontal) self.gate_slider.setRange(10, 200) self.gate_slider.setValue(100) self.gate_slider.setFixedHeight(25) gate_layout.addWidget(self.gate_slider) self.gate_label = QLabel("100%") self.gate_label.setFixedWidth(40) gate_layout.addWidget(self.gate_label) layout.addLayout(gate_layout) # Swing swing_layout = QHBoxLayout() swing_layout.addWidget(QLabel("Swing:")) self.swing_slider = QSlider(Qt.Horizontal) self.swing_slider.setRange(-100, 100) self.swing_slider.setValue(0) self.swing_slider.setFixedHeight(25) swing_layout.addWidget(self.swing_slider) self.swing_label = QLabel("0%") self.swing_label.setFixedWidth(40) swing_layout.addWidget(self.swing_label) layout.addLayout(swing_layout) # Velocity velocity_layout = QHBoxLayout() velocity_layout.addWidget(QLabel("Velocity:")) self.velocity_slider = QSlider(Qt.Horizontal) self.velocity_slider.setRange(1, 127) self.velocity_slider.setValue(80) self.velocity_slider.setFixedHeight(25) velocity_layout.addWidget(self.velocity_slider) self.velocity_label = QLabel("80") self.velocity_label.setFixedWidth(40) velocity_layout.addWidget(self.velocity_label) layout.addLayout(velocity_layout) # Delay/Echo controls delay_layout = QVBoxLayout() # Delay toggle self.delay_enabled = False delay_toggle_layout = QHBoxLayout() delay_toggle_layout.addWidget(QLabel("Delay/Echo:")) self.delay_toggle = QPushButton("OFF") self.delay_toggle.setFixedSize(50, 20) self.delay_toggle.setCheckable(True) self.delay_toggle.setStyleSheet("background: #5a2d2d; color: white; font-size: 10px; font-weight: bold;") self.delay_toggle.clicked.connect(self.on_delay_toggle) delay_toggle_layout.addWidget(self.delay_toggle) delay_toggle_layout.addStretch() delay_layout.addLayout(delay_toggle_layout) # Delay length (0-8 repeats) delay_length_layout = QHBoxLayout() delay_length_layout.addWidget(QLabel("Delay Length:")) self.delay_length_spin = QSpinBox() self.delay_length_spin.setRange(0, 8) self.delay_length_spin.setValue(3) self.delay_length_spin.setSuffix(" repeats") self.delay_length_spin.setFixedHeight(30) self.delay_length_spin.setEnabled(False) delay_length_layout.addWidget(self.delay_length_spin) delay_layout.addLayout(delay_length_layout) # Delay timing buttons (same as note speed) delay_timing_label = QLabel("Delay Timing:") delay_timing_label.setEnabled(False) delay_layout.addWidget(delay_timing_label) self.delay_timing_label = delay_timing_label delay_timing_widget = QWidget() delay_timing_layout = QGridLayout(delay_timing_widget) delay_timing_layout.setSpacing(0) delay_timing_layout.setContentsMargins(0, 0, 0, 0) self.delay_timing_buttons = {} delay_speeds = ["1/8", "1/8T", "1/4", "1/4T", "1/2", "1/2T", "1/1", "2/1", "2/1T", "4/1", "4/1T"] for i, speed in enumerate(delay_speeds): btn = QPushButton(speed) btn.setFixedSize(35, 18) # Slightly smaller to fit more btn.setCheckable(True) btn.setStyleSheet("background: #2a2a2a; color: #666666; font-size: 9px; font-weight: bold; padding: 0px; border: 1px solid #333333;") btn.setEnabled(False) btn.clicked.connect(lambda checked, s=speed: self.on_delay_timing_clicked(s)) if speed == "1/4": btn.setChecked(True) self.delay_timing_buttons[speed] = btn # Arrange in 2 rows (6 on top, 5 on bottom) row = i // 6 col = i % 6 delay_timing_layout.addWidget(btn, row, col) delay_timing_widget.setEnabled(False) self.delay_timing_widget = delay_timing_widget delay_layout.addWidget(delay_timing_widget) # Delay fade slider (percentage) delay_fade_layout = QHBoxLayout() delay_fade_label = QLabel("Delay Fade:") delay_fade_label.setEnabled(False) delay_fade_layout.addWidget(delay_fade_label) self.delay_fade_label = delay_fade_label self.delay_fade_slider = QSlider(Qt.Horizontal) self.delay_fade_slider.setRange(10, 90) self.delay_fade_slider.setValue(30) # 30% fade per repeat self.delay_fade_slider.setFixedHeight(25) self.delay_fade_slider.setEnabled(False) delay_fade_layout.addWidget(self.delay_fade_slider) self.delay_fade_value = QLabel("30%") self.delay_fade_value.setFixedWidth(40) self.delay_fade_value.setEnabled(False) delay_fade_layout.addWidget(self.delay_fade_value) delay_layout.addLayout(delay_fade_layout) layout.addLayout(delay_layout) # Presets preset_layout = QHBoxLayout() self.save_btn = QPushButton("Save Preset") self.save_btn.setFixedSize(80, 20) self.load_btn = QPushButton("Load Preset") self.load_btn.setFixedSize(80, 20) preset_layout.addWidget(self.save_btn) preset_layout.addWidget(self.load_btn) preset_layout.addStretch() layout.addLayout(preset_layout) return group def connect_signals(self): """Connect all signals""" self.tempo_spin.valueChanged.connect(self.on_tempo_changed) # Speed is now handled by individual button click handlers self.gate_slider.valueChanged.connect(self.on_gate_changed) self.swing_slider.valueChanged.connect(self.on_swing_changed) self.velocity_slider.valueChanged.connect(self.on_velocity_changed) self.delay_length_spin.valueChanged.connect(self.on_delay_length_changed) self.delay_fade_slider.valueChanged.connect(self.on_delay_fade_changed) self.octave_range_combo.currentIndexChanged.connect(self.on_octave_range_changed) self.save_btn.clicked.connect(self.save_preset) self.load_btn.clicked.connect(self.load_preset) if hasattr(self.arpeggiator, 'armed_state_changed'): self.arpeggiator.armed_state_changed.connect(self.update_armed_states) self.arpeggiator.settings_changed.connect(self.update_gui_from_engine) # Event handlers def on_root_note_clicked(self, note_index): midi_note = self.current_octave * 12 + note_index if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing: # ARMED STATE - button turns orange, waits for pattern end if self.armed_root_note_button: self.armed_root_note_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.armed_root_note_button = self.root_note_buttons[note_index] self.root_note_buttons[note_index].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") if hasattr(self.arpeggiator, 'arm_root_note'): self.arpeggiator.arm_root_note(midi_note) else: # IMMEDIATE CHANGE - apply right away if self.current_root_note in self.root_note_buttons: self.root_note_buttons[self.current_root_note].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_root_note = note_index self.root_note_buttons[note_index].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") # Update scale notes display when root note changes self.update_scale_notes_display() if hasattr(self.arpeggiator, 'set_root_note'): self.arpeggiator.set_root_note(midi_note) # Update starting scale note position self.update_arpeggiator_scale_note() def on_octave_clicked(self, octave): midi_note = octave * 12 + self.current_root_note if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing: # ARMED STATE - button turns orange if self.armed_octave_button: self.armed_octave_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.armed_octave_button = self.octave_buttons[octave] self.octave_buttons[octave].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") if hasattr(self.arpeggiator, 'arm_root_note'): self.arpeggiator.arm_root_note(midi_note) else: # IMMEDIATE CHANGE if self.current_octave in self.octave_buttons: self.octave_buttons[self.current_octave].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_octave = octave self.octave_buttons[octave].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") # Update scale notes display when octave changes self.update_scale_notes_display() if hasattr(self.arpeggiator, 'set_root_note'): self.arpeggiator.set_root_note(midi_note) # Update starting scale note position self.update_arpeggiator_scale_note() def on_scale_clicked(self, scale): if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing: # ARMED STATE - button turns orange if self.armed_scale_button: self.armed_scale_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.armed_scale_button = self.scale_buttons[scale] self.scale_buttons[scale].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") if hasattr(self.arpeggiator, 'arm_scale'): self.arpeggiator.arm_scale(scale) else: # IMMEDIATE CHANGE if self.current_scale in self.scale_buttons: self.scale_buttons[self.current_scale].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_scale = scale self.scale_buttons[scale].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") # Update scale notes display when scale changes self.current_scale_note_index = 0 # Reset to root when scale changes self.update_scale_notes_display() if hasattr(self.arpeggiator, 'set_scale'): self.arpeggiator.set_scale(scale) # Update starting scale note position self.update_arpeggiator_scale_note() def on_pattern_clicked(self, pattern): if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing: # ARMED STATE - button turns orange if self.armed_pattern_button: self.armed_pattern_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.armed_pattern_button = self.pattern_buttons[pattern] self.pattern_buttons[pattern].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") if hasattr(self.arpeggiator, 'arm_pattern_type'): self.arpeggiator.arm_pattern_type(pattern) else: # IMMEDIATE CHANGE if self.current_pattern in self.pattern_buttons: self.pattern_buttons[self.current_pattern].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_pattern = pattern self.pattern_buttons[pattern].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") if hasattr(self.arpeggiator, 'set_pattern_type'): self.arpeggiator.set_pattern_type(pattern) def on_distribution_clicked(self, pattern): if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing: # ARMED STATE - button turns orange if self.armed_distribution_button: self.armed_distribution_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.armed_distribution_button = self.distribution_buttons[pattern] self.distribution_buttons[pattern].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") if hasattr(self.arpeggiator, 'arm_channel_distribution'): self.arpeggiator.arm_channel_distribution(pattern) else: # IMMEDIATE CHANGE if self.current_distribution in self.distribution_buttons: self.distribution_buttons[self.current_distribution].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_distribution = pattern self.distribution_buttons[pattern].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") if hasattr(self.arpeggiator, 'set_channel_distribution'): self.arpeggiator.set_channel_distribution(pattern) def on_speed_clicked(self, speed): # Speed changes apply immediately (no armed state needed for timing) if self.current_speed in self.speed_buttons: self.speed_buttons[self.current_speed].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.current_speed = speed self.speed_buttons[speed].setStyleSheet("background: #9933cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;") if hasattr(self.arpeggiator, 'set_note_speed'): self.arpeggiator.set_note_speed(speed) def on_pattern_length_clicked(self, length): # Pattern length changes are armed (applied at pattern end) if self.armed_pattern_length_button: self.apply_button_style(self.armed_pattern_length_button, 12, "normal") self.armed_pattern_length_button = self.pattern_length_buttons[length] self.apply_button_style(self.pattern_length_buttons[length], 12, "orange") if hasattr(self.arpeggiator, 'set_pattern_length'): self.arpeggiator.set_pattern_length(length) def on_note_limit_clicked(self, limit): """Handle note limit button clicks (armed - applied at pattern end)""" if self.armed_note_limit_button: self.apply_button_style(self.armed_note_limit_button, 12, "normal") self.armed_note_limit_button = self.note_limit_buttons[limit] self.apply_button_style(self.note_limit_buttons[limit], 12, "orange") if hasattr(self.arpeggiator, 'set_note_limit'): self.arpeggiator.set_note_limit(limit) def on_delay_toggle(self): """Handle delay on/off toggle""" self.delay_enabled = self.delay_toggle.isChecked() if self.delay_enabled: self.delay_toggle.setText("ON") self.delay_toggle.setStyleSheet("background: #2d5a2d; color: white; font-size: 10px; font-weight: bold;") # Enable all delay controls self.delay_length_spin.setEnabled(True) self.delay_timing_label.setEnabled(True) self.delay_timing_widget.setEnabled(True) self.delay_fade_label.setEnabled(True) self.delay_fade_slider.setEnabled(True) self.delay_fade_value.setEnabled(True) # Enable timing buttons and update their style for btn in self.delay_timing_buttons.values(): btn.setEnabled(True) if btn.isChecked(): btn.setStyleSheet("background: #9933cc; color: white; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;") else: btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #555555;") else: self.delay_toggle.setText("OFF") self.delay_toggle.setStyleSheet("background: #5a2d2d; color: white; font-size: 10px; font-weight: bold;") # Disable all delay controls self.delay_length_spin.setEnabled(False) self.delay_timing_label.setEnabled(False) self.delay_timing_widget.setEnabled(False) self.delay_fade_label.setEnabled(False) self.delay_fade_slider.setEnabled(False) self.delay_fade_value.setEnabled(False) # Disable timing buttons and dim their style for btn in self.delay_timing_buttons.values(): btn.setEnabled(False) btn.setStyleSheet("background: #2a2a2a; color: #666666; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #333333;") if hasattr(self.arpeggiator, 'set_delay_enabled'): self.arpeggiator.set_delay_enabled(self.delay_enabled) def on_delay_timing_clicked(self, timing): """Handle delay timing button clicks""" if self.current_delay_timing in self.delay_timing_buttons: self.delay_timing_buttons[self.current_delay_timing].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #555555;") self.current_delay_timing = timing self.delay_timing_buttons[timing].setStyleSheet("background: #9933cc; color: white; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;") if hasattr(self.arpeggiator, 'set_delay_timing'): self.arpeggiator.set_delay_timing(timing) def on_delay_length_changed(self, length): """Handle delay length changes""" if hasattr(self.arpeggiator, 'set_delay_length'): self.arpeggiator.set_delay_length(length) def on_delay_fade_changed(self, fade_percent): """Handle delay fade changes""" self.delay_fade_value.setText(f"{fade_percent}%") if hasattr(self.arpeggiator, 'set_delay_fade'): self.arpeggiator.set_delay_fade(fade_percent / 100.0) # Convert to 0-1 range @pyqtSlot(int) def on_tempo_changed(self, tempo): if hasattr(self.arpeggiator, 'set_tempo'): self.arpeggiator.set_tempo(float(tempo)) # on_speed_changed removed - now using on_speed_clicked with buttons @pyqtSlot(int) def on_gate_changed(self, value): self.gate_label.setText(f"{value}%") if hasattr(self.arpeggiator, 'set_gate'): self.arpeggiator.set_gate(value / 100.0) @pyqtSlot(int) def on_swing_changed(self, value): self.swing_label.setText(f"{value}%") if hasattr(self.arpeggiator, 'set_swing'): self.arpeggiator.set_swing(value / 100.0) @pyqtSlot(int) def on_velocity_changed(self, value): self.velocity_label.setText(str(value)) if hasattr(self.arpeggiator, 'set_velocity'): self.arpeggiator.set_velocity(value) @pyqtSlot(int) def on_octave_range_changed(self, index): if hasattr(self.arpeggiator, 'set_octave_range'): self.arpeggiator.set_octave_range(index + 1) def save_preset(self): preset_name = f"Preset_{len(self.presets) + 1}" self.presets[preset_name] = { 'root_note': self.current_root_note, 'octave': self.current_octave, 'scale': self.current_scale, 'pattern': self.current_pattern, 'distribution': self.current_distribution } print(f"Saved {preset_name}") def load_preset(self): if not self.presets: print("No presets saved") return preset_name = list(self.presets.keys())[0] preset = self.presets[preset_name] # Apply preset logic here print(f"Loaded {preset_name}") @pyqtSlot() def update_armed_states(self): """Handle armed state updates - orange buttons become green at pattern end""" # Check if armed states were applied (armed values become None when applied) # Root note armed -> active if self.armed_root_note_button and hasattr(self.arpeggiator, 'armed_root_note') and self.arpeggiator.armed_root_note is None: # Find which note this was for note_index, btn in self.root_note_buttons.items(): if btn == self.armed_root_note_button: # Clear old active if self.current_root_note in self.root_note_buttons: self.root_note_buttons[self.current_root_note].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") # Set new active (orange -> green) self.current_root_note = note_index btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") self.armed_root_note_button = None # Update scale notes display when root note changes self.update_scale_notes_display() self.update_arpeggiator_scale_note() # Sync with engine break # Octave armed -> active if self.armed_octave_button and hasattr(self.arpeggiator, 'armed_root_note') and self.arpeggiator.armed_root_note is None: for octave, btn in self.octave_buttons.items(): if btn == self.armed_octave_button: if self.current_octave in self.octave_buttons: self.octave_buttons[self.current_octave].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_octave = octave btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") self.armed_octave_button = None # Update scale notes display when octave changes self.update_scale_notes_display() self.update_arpeggiator_scale_note() # Sync with engine break # Scale armed -> active if self.armed_scale_button and hasattr(self.arpeggiator, 'armed_scale') and self.arpeggiator.armed_scale is None: for scale, btn in self.scale_buttons.items(): if btn == self.armed_scale_button: if self.current_scale in self.scale_buttons: self.scale_buttons[self.current_scale].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_scale = scale btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") self.armed_scale_button = None # Update scale notes display when scale changes self.current_scale_note_index = 0 # Reset to root when scale changes self.update_scale_notes_display() self.update_arpeggiator_scale_note() # Sync with engine break # Pattern armed -> active if self.armed_pattern_button and hasattr(self.arpeggiator, 'armed_pattern_type') and self.arpeggiator.armed_pattern_type is None: for pattern, btn in self.pattern_buttons.items(): if btn == self.armed_pattern_button: if self.current_pattern in self.pattern_buttons: self.pattern_buttons[self.current_pattern].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_pattern = pattern btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") self.armed_pattern_button = None break # Distribution armed -> active if self.armed_distribution_button and hasattr(self.arpeggiator, 'armed_channel_distribution') and self.arpeggiator.armed_channel_distribution is None: for distribution, btn in self.distribution_buttons.items(): if btn == self.armed_distribution_button: if self.current_distribution in self.distribution_buttons: self.distribution_buttons[self.current_distribution].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") self.current_distribution = distribution btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") self.armed_distribution_button = None break # Scale note armed -> active if self.armed_scale_note_button and hasattr(self.arpeggiator, 'armed_scale_note_start') and self.arpeggiator.armed_scale_note_start is None: for scale_note_index, btn in self.scale_notes_buttons.items(): if btn == self.armed_scale_note_button: # Clear old active scale note if self.current_scale_note_index in self.scale_notes_buttons: self.scale_notes_buttons[self.current_scale_note_index].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;") # Set new active scale note (orange -> blue) self.current_scale_note_index = scale_note_index btn.setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;") self.armed_scale_note_button = None break # Pattern length armed -> active if self.armed_pattern_length_button and hasattr(self.arpeggiator, 'armed_pattern_length') and self.arpeggiator.armed_pattern_length is None: for length, btn in self.pattern_length_buttons.items(): if btn == self.armed_pattern_length_button: # Clear old active pattern length if self.current_pattern_length in self.pattern_length_buttons: self.apply_button_style(self.pattern_length_buttons[self.current_pattern_length], 12, "normal") # Set new active pattern length (orange -> orange) self.current_pattern_length = length self.apply_button_style(btn, 12, "orange") self.armed_pattern_length_button = None break # Note limit armed -> active if self.armed_note_limit_button and hasattr(self.arpeggiator, 'armed_note_limit') and self.arpeggiator.armed_note_limit is None: for limit, btn in self.note_limit_buttons.items(): if btn == self.armed_note_limit_button: # Clear old active note limit if self.current_note_limit in self.note_limit_buttons: self.apply_button_style(self.note_limit_buttons[self.current_note_limit], 12, "normal") # Set new active note limit (orange -> blue) self.current_note_limit = limit self.apply_button_style(btn, 12, "blue") self.armed_note_limit_button = None break # Speed changes apply immediately - no armed state needed def update_gui_from_engine(self): """Update all GUI controls to match engine settings""" try: # Update scale buttons if hasattr(self, 'scale_buttons'): # Clear current scale styling if hasattr(self, 'current_scale') and self.current_scale in self.scale_buttons: self.scale_buttons[self.current_scale].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") # Set new active scale self.current_scale = self.arpeggiator.scale if self.current_scale in self.scale_buttons: self.scale_buttons[self.current_scale].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") # Update pattern buttons if hasattr(self, 'pattern_buttons'): # Clear current pattern styling if hasattr(self, 'current_pattern') and self.current_pattern in self.pattern_buttons: self.pattern_buttons[self.current_pattern].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;") # Set new active pattern self.current_pattern = self.arpeggiator.pattern_type if self.current_pattern in self.pattern_buttons: self.pattern_buttons[self.current_pattern].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;") # Update scale note buttons if hasattr(self, 'scale_notes_buttons'): # Clear current scale note styling if hasattr(self, 'current_scale_note_index') and self.current_scale_note_index in self.scale_notes_buttons: self.scale_notes_buttons[self.current_scale_note_index].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;") # Set new active scale note self.current_scale_note_index = getattr(self.arpeggiator, 'scale_note_start', 0) if self.current_scale_note_index in self.scale_notes_buttons: self.scale_notes_buttons[self.current_scale_note_index].setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;") # Update note speed buttons if hasattr(self, 'speed_buttons'): # Clear current speed styling if hasattr(self, 'current_speed') and self.current_speed in self.speed_buttons: self.speed_buttons[self.current_speed].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") # Set new active speed self.current_speed = self.arpeggiator.note_speed if self.current_speed in self.speed_buttons: self.speed_buttons[self.current_speed].setStyleSheet("background: #9933cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;") # Update pattern length buttons if hasattr(self, 'pattern_length_buttons'): # Clear current pattern length styling if hasattr(self, 'current_pattern_length') and self.current_pattern_length in self.pattern_length_buttons: self.apply_button_style(self.pattern_length_buttons[self.current_pattern_length], 12, "normal") # Set new active pattern length if hasattr(self.arpeggiator, 'user_pattern_length'): self.current_pattern_length = self.arpeggiator.user_pattern_length if self.current_pattern_length in self.pattern_length_buttons: self.apply_button_style(self.pattern_length_buttons[self.current_pattern_length], 12, "orange") # Update note limit buttons if hasattr(self, 'note_limit_buttons'): # Clear current note limit styling if hasattr(self, 'current_note_limit') and self.current_note_limit in self.note_limit_buttons: self.apply_button_style(self.note_limit_buttons[self.current_note_limit], 12, "normal") # Set new active note limit if hasattr(self.arpeggiator, 'note_limit'): self.current_note_limit = self.arpeggiator.note_limit if self.current_note_limit in self.note_limit_buttons: self.apply_button_style(self.note_limit_buttons[self.current_note_limit], 12, "blue") # Update delay controls if hasattr(self, 'delay_enabled_checkbox'): self.delay_enabled_checkbox.setChecked(self.arpeggiator.delay_enabled) if hasattr(self, 'delay_length_spin'): self.delay_length_spin.setValue(self.arpeggiator.delay_length) if hasattr(self, 'delay_fade_slider'): self.delay_fade_slider.setValue(int(self.arpeggiator.delay_fade * 100)) # Update sliders and spinboxes if hasattr(self, 'gate_slider'): self.gate_slider.setValue(int(self.arpeggiator.gate * 100)) if hasattr(self, 'swing_slider'): self.swing_slider.setValue(int(self.arpeggiator.swing * 100)) if hasattr(self, 'velocity_slider'): self.velocity_slider.setValue(self.arpeggiator.velocity) if hasattr(self, 'octave_range_combo'): self.octave_range_combo.setCurrentIndex(self.arpeggiator.octave_range - 1) if hasattr(self, 'tempo_spin'): self.tempo_spin.setValue(int(self.arpeggiator.tempo)) except Exception as e: print(f"Error updating GUI from engine: {e}") def update_scale_notes_display(self): """Update the scale notes buttons based on current root note and scale""" # Clear existing buttons for button in self.scale_notes_buttons.values(): button.deleteLater() self.scale_notes_buttons.clear() # Get the scale definition scale_intervals = self.arpeggiator.SCALES.get(self.current_scale, [0, 2, 4, 5, 7, 9, 11]) # Note names for display note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] # Calculate the actual MIDI notes for this scale root_midi = self.current_octave * 12 + self.current_root_note scale_notes = [] for interval in scale_intervals: scale_notes.append(root_midi + interval) # Create buttons for each scale note for i, midi_note in enumerate(scale_notes): note_name = note_names[midi_note % 12] octave = midi_note // 12 display_text = f"{note_name}{octave}" btn = QPushButton(display_text) btn.setFixedSize(50, 25) btn.setCheckable(True) btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;") btn.clicked.connect(lambda checked, idx=i: self.on_scale_note_clicked(idx)) # Set first note (root) as selected by default if i == self.current_scale_note_index: btn.setChecked(True) btn.setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;") self.scale_notes_buttons[i] = btn self.scale_notes_layout.addWidget(btn, 0, i) def on_scale_note_clicked(self, scale_note_index): """Handle scale note selection with armed state support""" if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing: # ARMED STATE - button turns orange, waits for pattern end if self.armed_scale_note_button: # Reset previous armed button old_armed_index = None for idx, btn in self.scale_notes_buttons.items(): if btn == self.armed_scale_note_button: old_armed_index = idx break if old_armed_index is not None: if old_armed_index == self.current_scale_note_index: # It was the current active note, make it blue again self.armed_scale_note_button.setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;") else: # It was just armed, make it gray again self.armed_scale_note_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;") # Set new armed button to orange self.armed_scale_note_button = self.scale_notes_buttons[scale_note_index] self.scale_notes_buttons[scale_note_index].setStyleSheet("background: #ff8800; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") # Arm the scale note change in the engine if hasattr(self.arpeggiator, 'arm_scale_note_start'): self.arpeggiator.arm_scale_note_start(scale_note_index) else: # IMMEDIATE CHANGE - apply right away old_index = self.current_scale_note_index self.current_scale_note_index = scale_note_index # Update button styling if old_index in self.scale_notes_buttons: self.scale_notes_buttons[old_index].setChecked(False) self.scale_notes_buttons[old_index].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;") if scale_note_index in self.scale_notes_buttons: self.scale_notes_buttons[scale_note_index].setChecked(True) self.scale_notes_buttons[scale_note_index].setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;") # Update arpeggiator engine with new starting scale note self.update_arpeggiator_scale_note() def update_arpeggiator_scale_note(self): """Update the arpeggiator engine with the selected scale note starting position""" if hasattr(self.arpeggiator, 'set_scale_note_start'): self.arpeggiator.set_scale_note_start(self.current_scale_note_index)