""" Main Window GUI Primary application window with all controls and displays. Integrates all GUI components into a cohesive interface. """ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QSlider, QComboBox, QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame, QSizePolicy) from PyQt5.QtCore import Qt, QTimer, pyqtSlot, QSize from PyQt5.QtGui import QFont, QPalette, QColor, QKeySequence from .arpeggiator_controls import ArpeggiatorControls from .channel_controls import ChannelControls from .volume_controls import VolumeControls from .simulator_display import SimulatorDisplay from .output_controls import OutputControls from .preset_controls import PresetControls class ScalingUtils: """Utility class for handling dynamic GUI scaling""" @staticmethod def get_scale_factor(widget): """Calculate scale factor based on widget size""" # Base size is 1200x800 base_width = 1200 base_height = 800 # Get actual widget size actual_size = widget.size() width_factor = actual_size.width() / base_width height_factor = actual_size.height() / base_height # Use the smaller factor to maintain proportions scale_factor = min(width_factor, height_factor) # Clamp between 0.8 and 3.0 for reasonable bounds return max(0.8, min(3.0, scale_factor)) @staticmethod def scale_font_size(base_size, scale_factor): """Scale font size with factor""" return max(8, int(base_size * scale_factor)) @staticmethod def scale_button_size(base_width, base_height, scale_factor): """Scale button dimensions with factor""" return ( max(30, int(base_width * scale_factor)), max(20, int(base_height * scale_factor)) ) @staticmethod def apply_scalable_button_style(button, base_font_size=12, scale_factor=1.0): """Apply scalable styling to a button""" font_size = ScalingUtils.scale_font_size(base_font_size, scale_factor) padding = max(2, int(5 * scale_factor)) button.setStyleSheet(f""" QPushButton {{ background: #3a3a3a; color: #ffffff; font-size: {font_size}px; font-weight: bold; border: 1px solid #555555; padding: {padding}px; min-height: {max(18, int(22 * scale_factor))}px; }} QPushButton:hover {{ background: #505050; border: 1px solid #777777; }} """) # Set size policy to allow expansion button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) class MainWindow(QMainWindow): """ Main application window containing all GUI components. Provides organized layout and coordinates between different control panels. """ def __init__(self, arpeggiator, channel_manager, volume_engine, output_manager, simulator, maschine_controller=None): super().__init__() self.arpeggiator = arpeggiator self.channel_manager = channel_manager self.volume_engine = volume_engine self.output_manager = output_manager self.simulator = simulator self.maschine_controller = maschine_controller self.setWindowTitle("MIDI Arpeggiator - Lighting Controller") self.setMinimumSize(1200, 800) # Scaling support self.scale_factor = 1.0 self.scaling_enabled = True # Keyboard note mapping self.keyboard_notes = { Qt.Key_A: 60, # C Qt.Key_W: 61, # C# Qt.Key_S: 62, # D Qt.Key_E: 63, # D# Qt.Key_D: 64, # E Qt.Key_F: 65, # F Qt.Key_T: 66, # F# Qt.Key_G: 67, # G Qt.Key_Y: 68, # G# Qt.Key_H: 69, # A Qt.Key_U: 70, # A# Qt.Key_J: 71, # B Qt.Key_K: 72, # C (next octave) } self.held_keys = set() self.setup_ui() self.setup_connections() self.apply_dark_theme() def setup_ui(self): """Initialize the user interface""" central_widget = QWidget() self.setCentralWidget(central_widget) # Create main layout with full-window tabs main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(8, 8, 8, 8) main_layout.setSpacing(8) # Transport controls at top transport_frame = self.create_transport_controls() main_layout.addWidget(transport_frame) # Create tabbed interface that fills the window tab_widget = QTabWidget() tab_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) main_layout.addWidget(tab_widget, 1) # Give tab widget all extra space # Arpeggiator tab with quadrant layout self.arp_controls = ArpeggiatorControls(self.arpeggiator, self.channel_manager, self.simulator) tab_widget.addTab(self.arp_controls, "Arpeggiator") # Channels tab self.channel_controls = ChannelControls(self.channel_manager, self.output_manager) tab_widget.addTab(self.channel_controls, "Channels") # Volume/Lighting tab self.volume_controls = VolumeControls(self.volume_engine) tab_widget.addTab(self.volume_controls, "Volume/Lighting") # Output tab self.output_controls = OutputControls(self.output_manager) tab_widget.addTab(self.output_controls, "Output") # Simulator display now integrated into arpeggiator tab - removed standalone tab # Presets tab self.preset_controls = PresetControls(self.arpeggiator, self.channel_manager, self.volume_engine) tab_widget.addTab(self.preset_controls, "Presets") # Set up preset callback for armed preset system self.arpeggiator.set_preset_apply_callback(self.preset_controls.apply_preset_settings) # Status display at bottom status_frame = self.create_status_display() main_layout.addWidget(status_frame) # Create status bar self.statusBar().showMessage("Ready - Use keyboard (AWSDFGTGHYUJ) to play notes, SPACE to start/stop") # Create menu bar self.create_menu_bar() # Removed create_control_panel and create_display_panel methods - now using direct tab layout def create_transport_controls(self) -> QFrame: """Create transport control buttons""" frame = QFrame() frame.setFrameStyle(QFrame.Box) frame.setMaximumHeight(60) # Limit height to just accommodate buttons layout = QHBoxLayout(frame) # Play/Stop buttons self.play_button = QPushButton("▶ PLAY") self.play_button.setObjectName("playButton") self.stop_button = QPushButton("⏹ STOP") self.stop_button.setObjectName("stopButton") self.panic_button = QPushButton("⚠ PANIC") self.panic_button.setObjectName("panicButton") # Style buttons button_style = """ QPushButton { font-size: 14px; font-weight: bold; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #404040; } """ play_style = button_style + """ QPushButton { background-color: #2d5a2d; color: white; } """ stop_style = button_style + """ QPushButton { background-color: #5a2d2d; color: white; } """ panic_style = button_style + """ QPushButton { background-color: #5a2d5a; color: white; } """ self.play_button.setStyleSheet(play_style) self.stop_button.setStyleSheet(stop_style) self.panic_button.setStyleSheet(panic_style) # Connect buttons self.play_button.clicked.connect(self.on_play_clicked) self.stop_button.clicked.connect(self.on_stop_clicked) self.panic_button.clicked.connect(self.on_panic_clicked) # Add to layout layout.addWidget(self.play_button) layout.addWidget(self.stop_button) layout.addWidget(self.panic_button) layout.addStretch() # Tempo display tempo_label = QLabel("Tempo:") self.tempo_display = QLabel("120 BPM") self.tempo_display.setStyleSheet("font-weight: bold; font-size: 14px;") layout.addWidget(tempo_label) layout.addWidget(self.tempo_display) return frame def create_status_display(self) -> QFrame: """Create status information display""" frame = QFrame() frame.setFrameStyle(QFrame.Box) frame.setMaximumHeight(60) layout = QHBoxLayout(frame) # Output mode indicator self.mode_indicator = QLabel("Mode: Simulator") self.mode_indicator.setStyleSheet("font-weight: bold; color: #00aa00;") # Connection status self.connection_status = QLabel("Connected") self.connection_status.setStyleSheet("color: #00aa00;") # Active voices count self.voices_display = QLabel("Voices: 0") layout.addWidget(QLabel("Status:")) layout.addWidget(self.mode_indicator) layout.addWidget(QFrame()) # Separator layout.addWidget(self.connection_status) layout.addWidget(QFrame()) # Separator layout.addWidget(self.voices_display) layout.addStretch() return frame def create_menu_bar(self): """Create application menu bar""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu('File') file_menu.addAction('New Preset', self.preset_controls.new_preset) file_menu.addAction('Load Preset', self.preset_controls.load_preset) file_menu.addAction('Save Preset', self.preset_controls.save_preset) file_menu.addSeparator() file_menu.addAction('Exit', self.close) # View menu view_menu = menubar.addMenu('View') view_menu.addAction('Reset Layout', self.reset_layout) # MIDI menu midi_menu = menubar.addMenu('MIDI') midi_menu.addAction('Refresh Devices', self.output_controls.refresh_midi_devices) midi_menu.addAction('Panic (All Notes Off)', self.on_panic_clicked) # Help menu help_menu = menubar.addMenu('Help') help_menu.addAction('About', self.show_about) def setup_connections(self): """Connect signals and slots""" # Arpeggiator signals self.arpeggiator.playing_state_changed.connect(self.on_playing_state_changed) self.arpeggiator.tempo_changed.connect(self.on_tempo_changed) # Output manager signals self.output_manager.mode_changed.connect(self.on_output_mode_changed) # Simulator signals now only connected to embedded display in arpeggiator controls # Connect signals to embedded simulator display in arpeggiator controls if hasattr(self.arp_controls, 'simulator_display'): self.channel_manager.active_synth_count_changed.connect( self.arp_controls.simulator_display.set_synth_count ) # Disabled lighting_updated connection - using MIDI volume changes instead # self.simulator.lighting_updated.connect( # self.arp_controls.simulator_display.update_lighting # ) self.arpeggiator.note_triggered.connect( self.arp_controls.simulator_display.on_note_played ) # Connect MIDI volume changes to display self.output_manager.volume_sent.connect( self.arp_controls.simulator_display.on_midi_volume_changed ) # Initialize display with current volume values for channel in range(1, 17): current_volume = self.output_manager.get_channel_volume(channel) self.arp_controls.simulator_display.on_midi_volume_changed(channel, current_volume) # Update timer for status display self.status_timer = QTimer() self.status_timer.timeout.connect(self.update_status_display) self.status_timer.start(100) # Update 10 times per second def apply_dark_theme(self): """Apply modern dark theme optimized for live performance""" dark_palette = QPalette() # Modern dark colors dark_palette.setColor(QPalette.Window, QColor(32, 32, 36)) dark_palette.setColor(QPalette.WindowText, QColor(255, 255, 255)) dark_palette.setColor(QPalette.Base, QColor(18, 18, 20)) dark_palette.setColor(QPalette.AlternateBase, QColor(42, 42, 46)) dark_palette.setColor(QPalette.ToolTipBase, QColor(0, 0, 0)) dark_palette.setColor(QPalette.ToolTipText, QColor(255, 255, 255)) dark_palette.setColor(QPalette.Text, QColor(255, 255, 255)) dark_palette.setColor(QPalette.Button, QColor(48, 48, 52)) dark_palette.setColor(QPalette.ButtonText, QColor(255, 255, 255)) dark_palette.setColor(QPalette.BrightText, QColor(255, 100, 100)) dark_palette.setColor(QPalette.Link, QColor(100, 200, 255)) dark_palette.setColor(QPalette.Highlight, QColor(0, 150, 255)) dark_palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) self.setPalette(dark_palette) # Modern performance-oriented styling self.setStyleSheet(""" /* Main Window */ QMainWindow { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #202024, stop:1 #181820); } /* Tabs - More prominent for live use */ QTabWidget::pane { border: 2px solid #00aaff; border-radius: 8px; background-color: #2a2a2e; } QTabBar::tab { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #404044, stop:1 #353538); color: white; padding: 12px 20px; margin: 2px; border-top-left-radius: 8px; border-top-right-radius: 8px; min-width: 100px; font-size: 12px; font-weight: bold; } QTabBar::tab:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #0099ff, stop:1 #0077cc); color: white; border: 2px solid #00aaff; } QTabBar::tab:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505054, stop:1 #454548); } /* Group Boxes - Better organization */ QGroupBox { font-weight: bold; font-size: 13px; color: #ffffff; border: 2px solid #00aaff; border-radius: 10px; margin-top: 20px; padding-top: 15px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2a2a2e, stop:1 #242428); } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; color: #00aaff; background-color: #2a2a2e; border-radius: 5px; } /* Buttons - Individual styling only */ QPushButton { color: white; font-weight: bold; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505054, stop:1 #454548); border: 2px solid #00aaff; } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #353538, stop:1 #2a2a2e); border: 2px solid #0088cc; } /* Combo Boxes - Cleaner look */ QComboBox { background: #353538; border: 2px solid #555555; border-radius: 5px; color: white; padding: 5px 10px; min-width: 120px; } QComboBox:hover { border: 2px solid #00aaff; } QComboBox::drop-down { border: none; width: 20px; } QComboBox::down-arrow { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid white; margin-right: 5px; } /* Sliders - Better for real-time control */ QSlider::groove:horizontal { border: 1px solid #555555; height: 8px; background: #2a2a2e; border-radius: 4px; } QSlider::handle:horizontal { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #00aaff, stop:1 #0088cc); border: 2px solid #ffffff; width: 20px; margin: -8px 0; border-radius: 10px; } QSlider::handle:horizontal:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #00ccff, stop:1 #00aacc); } /* Spin Boxes */ QSpinBox { background: #353538; border: 2px solid #555555; border-radius: 5px; color: white; padding: 5px; min-width: 60px; } QSpinBox:hover { border: 2px solid #00aaff; } /* Labels - Better contrast */ QLabel { color: #ffffff; font-size: 11px; } /* Status Bar - More prominent */ QStatusBar { background: #1a1a1e; color: #00aaff; border-top: 1px solid #555555; font-weight: bold; padding: 5px; } /* Transport Controls - Special styling */ QPushButton#playButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2d5a2d, stop:1 #1a3d1a); border: 2px solid #4a8a4a; color: #aaffaa; font-size: 16px; font-weight: bold; min-width: 100px; min-height: 40px; } QPushButton#playButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3d6a3d, stop:1 #2a4d2a); border: 2px solid #5aaa5a; } QPushButton#stopButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a2d2d, stop:1 #3d1a1a); border: 2px solid #8a4a4a; color: #ffaaaa; font-size: 16px; font-weight: bold; min-width: 100px; min-height: 40px; } QPushButton#stopButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #6a3d3d, stop:1 #4d2a2a); border: 2px solid #aa5a5a; } QPushButton#panicButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a2d5a, stop:1 #3d1a3d); border: 2px solid #8a4a8a; color: #ffaaff; font-size: 16px; font-weight: bold; min-width: 100px; min-height: 40px; } QPushButton#panicButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #6a3d6a, stop:1 #4d2a4d); border: 2px solid #aa5aaa; } """) @pyqtSlot() def on_play_clicked(self): """Handle play button click""" # If no notes are held, add some default notes for testing if not self.arpeggiator.held_notes: # Add a C major chord for testing self.arpeggiator.note_on(60) # C self.arpeggiator.note_on(64) # E self.arpeggiator.note_on(67) # G self.statusBar().showMessage("Added test notes (C major chord)", 2000) success = self.arpeggiator.start() if not success: self.statusBar().showMessage("Could not start arpeggiator", 3000) @pyqtSlot() def on_stop_clicked(self): """Handle stop button click""" self.arpeggiator.stop() @pyqtSlot() def on_panic_clicked(self): """Handle panic button click""" self.output_manager.send_panic() self.statusBar().showMessage("Panic sent - all notes off", 2000) @pyqtSlot(bool) def on_playing_state_changed(self, is_playing): """Handle arpeggiator play state change""" if is_playing: self.play_button.setText("⏸ Pause") self.statusBar().showMessage("Arpeggiator playing") else: self.play_button.setText("▶ Play") self.statusBar().showMessage("Arpeggiator stopped") @pyqtSlot(float) def on_tempo_changed(self, tempo): """Handle tempo change""" self.tempo_display.setText(f"{tempo:.1f} BPM") @pyqtSlot(str) def on_output_mode_changed(self, mode): """Handle output mode change""" if mode == "simulator": self.mode_indicator.setText("Mode: Simulator") self.mode_indicator.setStyleSheet("font-weight: bold; color: #00aa00;") else: self.mode_indicator.setText("Mode: Hardware") self.mode_indicator.setStyleSheet("font-weight: bold; color: #aaaa00;") def update_status_display(self): """Update status display information""" # Update connection status if self.output_manager.is_connected(): self.connection_status.setText("Connected") self.connection_status.setStyleSheet("color: #00aa00;") else: self.connection_status.setText("Disconnected") self.connection_status.setStyleSheet("color: #aa0000;") # Update active voices count if self.output_manager.current_mode == "simulator": voice_count = self.simulator.get_active_voices_count() else: voice_count = sum(len(voices) for voices in self.channel_manager.active_voices.values()) self.voices_display.setText(f"Voices: {voice_count}") def reset_layout(self): """Reset window layout to default""" # This could restore default sizes, positions, etc. self.statusBar().showMessage("Layout reset", 2000) def show_about(self): """Show about dialog""" from PyQt5.QtWidgets import QMessageBox QMessageBox.about(self, "About MIDI Arpeggiator", "MIDI Arpeggiator with Lighting Control\n\n" "A modular arpeggiator for controlling synthesizers\n" "and synchronized lighting effects.\n\n" "Features:\n" "• FL Studio-style arpeggiator patterns\n" "• Multi-synth routing and voice management\n" "• Volume/brightness pattern generation\n" "• Built-in simulator mode\n" "• Native Instruments Maschine integration") def keyPressEvent(self, event): """Handle key press for note input""" key = event.key() # Avoid key repeat if event.isAutoRepeat(): return if key in self.keyboard_notes: note = self.keyboard_notes[key] if note not in self.held_keys: self.held_keys.add(note) self.arpeggiator.note_on(note) self.statusBar().showMessage(f"Note ON: {note}", 500) elif key == Qt.Key_Space: # Spacebar starts/stops arpeggiator if self.arpeggiator.is_playing: self.on_stop_clicked() else: self.on_play_clicked() super().keyPressEvent(event) def keyReleaseEvent(self, event): """Handle key release for note input""" key = event.key() # Avoid key repeat if event.isAutoRepeat(): return if key in self.keyboard_notes: note = self.keyboard_notes[key] if note in self.held_keys: self.held_keys.remove(note) self.arpeggiator.note_off(note) self.statusBar().showMessage(f"Note OFF: {note}", 500) super().keyReleaseEvent(event) def resizeEvent(self, event): """Handle window resize for dynamic scaling""" super().resizeEvent(event) if self.scaling_enabled and hasattr(self, 'arp_controls'): # Calculate new scale factor new_scale_factor = ScalingUtils.get_scale_factor(self) # Only update if scale factor changed significantly if abs(new_scale_factor - self.scale_factor) > 0.1: self.scale_factor = new_scale_factor self.update_scaling() def update_scaling(self): """Update all GUI elements with new scaling""" # Update controls with new scaling if hasattr(self.arp_controls, 'apply_scaling'): self.arp_controls.apply_scaling(self.scale_factor) if hasattr(self.volume_controls, 'apply_scaling'): self.volume_controls.apply_scaling(self.scale_factor) if hasattr(self.preset_controls, 'apply_scaling'): self.preset_controls.apply_scaling(self.scale_factor) def closeEvent(self, event): """Handle window close event""" # Clean up resources self.arpeggiator.stop() self.output_manager.close() self.simulator.cleanup() event.accept()