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.
705 lines
27 KiB
705 lines
27 KiB
"""
|
|
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, QShortcut)
|
|
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
|
|
# No keyboard note input - removed per user request
|
|
|
|
self.setup_ui()
|
|
self.setup_connections()
|
|
self.apply_dark_theme()
|
|
|
|
# Set up global keyboard shortcut for emergency stop
|
|
self.setup_emergency_stop()
|
|
|
|
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, self.arp_controls, self.volume_controls)
|
|
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 - SPACE for emergency stop")
|
|
|
|
# Create menu bar
|
|
self.create_menu_bar()
|
|
|
|
def setup_emergency_stop(self):
|
|
"""Set up global emergency stop that works even with text boxes focused"""
|
|
# Install event filter on the application to catch spacebar before any widget gets it
|
|
from PyQt5.QtWidgets import QApplication
|
|
app = QApplication.instance()
|
|
app.installEventFilter(self)
|
|
print("DEBUG: Emergency stop event filter installed")
|
|
|
|
def emergency_stop(self):
|
|
"""Emergency stop triggered by spacebar shortcut"""
|
|
print(f"DEBUG: Emergency stop activated, arpeggiator.is_playing = {self.arpeggiator.is_playing}")
|
|
if self.arpeggiator.is_playing:
|
|
print("DEBUG: Calling arpeggiator.stop()")
|
|
self.arpeggiator.stop()
|
|
self.statusBar().showMessage("Emergency stop activated", 1000)
|
|
print(f"DEBUG: After stop, is_playing = {self.arpeggiator.is_playing}")
|
|
else:
|
|
print("DEBUG: Arpeggiator not playing, ignoring emergency stop")
|
|
|
|
def eventFilter(self, source, event):
|
|
"""Global event filter to catch spacebar for emergency stop"""
|
|
from PyQt5.QtCore import QEvent
|
|
from PyQt5.QtGui import QKeyEvent
|
|
|
|
# Only handle key press events
|
|
if event.type() == QEvent.KeyPress:
|
|
key_event = event
|
|
if key_event.key() == Qt.Key_Space and not key_event.isAutoRepeat():
|
|
print(f"DEBUG: Spacebar intercepted from {source.__class__.__name__}")
|
|
self.emergency_stop()
|
|
return True # Consume the event to prevent text boxes from getting it
|
|
|
|
# Let other events pass through normally
|
|
return super().eventFilter(source, event)
|
|
|
|
# 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")
|
|
|
|
# Keyboard event handling removed - using QShortcut for emergency stop instead
|
|
|
|
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()
|