import sys import json import os import uuid import socket import rtmidi from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSlider, QComboBox, QLabel, QPushButton, QSizePolicy, QSpinBox, QLineEdit, QInputDialog, QFrame, QCheckBox, QTextEdit, QDialog, QListWidget, QListWidgetItem ) from PyQt6.QtCore import Qt, QObject, pyqtSignal, QTimer, QEvent from PyQt6.QtGui import QAction, QColor from pythonosc import dispatcher as osc_dispatcher from pythonosc import osc_server import threading from zeroconf import Zeroconf, ServiceInfo from togglewidget import ToggleWidget from faderwidget import FaderWidget from focusfaderwidget import FocusFaderWidget from focustogglewidget import FocusToggleWidget from focusfeedbackwidget import FocusFeedbackWidget from transportwidget import TransportWidget from presetwidget import PresetWidget from palette import ( PALETTE, PALETTE_NAMES, PALETTE_HEX, PALETTE_VIVID, lighten_hex, darken_hex, ) from styles import ( FADER_STYLE, TITLE_STYLE, ) from presets import ( load_presets_file, save_presets_file, ensure_preset_structure, on_preset_selected, save_preset, save_preset_as, delete_preset, ) from zonebutton import ZoneButton from colorswatchpopup import ColorSwatchPopup from midi_receiver import midi_receiver from midi_sender import ( midi_out, transport_midi_out, available_ports, send_cc, send_transport_cc, ) from midi_ports import ( get_available_input_ports, open_midi_input_port, close_midi_input_port, ) from osc_receiver import osc_receiver from osc_sender import ( send_osc_message, set_osc_target, ) from extension_sender import ( send_to_extension, set_extension_target, set_extension_enabled, is_extension_enabled, ) from ws_server import WSServer def set_window(w): global _window _window = w _osc_server = None _osc_thread = None ws_server = None BONJOUR_SERVICE_TYPE = "_vcremote._tcp.local." _bonjour_zc = None _bonjour_info = None #MARK: GLOBALS ready = False selected_strips = [] last_clicked_strip = None learning_strip = None faders = [] strip_width = 100 spacing = 10 padding = 40 loaded_presets = load_presets_file() app_window = None toggles = [] transports = [] toggle_strip_width = 100 midi_in_port = None midi_controller_in_port = None fader_show_hide_config = False transport_show_hide_config = False toggle_show_hide_config = False preset_switch_locked = True uuid_presets_matched = [] uuid_presets_received_values = [] uuid_presets_batch_timer = None _track_uuid_check_pending = False container_preset_browser = None container_rows_and_widgets = None view_uuid_error_overlay = None label_overlay_message = None placeholder_fader_empty = None placeholder_toggle_empty = None big_title_edit = None #MARK: CREATE APP app = QApplication(sys.argv) app.setStyle("Fusion") #MARK: MAIN WINDOW containerMain = QWidget() #MARK: PRESET ROW hstack_preset_row = QHBoxLayout() #MARK: PRESENT menuPresets = QComboBox() menuPresets.setMinimumWidth(160) menuPresets.currentTextChanged.connect(lambda name: on_preset_selected(menuPresets, app_window, ready, loaded_presets, on_load=preset_load)) buttonPresetPrev = QPushButton("◀") buttonPresetPrev.setFixedWidth(28) buttonPresetPrev.setToolTip("Previous preset") buttonPresetPrev.clicked.connect(lambda: menuPresets.setCurrentIndex(max(0, menuPresets.currentIndex() - 1))) buttonPresetNext = QPushButton("▶") buttonPresetNext.setFixedWidth(28) buttonPresetNext.setToolTip("Next preset") buttonPresetNext.clicked.connect(lambda: menuPresets.setCurrentIndex(min(menuPresets.count() - 1, menuPresets.currentIndex() + 1))) #button new blank preset buttonNew = QPushButton("New") buttonNew.setFixedWidth(45) buttonNew.clicked.connect(lambda: new_blank_preset()) #button preset save buttonSave = QPushButton("Save") buttonSave.setFixedWidth(50) buttonSave.clicked.connect(lambda: save_preset(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_build=preset_build_snapshot, on_save_as=lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot))) #button preset save as -> save_as_btn buttonSaveAs = QPushButton("Save As") buttonSaveAs.setFixedWidth(60) buttonSaveAs.clicked.connect(lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot)) #button preset delete -> del_btn buttonRemove = QPushButton("Remove") buttonRemove.setFixedWidth(65) buttonRemove.clicked.connect(lambda: delete_preset(app_window, menuPresets, loaded_presets, on_refresh=on_preset_deleted)) buttonExport = QPushButton("Export") buttonExport.setFixedWidth(55) buttonExport.clicked.connect(lambda: export_preset()) buttonImport = QPushButton("Import") buttonImport.setFixedWidth(55) buttonImport.clicked.connect(lambda: import_preset()) buttonDuplicate = QPushButton("Duplicate") buttonDuplicate.setFixedWidth(75) buttonDuplicate.clicked.connect(lambda: duplicate_preset()) buttonRename = QPushButton("Rename") buttonRename.setFixedWidth(65) buttonRename.clicked.connect(lambda: rename_preset()) checkbox_lock_preset = QCheckBox("Lock Preset") checkbox_lock_preset.setStyleSheet("color: #aaa; font-size: 11px;") checkbox_lock_preset.setChecked(True) def on_lock_preset_toggled(checked): global preset_switch_locked preset_switch_locked = checked checkbox_lock_preset.toggled.connect(on_lock_preset_toggled) #save status label (should be used for other stuff as well) -> save_status_label labelStatus = QLabel("") labelStatus.setStyleSheet("color: #00e676; font-style: italic;") # Widget: label_preset_uuid label_preset_uuid = QLabel("") label_preset_uuid.setFixedWidth(240) label_preset_uuid.setStyleSheet("color: #888; font-size: 11px; font-family: monospace;") # Widget: button_preset_uuid_gen button_preset_uuid_gen = QPushButton("Gen") button_preset_uuid_gen.setFixedWidth(36) button_preset_uuid_gen.clicked.connect(lambda: generate_preset_uuid()) # Widget: button_preset_uuid_copy button_preset_uuid_copy = QPushButton("Copy") button_preset_uuid_copy.setFixedWidth(40) button_preset_uuid_copy.clicked.connect(lambda: copy_preset_uuid()) #container io row hstack_io_row = QHBoxLayout() hstack_io_row.setSpacing(16) # REFACTOR MAP: Old names → New names # self.in_blink → activity_indicator_midi_in # self.in_status → label_connection_midi_in # self.in_combo → picker_midi_in # self.available_in_ports → available_ports_midi_in # vstack_midi_in vstack_midi_in = QVBoxLayout() vstack_midi_in.setSpacing(2) # hstack_midi_in_activity_and_label hstack_midi_in_activity_and_label = QHBoxLayout() # activity_indicator_midi_in activity_indicator_midi_in = QLabel("●") # activity_indicator_midi_in.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # label_connection_midi_in label_connection_midi_in = QLabel("Not Connected") # label_connection_midi_in.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # label_midi_in label_midi_in = QLabel("MIDI In") # label_midi_in.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") # picker_midi_in picker_midi_in = QComboBox() picker_midi_in.setFixedWidth(200) # picker_midi_in.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") # Second, independent MIDI input dedicated to controllers only (e.g. JL Cooper # fader bank) — same CC learn/routing pipeline as picker_midi_in, but never # passes Note On/Off through to MIDI Out. # vstack_midi_controller_in vstack_midi_controller_in = QVBoxLayout() vstack_midi_controller_in.setSpacing(2) # hstack_midi_controller_in_activity_and_label hstack_midi_controller_in_activity_and_label = QHBoxLayout() # activity_indicator_midi_controller_in activity_indicator_midi_controller_in = QLabel("●") # label_connection_midi_controller_in label_connection_midi_controller_in = QLabel("Not Connected") # label_midi_controller_in label_midi_controller_in = QLabel("MIDI Controller In") # picker_midi_controller_in picker_midi_controller_in = QComboBox() picker_midi_controller_in.setFixedWidth(200) # REFACTOR MAP: Old names → New names # self.out_blink → activity_indicator_midi_out # self.out_status → label_connection_midi_out # self.port_combo → picker_midi_out # vstack_midi_out vstack_midi_out = QVBoxLayout() vstack_midi_out.setSpacing(2) # hstack_midi_out_activity_and_label hstack_midi_out_activity_and_label = QHBoxLayout() # activity_indicator_midi_out activity_indicator_midi_out = QLabel("●") # activity_indicator_midi_out.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # label_connection_midi_out label_connection_midi_out = QLabel("Not Connected") # label_connection_midi_out.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # label_midi_out label_midi_out = QLabel("MIDI Out") # label_midi_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") # picker_midi_out picker_midi_out = QComboBox() picker_midi_out.setFixedWidth(200) # picker_midi_out.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") # REFACTOR MAP: Old names → New names # self.t_out_blink → activity_indicator_transport_out # self.t_out_status → label_connection_transport_out # self.t_out_combo → picker_transport_out # vstack_transport_out vstack_transport_out = QVBoxLayout() vstack_transport_out.setSpacing(2) # hstack_transport_out_activity_and_label hstack_transport_out_activity_and_label = QHBoxLayout() # activity_indicator_transport_out activity_indicator_transport_out = QLabel("●") # activity_indicator_transport_out.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # label_connection_transport_out label_connection_transport_out = QLabel("Not Connected") # label_connection_transport_out.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # label_transport_out label_transport_out = QLabel("MIDI Transport Out") # label_transport_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") # picker_transport_out picker_transport_out = QComboBox() picker_transport_out.setFixedWidth(200) # picker_transport_out.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") # REFACTOR MAP: Old names → New names # self.osc_blink → activity_indicator_osc # self.osc_status → label_connection_osc # self.osc_ip_edit → textbox_osc_daw_ip # self.osc_port_spin → spinner_osc_listen_port # self.osc_send_port_spin → spinner_osc_send_port # self.osc_toggle_btn → button_osc_toggle # self.osc_log_btn → button_osc_messages_log # Layout: vstack_osc vstack_osc = QVBoxLayout() vstack_osc.setSpacing(2) # Layout: vstack_remote — Remote's own self-contained column (indicator, # address, buttons), placed beside vstack_osc rather than mixed into it, so # the Remote status rows align above the Remote buttons, not OSC's. vstack_remote = QVBoxLayout() vstack_remote.setSpacing(2) # Layout: hstack_remote_buttons hstack_remote_buttons = QHBoxLayout() # Layout: hstack_osc_and_remote — holds OSC, Remote, and Extension columns # side by side (name kept from when it only held two, to avoid churn) hstack_osc_and_remote = QHBoxLayout() hstack_osc_and_remote.setSpacing(16) # Layout: vstack_extension — Extension's own column, mirrors vstack_osc's # row structure exactly (activity, address, port, buttons — no stretch # needed since it has the same number of rows as OSC's column). vstack_extension = QVBoxLayout() vstack_extension.setSpacing(2) # Layout: hstack_extension_activity_and_label hstack_extension_activity_and_label = QHBoxLayout() # Layout: hstack_extension_ip hstack_extension_ip = QHBoxLayout() # Layout: hstack_extension_port hstack_extension_port = QHBoxLayout() # Layout: hstack_extension_buttons hstack_extension_buttons = QHBoxLayout() # Widget: activity_indicator_extension activity_indicator_extension = QLabel("●") activity_indicator_extension.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # Widget: label_connection_extension label_connection_extension = QLabel("Not Connected") label_connection_extension.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # Widget: textbox_extension_ip textbox_extension_ip = QLineEdit("127.0.0.1") textbox_extension_ip.setFixedWidth(110) textbox_extension_ip.setPlaceholderText("127.0.0.1") # Widget: spinner_extension_port spinner_extension_port = QSpinBox() spinner_extension_port.setMinimum(1024) spinner_extension_port.setMaximum(65535) spinner_extension_port.setValue(9124) spinner_extension_port.setFixedWidth(65) # Widget: button_extension_toggle button_extension_toggle = QPushButton("Connect Extension") button_extension_toggle.setFixedWidth(130) # Widget: button_extension_log button_extension_log = QPushButton("Log") button_extension_log.setFixedWidth(65) button_extension_log.setCheckable(True) # Layout: hstack_osc_activity_and_label hstack_osc_activity_and_label = QHBoxLayout() # Layout: hstack_osc_ip hstack_osc_ip = QHBoxLayout() # Layout: hstack_osc_ports hstack_osc_ports = QHBoxLayout() # Layout: hstack_osc_buttons hstack_osc_buttons = QHBoxLayout() # Widget: activity_indicator_osc activity_indicator_osc = QLabel("●") activity_indicator_osc.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # Widget: label_connection_osc label_connection_osc = QLabel("Not Listening") label_connection_osc.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # Widget: textbox_osc_daw_ip textbox_osc_daw_ip = QLineEdit("127.0.0.1") textbox_osc_daw_ip.setFixedWidth(110) textbox_osc_daw_ip.setPlaceholderText("127.0.0.1") # Widget: spinner_osc_listen_port spinner_osc_listen_port = QSpinBox() spinner_osc_listen_port.setMinimum(1024) spinner_osc_listen_port.setMaximum(65535) spinner_osc_listen_port.setValue(9000) spinner_osc_listen_port.setFixedWidth(65) # Widget: spinner_osc_send_port spinner_osc_send_port = QSpinBox() spinner_osc_send_port.setMinimum(1024) spinner_osc_send_port.setMaximum(65535) spinner_osc_send_port.setValue(8000) spinner_osc_send_port.setFixedWidth(65) # Widget: button_osc_toggle button_osc_toggle = QPushButton("Start OSC") button_osc_toggle.setFixedWidth(90) # Widget: button_tablet_server button_tablet_server = QPushButton("Enable Remote") button_tablet_server.setFixedWidth(110) # Widget: button_tablet_log button_tablet_log = QPushButton("Remote Log") button_tablet_log.setFixedWidth(85) button_tablet_log.setCheckable(True) # Widget: button_ws_log button_ws_log = QPushButton("WS Log") button_ws_log.setFixedWidth(65) button_ws_log.setCheckable(True) # Widget: label_tablet_status label_tablet_status = QLabel("") label_tablet_status.setStyleSheet("color: #8e24aa; font-size: 11px; font-family: monospace;") # Layout: hstack_remote_activity_and_label hstack_remote_activity_and_label = QHBoxLayout() # Layout: hstack_remote_ip hstack_remote_ip = QHBoxLayout() # Widget: activity_indicator_remote activity_indicator_remote = QLabel("●") activity_indicator_remote.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # Widget: label_connection_remote label_connection_remote = QLabel("Not Listening") label_connection_remote.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # Widget: button_osc_messages_log button_osc_messages_log = QPushButton("OSC/MIDI Log") button_osc_messages_log.setFixedWidth(100) button_osc_messages_log.setCheckable(True) # Widget: button_layout_panel button_layout_panel = QPushButton("Layout") button_layout_panel.setFixedWidth(65) button_layout_panel.setCheckable(True) # Widget: button_focus_panel button_focus_panel = QPushButton("Focus") button_focus_panel.setFixedWidth(65) button_focus_panel.setCheckable(True) # Layout: hstack_osc_status hstack_osc_status = QHBoxLayout() hstack_osc_status.setSpacing(8) # Widget: label_osc_status label_osc_status = QLabel("") label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;") # MARK: OSC STATUS ROW MODULE # REFACTOR MAP: Old names → New names # self.osc_panel → container_osc_feedback # self.osc_play_lbl → label_osc_play_state # self.osc_record_lbl → label_osc_record_state # self.osc_tempo_lbl → label_osc_tempo # self.osc_pos_lbl → label_osc_position # self.osc_track_lbl → label_osc_track # self.osc_preset_lbl → label_osc_preset # self.osc_log_panel → container_osc_in_log # self.osc_log → textedit_osc_in_log # self.osc_out_log_panel → container_osc_out_log # self.osc_out_log → textedit_osc_out_log # Container: container_osc_feedback container_osc_feedback = QWidget() container_osc_feedback.setStyleSheet("background-color: #252526; border-radius: 4px;") # Layout: vstack_osc_feedback vstack_osc_feedback = QVBoxLayout(container_osc_feedback) vstack_osc_feedback.setContentsMargins(8, 6, 8, 6) vstack_osc_feedback.setSpacing(4) # Layout: hstack_osc_transport_state hstack_osc_transport_state = QHBoxLayout() hstack_osc_transport_state.setSpacing(12) # Widget: label_osc_play_state label_osc_play_state = QLabel("■ STOPPED") label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") # Widget: label_osc_record_state label_osc_record_state = QLabel("● REC") label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") # Widget: label_osc_position label_osc_position = QLabel("Time: —") label_osc_position.setStyleSheet("color: #888; font-size: 11px;") # Widget: label_osc_bar label_osc_bar = QLabel("Bar: —") label_osc_bar.setStyleSheet("color: #888; font-size: 11px;") # Widget: divider_osc_feedback divider_osc_feedback = QWidget() divider_osc_feedback.setFixedHeight(1) divider_osc_feedback.setStyleSheet("background-color: #3a3a3a;") # Layout: hstack_osc_info hstack_osc_info = QHBoxLayout() hstack_osc_info.setSpacing(8) # Widget: label_track_header label_track_header = QLabel("TRACK") label_track_header.setStyleSheet("color: #555; font-size: 9px; font-weight: bold;") # Widget: label_osc_track label_osc_track = QLabel("—") label_osc_track.setStyleSheet("color: #d4d4d4; font-size: 12px; font-weight: bold;") # Widget: label_preset_header label_preset_header = QLabel("PRESET") label_preset_header.setStyleSheet("color: #555; font-size: 9px; font-weight: bold;") # Widget: label_osc_preset label_osc_preset = QLabel("—") label_osc_preset.setStyleSheet("color: #00e676; font-size: 12px; font-weight: bold;") # Widget: spacer_osc_info spacer_osc_info = QWidget() spacer_osc_info.setFixedWidth(16) # Container: container_osc_in_log container_osc_in_log = QWidget(None, Qt.WindowType.Window) container_osc_in_log.setWindowTitle("OSC In — Incoming") container_osc_in_log.setStyleSheet("background-color: #1a1a1a;") container_osc_in_log.setFixedWidth(360) # Layout: vstack_osc_in_log vstack_osc_in_log = QVBoxLayout(container_osc_in_log) vstack_osc_in_log.setContentsMargins(6, 6, 6, 6) vstack_osc_in_log.setSpacing(4) # Layout: hstack_osc_in_log_header hstack_osc_in_log_header = QHBoxLayout() # Widget: label_osc_in_log_title label_osc_in_log_title = QLabel("OSC In — Incoming from REAPER") label_osc_in_log_title.setStyleSheet("color: #00e676; font-weight: bold; font-size: 11px;") # Widget: button_osc_in_clear button_osc_in_clear = QPushButton("Clear") button_osc_in_clear.setFixedSize(45, 18) # Widget: textedit_osc_in_log textedit_osc_in_log = QTextEdit() textedit_osc_in_log.setReadOnly(True) textedit_osc_in_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #00e676; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_osc_out_log container_osc_out_log = QWidget(None, Qt.WindowType.Window) container_osc_out_log.setWindowTitle("OSC Out — Outgoing") container_osc_out_log.setStyleSheet("background-color: #1a1a1a;") container_osc_out_log.setFixedWidth(360) # Layout: vstack_osc_out_log vstack_osc_out_log = QVBoxLayout(container_osc_out_log) vstack_osc_out_log.setContentsMargins(6, 6, 6, 6) vstack_osc_out_log.setSpacing(4) # Layout: hstack_osc_out_log_header hstack_osc_out_log_header = QHBoxLayout() # Widget: label_osc_out_log_title label_osc_out_log_title = QLabel("OSC Out — Sent to REAPER") label_osc_out_log_title.setStyleSheet("color: #1e88e5; font-weight: bold; font-size: 11px;") # Widget: button_osc_out_clear button_osc_out_clear = QPushButton("Clear") button_osc_out_clear.setFixedSize(45, 18) # Widget: textedit_osc_out_log textedit_osc_out_log = QTextEdit() textedit_osc_out_log.setReadOnly(True) textedit_osc_out_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #1e88e5; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_midi_in_log container_midi_in_log = QWidget(None, Qt.WindowType.Window) container_midi_in_log.setWindowTitle("MIDI In — Incoming") container_midi_in_log.setStyleSheet("background-color: #1a1a1a;") container_midi_in_log.setFixedWidth(360) vstack_midi_in_log = QVBoxLayout(container_midi_in_log) vstack_midi_in_log.setContentsMargins(6, 6, 6, 6) vstack_midi_in_log.setSpacing(4) hstack_midi_in_log_header = QHBoxLayout() label_midi_in_log_title = QLabel("MIDI In — Incoming") label_midi_in_log_title.setStyleSheet("color: #ff8c00; font-weight: bold; font-size: 11px;") button_midi_in_clear = QPushButton("Clear") button_midi_in_clear.setFixedSize(45, 18) textedit_midi_in_log = QTextEdit() textedit_midi_in_log.setReadOnly(True) textedit_midi_in_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #ff8c00; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_midi_out_log container_midi_out_log = QWidget(None, Qt.WindowType.Window) container_midi_out_log.setWindowTitle("MIDI Out — Outgoing") container_midi_out_log.setStyleSheet("background-color: #1a1a1a;") container_midi_out_log.setFixedWidth(360) vstack_midi_out_log = QVBoxLayout(container_midi_out_log) vstack_midi_out_log.setContentsMargins(6, 6, 6, 6) vstack_midi_out_log.setSpacing(4) hstack_midi_out_log_header = QHBoxLayout() label_midi_out_log_title = QLabel("MIDI Out — Outgoing") label_midi_out_log_title.setStyleSheet("color: #00bcd4; font-weight: bold; font-size: 11px;") button_midi_out_clear = QPushButton("Clear") button_midi_out_clear.setFixedSize(45, 18) textedit_midi_out_log = QTextEdit() textedit_midi_out_log.setReadOnly(True) textedit_midi_out_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #00bcd4; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_ws_in_log container_ws_in_log = QWidget(None, Qt.WindowType.Window) container_ws_in_log.setWindowTitle("WS In — Incoming from Tablet") container_ws_in_log.setStyleSheet("background-color: #1a1a1a;") container_ws_in_log.setFixedWidth(420) vstack_ws_in_log = QVBoxLayout(container_ws_in_log) vstack_ws_in_log.setContentsMargins(6, 6, 6, 6) vstack_ws_in_log.setSpacing(4) hstack_ws_in_log_header = QHBoxLayout() label_ws_in_log_title = QLabel("WS In — Incoming from Tablet") label_ws_in_log_title.setStyleSheet("color: #00acc1; font-weight: bold; font-size: 11px;") button_ws_in_clear = QPushButton("Clear") button_ws_in_clear.setFixedSize(45, 18) textedit_ws_in_log = QTextEdit() textedit_ws_in_log.setReadOnly(True) textedit_ws_in_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #00e5ff; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_ws_out_log container_ws_out_log = QWidget(None, Qt.WindowType.Window) container_ws_out_log.setWindowTitle("WS Out — Outgoing to Tablet") container_ws_out_log.setStyleSheet("background-color: #1a1a1a;") container_ws_out_log.setFixedWidth(420) vstack_ws_out_log = QVBoxLayout(container_ws_out_log) vstack_ws_out_log.setContentsMargins(6, 6, 6, 6) vstack_ws_out_log.setSpacing(4) hstack_ws_out_log_header = QHBoxLayout() label_ws_out_log_title = QLabel("WS Out — Outgoing to Tablet") label_ws_out_log_title.setStyleSheet("color: #ff6f00; font-weight: bold; font-size: 11px;") button_ws_out_clear = QPushButton("Clear") button_ws_out_clear.setFixedSize(45, 18) textedit_ws_out_log = QTextEdit() textedit_ws_out_log.setReadOnly(True) textedit_ws_out_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #ffb300; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_tablet_log container_tablet_log = QWidget(None, Qt.WindowType.Window) container_tablet_log.setWindowTitle("Tablet Server") container_tablet_log.setStyleSheet("background-color: #1a1a1a;") container_tablet_log.setFixedWidth(360) vstack_tablet_log = QVBoxLayout(container_tablet_log) vstack_tablet_log.setContentsMargins(6, 6, 6, 6) vstack_tablet_log.setSpacing(4) hstack_tablet_log_header = QHBoxLayout() label_tablet_log_title = QLabel("Tablet Server") label_tablet_log_title.setStyleSheet("color: #8e24aa; font-weight: bold; font-size: 11px;") button_tablet_log_clear = QPushButton("Clear") button_tablet_log_clear.setFixedSize(45, 18) textedit_tablet_log = QTextEdit() textedit_tablet_log.setReadOnly(True) textedit_tablet_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #8e24aa; font-family: monospace; font-size: 10px; border: none; } """) # Container: container_extension_log container_extension_log = QWidget(None, Qt.WindowType.Window) container_extension_log.setWindowTitle("Extension") container_extension_log.setStyleSheet("background-color: #1a1a1a;") container_extension_log.setFixedWidth(360) vstack_extension_log = QVBoxLayout(container_extension_log) vstack_extension_log.setContentsMargins(6, 6, 6, 6) vstack_extension_log.setSpacing(4) hstack_extension_log_header = QHBoxLayout() label_extension_log_title = QLabel("Extension") label_extension_log_title.setStyleSheet("color: #00bcd4; font-weight: bold; font-size: 11px;") button_extension_log_clear = QPushButton("Clear") button_extension_log_clear.setFixedSize(45, 18) textedit_extension_log = QTextEdit() textedit_extension_log.setReadOnly(True) textedit_extension_log.setStyleSheet(""" QTextEdit { background-color: #111111; color: #00bcd4; font-family: monospace; font-size: 10px; border: none; } """) # MARK: LAYOUT PANEL container_layout_panel = QWidget(None, Qt.WindowType.Window) container_layout_panel.setWindowTitle("Layout") container_layout_panel.setStyleSheet("background-color: #1a1a1a;") container_layout_panel.setFixedWidth(220) vstack_layout_panel = QVBoxLayout(container_layout_panel) vstack_layout_panel.setContentsMargins(10, 10, 10, 10) vstack_layout_panel.setSpacing(6) label_layout_panel_title = QLabel("Sections") label_layout_panel_title.setStyleSheet("color: #ffffff; font-weight: bold; font-size: 12px;") vstack_layout_panel.addWidget(label_layout_panel_title) _section_sep = QFrame() _section_sep.setFrameShape(QFrame.Shape.HLine) _section_sep.setStyleSheet("color: #3a3a3a;") vstack_layout_panel.addWidget(_section_sep) _section_btn_style = ( "QPushButton { background: transparent; border: none; color: #ffffff;" " font-size: 12px; font-weight: bold; text-align: left; padding: 4px 6px; text-decoration: none; }" "QPushButton:!checked { color: #444444; font-weight: normal; text-decoration: line-through; }" ) def _make_section_row(label_text): btn = QPushButton(label_text) btn.setCheckable(True) btn.setChecked(True) btn.setFlat(True) btn.setStyleSheet(_section_btn_style) return btn chk_section_preset_browser = _make_section_row("Instrument Browser") chk_section_big_title = _make_section_row("Big Title") chk_section_fader_row = _make_section_row("Fader Row") chk_section_toggle_row = _make_section_row("Toggle Row") chk_section_transport_row = _make_section_row("Transport Row") for _btn in (chk_section_preset_browser, chk_section_big_title, chk_section_fader_row, chk_section_toggle_row, chk_section_transport_row): vstack_layout_panel.addWidget(_btn) del _btn vstack_layout_panel.addStretch() # MARK: FOCUS PANEL container_focus_panel = QWidget(None, Qt.WindowType.Window) container_focus_panel.setWindowTitle("Focus") container_focus_panel.setStyleSheet("background-color: #1a1a1a;") container_focus_panel.setFixedWidth(220) vstack_focus_panel = QVBoxLayout(container_focus_panel) vstack_focus_panel.setContentsMargins(10, 10, 10, 10) vstack_focus_panel.setSpacing(6) label_focus_panel_title = QLabel("Channel Focus") label_focus_panel_title.setStyleSheet("color: #ffffff; font-weight: bold; font-size: 12px;") vstack_focus_panel.addWidget(label_focus_panel_title) _focus_section_sep = QFrame() _focus_section_sep.setFrameShape(QFrame.Shape.HLine) _focus_section_sep.setStyleSheet("color: #3a3a3a;") vstack_focus_panel.addWidget(_focus_section_sep) # Row: focus_faders (multiple OSC-only faders — track volume, sends, master, etc.) hstack_focus_faders = QHBoxLayout() hstack_focus_faders.setSpacing(6) vstack_focus_panel.addLayout(hstack_focus_faders) focus_faders = [] def _wire_focus_fader(fader): fader.learn_btn.clicked.connect(lambda: start_osc_learn(fader)) fader.name_learn_btn.clicked.connect(lambda: start_osc_learn(fader, target="name")) fader.hw_learn_btn.clicked.connect(lambda: check_and_start_focus_hw_learn(fader)) fader.title_edit.editingFinished.connect(lambda: _broadcast_preset_update()) fader.plus_btn.clicked.connect(lambda: focus_fader_add(insert_after=fader)) fader.minus_btn.clicked.connect(lambda: focus_fader_remove(fader)) _first_focus_fader = FocusFaderWidget("Focus") focus_faders.append(_first_focus_fader) hstack_focus_faders.addWidget(_first_focus_fader, alignment=Qt.AlignmentFlag.AlignHCenter) _wire_focus_fader(_first_focus_fader) # Row: focus_toggles (multiple OSC-only toggles, shown on any iPad in Channel Focus mode) hstack_focus_toggles = QHBoxLayout() hstack_focus_toggles.setSpacing(6) vstack_focus_panel.addLayout(hstack_focus_toggles) focus_toggles = [] def _wire_focus_toggle(toggle): toggle.learn_btn.clicked.connect(lambda: start_osc_learn(toggle)) toggle.hw_learn_btn.clicked.connect(lambda: check_and_start_focus_hw_learn(toggle)) toggle.title_edit.editingFinished.connect(lambda: _broadcast_preset_update()) toggle.plus_btn.clicked.connect(lambda: focus_toggle_add(insert_after=toggle)) toggle.minus_btn.clicked.connect(lambda: focus_toggle_remove(toggle)) _first_focus_toggle = FocusToggleWidget("Toggle 1") focus_toggles.append(_first_focus_toggle) hstack_focus_toggles.addWidget(_first_focus_toggle, alignment=Qt.AlignmentFlag.AlignHCenter) _wire_focus_toggle(_first_focus_toggle) # Row: focus_feedbacks (multiple read-only OSC feedback displays, e.g. track name, bar, time) hstack_focus_feedbacks = QHBoxLayout() hstack_focus_feedbacks.setSpacing(6) vstack_focus_panel.addLayout(hstack_focus_feedbacks) focus_feedbacks = [] def _wire_focus_feedback(widget): widget.learn_btn.clicked.connect(lambda: start_osc_learn(widget)) widget.title_edit.editingFinished.connect(lambda: _broadcast_preset_update()) widget.plus_btn.clicked.connect(lambda: focus_feedback_add(insert_after=widget)) widget.minus_btn.clicked.connect(lambda: focus_feedback_remove(widget)) _first_focus_feedback = FocusFeedbackWidget("Track Name") focus_feedbacks.append(_first_focus_feedback) hstack_focus_feedbacks.addWidget(_first_focus_feedback, alignment=Qt.AlignmentFlag.AlignHCenter) _wire_focus_feedback(_first_focus_feedback) vstack_focus_panel.addStretch() # MARK: PRESET BROWSER ROW MODULE label_preset_browser_title = QLabel("Instrument Browser") hstack_preset_browser = QHBoxLayout() hstack_preset_browser.setSpacing(8) preset_browser_back = PresetWidget(label="Back", default_cc=22) preset_browser_fwd = PresetWidget(label="Fwd", default_cc=23) preset_browsers = [preset_browser_back, preset_browser_fwd] # show/hide state (mirrors fader/toggle pattern) preset_browser_show_hide_config = False # MARK: TRANSPORT MODULE # REFACTOR MAP: Old names → New names # self.full_transport_row → hstack_full_transport # self.transport_zone_left_slot → hstack_transport_zone_left # self.transport_vsep_left → vsep_transport_left # self.transport_row → hstack_transport_center # self.transport_vsep_right → vsep_transport_right # self.transport_zone_right_slot → hstack_transport_zone_right # Layout: hstack_full_transport hstack_full_transport = QHBoxLayout() hstack_full_transport.setSpacing(spacing) # Layout: hstack_transport_zone_left hstack_transport_zone_left = QHBoxLayout() hstack_transport_zone_left.setSpacing(0) # Widget: vsep_transport_left vsep_transport_left = QFrame() vsep_transport_left.setFrameShape(QFrame.Shape.VLine) vsep_transport_left.setFixedWidth(1) vsep_transport_left.setStyleSheet("background-color: #3a3a3a; border: none;") vsep_transport_left.setVisible(False) # Layout: hstack_transport_center hstack_transport_center = QHBoxLayout() hstack_transport_center.setSpacing(spacing) # Widget: vsep_transport_right vsep_transport_right = QFrame() vsep_transport_right.setFrameShape(QFrame.Shape.VLine) vsep_transport_right.setFixedWidth(1) vsep_transport_right.setStyleSheet("background-color: #3a3a3a; border: none;") vsep_transport_right.setVisible(False) # Layout: hstack_transport_zone_right hstack_transport_zone_right = QHBoxLayout() hstack_transport_zone_right.setSpacing(0) # Widgets: transport strip (initial) for _label, _cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]: _t = TransportWidget(_label, _cc) transports.append(_t) hstack_transport_center.addWidget(_t) _t.plus_btn.clicked.connect(lambda checked=False, t=_t: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t)) _t.minus_btn.clicked.connect(lambda checked=False, t=_t: transport_remove(t)) _t.learn_btn.clicked.connect(lambda checked=False, t=_t: check_and_start_midi_learn(t)) _t.on_select_callback = lambda strip: check_and_update_selected_strip(strip) _t.on_context_menu_callback = lambda strip, pos: strip_context_menu(strip, pos) _t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip) _t.remote_checkbox.clicked.connect(lambda: _on_remote_checkbox_changed()) _t.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed()) _t.title_edit.editingFinished.connect(lambda: _broadcast_preset_update()) del _t, _label, _cc # MARK: SELECTED CHANNEL ROW MODULE # REFACTOR MAP: Old names → New names # self.selected_label → label_selected_channel # Layout: hstack_selected_channel_and_save_status hstack_selected_channel_and_save_status = QHBoxLayout() # Widget: label_selected_channel label_selected_channel = QLabel("Software item selected: None") label_selected_channel.setStyleSheet("color: #00ff88; font-weight: bold; border: 1px solid #00ff88; padding: 2px 8px;") label_selected_channel.setAlignment(Qt.AlignmentFlag.AlignCenter) # MARK: FADERS ROW MODULE # REFACTOR MAP: Old names → New names # self.full_fader_row → hstack_full_fader # self.zone_left_slot → hstack_fader_zone_left # self.vsep_left → separator_vertical_fader_zone_left # self.fader_row → hstack_fader_center # self.vsep_right → separator_vertical_fader_zone_right # self.zone_right_slot → hstack_fader_zone_right # Layout: hstack_full_fader hstack_full_fader = QHBoxLayout() hstack_full_fader.setSpacing(spacing) # Layout: hstack_fader_zone_left hstack_fader_zone_left = QHBoxLayout() hstack_fader_zone_left.setSpacing(0) # Widget: separator_vertical_fader_zone_left separator_vertical_fader_zone_left = QFrame() separator_vertical_fader_zone_left.setFrameShape(QFrame.Shape.VLine) separator_vertical_fader_zone_left.setFixedWidth(1) separator_vertical_fader_zone_left.setStyleSheet("background-color: #3a3a3a; border: none;") separator_vertical_fader_zone_left.setVisible(False) # Layout: hstack_fader_center hstack_fader_center = QHBoxLayout() hstack_fader_center.setSpacing(spacing) # Widget: separator_vertical_fader_zone_right separator_vertical_fader_zone_right = QFrame() separator_vertical_fader_zone_right.setFrameShape(QFrame.Shape.VLine) separator_vertical_fader_zone_right.setFixedWidth(1) separator_vertical_fader_zone_right.setStyleSheet("background-color: #3a3a3a; border: none;") separator_vertical_fader_zone_right.setVisible(False) # Layout: hstack_fader_zone_right hstack_fader_zone_right = QHBoxLayout() hstack_fader_zone_right.setSpacing(0) # MARK: TOGGLE ROW MODULE # REFACTOR MAP: Old names → New names # self.full_toggle_row → hstack_full_toggle # self.toggle_zone_left_slot → hstack_toggle_zone_left # self.toggle_vsep_left → separator_vertical_toggle_zone_left # self.toggle_row → hstack_toggle_center # self.toggle_vsep_right → separator_vertical_toggle_zone_right # self.toggle_zone_right_slot → hstack_toggle_zone_right # Layout: hstack_full_toggle hstack_full_toggle = QHBoxLayout() hstack_full_toggle.setSpacing(spacing) # Layout: hstack_toggle_zone_left hstack_toggle_zone_left = QHBoxLayout() hstack_toggle_zone_left.setSpacing(0) # Widget: separator_vertical_toggle_zone_left separator_vertical_toggle_zone_left = QFrame() separator_vertical_toggle_zone_left.setFrameShape(QFrame.Shape.VLine) separator_vertical_toggle_zone_left.setFixedWidth(1) separator_vertical_toggle_zone_left.setStyleSheet("background-color: #3a3a3a; border: none;") separator_vertical_toggle_zone_left.setVisible(False) # Layout: hstack_toggle_center hstack_toggle_center = QHBoxLayout() hstack_toggle_center.setSpacing(10) # Widget: separator_vertical_toggle_zone_right separator_vertical_toggle_zone_right = QFrame() separator_vertical_toggle_zone_right.setFrameShape(QFrame.Shape.VLine) separator_vertical_toggle_zone_right.setFixedWidth(1) separator_vertical_toggle_zone_right.setStyleSheet("background-color: #3a3a3a; border: none;") separator_vertical_toggle_zone_right.setVisible(False) # Layout: hstack_toggle_zone_right hstack_toggle_zone_right = QHBoxLayout() hstack_toggle_zone_right.setSpacing(0) # Layout: hstack_row_fader_control_buttons hstack_row_fader_control_buttons = QHBoxLayout() hstack_row_fader_control_buttons.setSpacing(8) # Widget: button_fader_show_hide_config _show_hide_btn_style = "QPushButton { background: transparent; border: 1px solid #555; color: #aaa; border-radius: 4px; font-size: 12px; font-weight: normal; } QPushButton:hover { border-color: #aaa; color: #fff; }" button_fader_show_hide_config = QPushButton("Hide Parameters") button_fader_show_hide_config.setFixedWidth(120) button_fader_show_hide_config.setCheckable(True) button_fader_show_hide_config.setFlat(True) button_fader_show_hide_config.setStyleSheet(_show_hide_btn_style) button_fader_clear_all = QPushButton("Clear All") button_fader_clear_all.setFixedWidth(70) button_fader_clear_all.setFlat(True) button_fader_clear_all.setStyleSheet(_show_hide_btn_style) # MARK: TRANSPORT ROW CONTROLS MODULE # Layout: hstack_row_transport_control_buttons hstack_row_transport_control_buttons = QHBoxLayout() hstack_row_transport_control_buttons.setSpacing(8) # Widget: button_transport_show_hide_config button_transport_show_hide_config = QPushButton("Hide Parameters") button_transport_show_hide_config.setFixedWidth(120) button_transport_show_hide_config.setCheckable(True) button_transport_show_hide_config.setFlat(True) button_transport_show_hide_config.setStyleSheet(_show_hide_btn_style) # MARK: TOGGLE ROW CONTROLS MODULE # Layout: hstack_row_toggle_control_buttons hstack_row_toggle_control_buttons = QHBoxLayout() hstack_row_toggle_control_buttons.setSpacing(8) # Widget: button_preset_browser_show_hide_config button_preset_browser_show_hide_config = QPushButton("Hide Parameters") button_preset_browser_show_hide_config.setFixedWidth(120) button_preset_browser_show_hide_config.setCheckable(True) button_preset_browser_show_hide_config.setFlat(True) button_preset_browser_show_hide_config.setStyleSheet(_show_hide_btn_style) # Widget: button_toggle_show_hide_config button_toggle_show_hide_config = QPushButton("Hide Parameters") button_toggle_show_hide_config.setFixedWidth(120) button_toggle_show_hide_config.setCheckable(True) button_toggle_show_hide_config.setFlat(True) button_toggle_show_hide_config.setStyleSheet(_show_hide_btn_style) button_toggle_clear_all = QPushButton("Clear All") button_toggle_clear_all.setFixedWidth(70) button_toggle_clear_all.setFlat(True) button_toggle_clear_all.setStyleSheet(_show_hide_btn_style) #MARK: MAIN WINDOW START class MainWindow(QMainWindow): def __init__(self): super().__init__() global loaded_presets global containerPresetRow global menuPresets global buttonSave global buttonSaveAs global buttonRemove global buttonExport global buttonImport global labelStatus global hstack_io_row global available_ports_midi_in global picker_midi_in global label_midi_in global label_connection_midi_in global activity_indicator_midi_in global hstack_midi_in_activity_and_label global vstack_midi_in self.setWindowTitle("Controller") #set app window reference set_app_window(self) # ui main window self.setCentralWidget(containerMain) main_layout = QVBoxLayout(containerMain) #MARK: PRESETS ROW # ui preset row + drop down + buttont main_layout.addLayout(hstack_preset_row) hstack_preset_row.addWidget(menuPresets) #dropdown hstack_preset_row.addWidget(buttonPresetPrev) hstack_preset_row.addWidget(buttonPresetNext) hstack_preset_row.addWidget(buttonNew) hstack_preset_row.addWidget(buttonSave) #save button hstack_preset_row.addWidget(buttonSaveAs) #save as button hstack_preset_row.addWidget(buttonRemove) #remove button hstack_preset_row.addWidget(buttonDuplicate) hstack_preset_row.addWidget(buttonRename) hstack_preset_row.addWidget(buttonExport) hstack_preset_row.addWidget(buttonImport) hstack_preset_row.addWidget(checkbox_lock_preset) hstack_preset_row.addWidget(label_preset_uuid) hstack_preset_row.addWidget(button_preset_uuid_gen) hstack_preset_row.addWidget(button_preset_uuid_copy) hstack_preset_row.addWidget(labelStatus) hstack_preset_row.addStretch() hstack_preset_row.addWidget(label_selected_channel) #section seperator main_layout.addWidget(make_separator()) #MARK: MIDI CONTROLLER IN COLUMN # Globals for mutation safety global vstack_midi_controller_in global hstack_midi_controller_in_activity_and_label global activity_indicator_midi_controller_in global label_connection_midi_controller_in global label_midi_controller_in global picker_midi_controller_in global available_ports_midi_controller_in # build the hstack with activity indicator and connection state label hstack_midi_controller_in_activity_and_label.addWidget(activity_indicator_midi_controller_in) hstack_midi_controller_in_activity_and_label.addWidget(label_connection_midi_controller_in) hstack_midi_controller_in_activity_and_label.addStretch() # add all 3 to the vstack vstack_midi_controller_in.addLayout(hstack_midi_controller_in_activity_and_label) #activity indicator and connection label vstack_midi_controller_in.addWidget(label_midi_controller_in) # midi controller input label vstack_midi_controller_in.addWidget(picker_midi_controller_in) # dropdown menu # picker_midi_controller_in population & signal available_ports_midi_controller_in = get_available_input_ports() picker_midi_controller_in.addItem("Not Connected") for display_name, idx, available in available_ports_midi_controller_in: picker_midi_controller_in.addItem(display_name) if not available: item_index = picker_midi_controller_in.count() - 1 picker_midi_controller_in.model().item(item_index).setEnabled(False) picker_midi_controller_in.model().item(item_index).setForeground(QColor('#888888')) picker_midi_controller_in.currentTextChanged.connect(check_controller_in_port_avail_and_assign) # add the combined stack to the row hstack_io_row.addLayout(vstack_midi_controller_in) #MARK: MIDI IN COLUMN 1 #build the hstack with activity indicator and connection state label hstack_midi_in_activity_and_label.addWidget(activity_indicator_midi_in) hstack_midi_in_activity_and_label.addWidget(label_connection_midi_in) hstack_midi_in_activity_and_label.addStretch() #add all 3 to the vstack vstack_midi_in.addLayout(hstack_midi_in_activity_and_label) #activity indicator and connection label vstack_midi_in.addWidget(label_midi_in) # midi input label vstack_midi_in.addWidget(picker_midi_in) # dropdown menu # add the combined stack to the row hstack_io_row.addLayout(vstack_midi_in) # picker_midi_in population & signal available_ports_midi_in = get_available_input_ports() picker_midi_in.addItem("Not Connected") for display_name, idx, available in available_ports_midi_in: picker_midi_in.addItem(display_name) if not available: item_index = picker_midi_in.count() - 1 picker_midi_in.model().item(item_index).setEnabled(False) picker_midi_in.model().item(item_index).setForeground(QColor('#888888')) picker_midi_in.currentTextChanged.connect(check_midi_port_avail_and_assign) #MARK: MIDI OUT COLUMN 2 # Globals for mutation safety global vstack_midi_out global hstack_midi_out_activity_and_label global activity_indicator_midi_out global label_connection_midi_out global label_midi_out global picker_midi_out # build the hstack with activity indicator and connection state label hstack_midi_out_activity_and_label.addWidget(activity_indicator_midi_out) hstack_midi_out_activity_and_label.addWidget(label_connection_midi_out) hstack_midi_out_activity_and_label.addStretch() # add all 3 to the vstack vstack_midi_out.addLayout(hstack_midi_out_activity_and_label) # activity indicator and connection label vstack_midi_out.addWidget(label_midi_out) # midi output label vstack_midi_out.addWidget(picker_midi_out) # port dropdown # populate the dropdown picker_midi_out.addItem("Not Connected") picker_midi_out.addItems(available_ports) picker_midi_out.currentTextChanged.connect(set_port_midi_output) # add the combined stack to the row hstack_io_row.addLayout(vstack_midi_out) #MARK: TRANSPORT OUT COLUMN 3 # Globals for mutation safety global vstack_transport_out global hstack_transport_out_activity_and_label global activity_indicator_transport_out global label_connection_transport_out global label_transport_out global picker_transport_out # build the hstack with activity indicator and connection state label hstack_transport_out_activity_and_label.addWidget(activity_indicator_transport_out) hstack_transport_out_activity_and_label.addWidget(label_connection_transport_out) hstack_transport_out_activity_and_label.addStretch() # add all 3 to the vstack vstack_transport_out.addLayout(hstack_transport_out_activity_and_label) # activity indicator and connection label vstack_transport_out.addWidget(label_transport_out) # transport output label vstack_transport_out.addWidget(picker_transport_out) # port dropdown # populate the dropdown picker_transport_out.addItem("Not Connected") picker_transport_out.addItems(available_ports) picker_transport_out.currentTextChanged.connect(set_port_transport_output) # add the combined stack to the row hstack_io_row.addLayout(vstack_transport_out) #MARK: OSC COLUMN 4 # Globals for mutation safety global vstack_osc global hstack_osc_activity_and_label global activity_indicator_osc global label_connection_osc global hstack_osc_ip global textbox_osc_daw_ip global hstack_osc_ports global spinner_osc_listen_port global spinner_osc_send_port global hstack_osc_buttons global button_osc_toggle global button_osc_messages_log # build hstack with activity indicator and connection label hstack_osc_activity_and_label.addWidget(activity_indicator_osc) hstack_osc_activity_and_label.addWidget(label_connection_osc) hstack_osc_activity_and_label.addStretch() # build hstack for DAW IP configuration hstack_osc_ip.addWidget(QLabel("DAW IP")) hstack_osc_ip.addWidget(textbox_osc_daw_ip) hstack_osc_ip.addStretch() # build hstack for listen/send ports hstack_osc_ports.addWidget(QLabel("Listen")) hstack_osc_ports.addWidget(spinner_osc_listen_port) hstack_osc_ports.addSpacing(6) hstack_osc_ports.addWidget(QLabel("Send")) hstack_osc_ports.addWidget(spinner_osc_send_port) hstack_osc_ports.addStretch() # build hstack for control buttons (OSC's own only — Remote's live # in hstack_remote_buttons instead, in their own column) hstack_osc_buttons.addWidget(button_osc_toggle) hstack_osc_buttons.addWidget(button_osc_messages_log) hstack_osc_buttons.addWidget(button_layout_panel) hstack_osc_buttons.addWidget(button_focus_panel) hstack_osc_buttons.addStretch() # build hstack with activity indicator and connection label (Remote) hstack_remote_activity_and_label.addWidget(activity_indicator_remote) hstack_remote_activity_and_label.addWidget(label_connection_remote) hstack_remote_activity_and_label.addStretch() # build hstack for the remote's ws:// address hstack_remote_ip.addWidget(label_tablet_status) hstack_remote_ip.addStretch() # build hstack for the remote's own buttons hstack_remote_buttons.addWidget(button_tablet_server) hstack_remote_buttons.addWidget(button_tablet_log) hstack_remote_buttons.addWidget(button_ws_log) hstack_remote_buttons.addStretch() # add OSC's rows to its own vstack vstack_osc.addLayout(hstack_osc_activity_and_label) # activity indicator and connection label vstack_osc.addLayout(hstack_osc_ip) # DAW IP input vstack_osc.addLayout(hstack_osc_ports) # listen and send ports vstack_osc.addLayout(hstack_osc_buttons) # control buttons # add Remote's rows to its own vstack, so its indicator/address sit # directly above its own buttons, not OSC's vstack_remote.addLayout(hstack_remote_activity_and_label) # remote activity indicator and connection label vstack_remote.addLayout(hstack_remote_ip) # remote ws:// address vstack_remote.addStretch() vstack_remote.addLayout(hstack_remote_buttons) # remote control buttons # build hstack with activity indicator and connection label (Extension) hstack_extension_activity_and_label.addWidget(activity_indicator_extension) hstack_extension_activity_and_label.addWidget(label_connection_extension) hstack_extension_activity_and_label.addStretch() # build hstack for the extension's target IP hstack_extension_ip.addWidget(QLabel("IP")) hstack_extension_ip.addWidget(textbox_extension_ip) hstack_extension_ip.addStretch() # build hstack for the extension's target port hstack_extension_port.addWidget(QLabel("Port")) hstack_extension_port.addWidget(spinner_extension_port) hstack_extension_port.addStretch() # build hstack for the extension's own buttons hstack_extension_buttons.addWidget(button_extension_toggle) hstack_extension_buttons.addWidget(button_extension_log) hstack_extension_buttons.addStretch() # add Extension's rows to its own vstack vstack_extension.addLayout(hstack_extension_activity_and_label) vstack_extension.addLayout(hstack_extension_ip) vstack_extension.addLayout(hstack_extension_port) vstack_extension.addLayout(hstack_extension_buttons) # place OSC, Remote, and Extension columns side by side hstack_osc_and_remote.addLayout(vstack_osc) hstack_osc_and_remote.addLayout(vstack_remote) hstack_osc_and_remote.addLayout(vstack_extension) # connect signals textbox_osc_daw_ip.textChanged.connect(update_osc_server_ip) spinner_osc_send_port.valueChanged.connect(update_osc_server_port) spinner_osc_listen_port.valueChanged.connect(lambda _: save_io_config()) button_osc_toggle.clicked.connect(start_stop_osc_server) button_osc_messages_log.clicked.connect(log_ui_show_hide) button_layout_panel.clicked.connect(layout_panel_show_hide) button_focus_panel.clicked.connect(focus_panel_show_hide) # add the combined OSC + Remote stack to the row hstack_io_row.addLayout(hstack_osc_and_remote) hstack_io_row.addStretch() main_layout.addLayout(hstack_io_row) main_layout.addWidget(make_separator()) # MARK: OSC STATUS ROW global container_osc_feedback global vstack_osc_feedback global hstack_osc_transport_state global label_osc_play_state global label_osc_record_state global label_osc_position global label_osc_bar global divider_osc_feedback global hstack_osc_info global label_track_header global label_osc_track global label_preset_header global label_osc_preset global spacer_osc_info global container_osc_in_log global vstack_osc_in_log global hstack_osc_in_log_header global label_osc_in_log_title global button_osc_in_clear global textedit_osc_in_log global container_osc_out_log global vstack_osc_out_log global hstack_osc_out_log_header global label_osc_out_log_title global button_osc_out_clear global textedit_osc_out_log #MARK: OSC FEEDBACK CONTAINER # assemble transport state row hstack_osc_transport_state.addWidget(label_osc_play_state) hstack_osc_transport_state.addWidget(label_osc_record_state) hstack_osc_transport_state.addStretch() hstack_osc_transport_state.addWidget(label_osc_position) hstack_osc_transport_state.addWidget(label_osc_bar) # assemble track and preset info row hstack_osc_info.addWidget(label_track_header) hstack_osc_info.addWidget(label_osc_track) hstack_osc_info.addWidget(spacer_osc_info) hstack_osc_info.addWidget(label_preset_header) hstack_osc_info.addWidget(label_osc_preset) hstack_osc_info.addStretch() # assemble status row hstack_osc_status.addWidget(label_osc_status) hstack_osc_status.addStretch() # assemble main feedback container vstack_osc_feedback.addLayout(hstack_osc_transport_state) # play, record, tempo, position vstack_osc_feedback.addWidget(divider_osc_feedback) # divider line vstack_osc_feedback.addLayout(hstack_osc_info) # track and preset labels vstack_osc_feedback.addLayout(hstack_osc_status) # osc status messages container_osc_feedback.setVisible(False) main_layout.addWidget(container_osc_feedback) #MARK: OSC IN LOG CONTAINER # assemble header hstack_osc_in_log_header.addWidget(label_osc_in_log_title) hstack_osc_in_log_header.addStretch() hstack_osc_in_log_header.addWidget(button_osc_in_clear) # assemble container vstack_osc_in_log.addLayout(hstack_osc_in_log_header) vstack_osc_in_log.addWidget(textedit_osc_in_log) # connect signal button_osc_in_clear.clicked.connect(lambda: textedit_osc_in_log.clear()) container_osc_in_log.setVisible(False) #MARK: OSC OUT LOG CONTAINER # assemble header hstack_osc_out_log_header.addWidget(label_osc_out_log_title) hstack_osc_out_log_header.addStretch() hstack_osc_out_log_header.addWidget(button_osc_out_clear) # assemble container vstack_osc_out_log.addLayout(hstack_osc_out_log_header) vstack_osc_out_log.addWidget(textedit_osc_out_log) # connect signal button_osc_out_clear.clicked.connect(lambda: textedit_osc_out_log.clear()) container_osc_out_log.setVisible(False) #MARK: MIDI IN LOG CONTAINER hstack_midi_in_log_header.addWidget(label_midi_in_log_title) hstack_midi_in_log_header.addStretch() hstack_midi_in_log_header.addWidget(button_midi_in_clear) vstack_midi_in_log.addLayout(hstack_midi_in_log_header) vstack_midi_in_log.addWidget(textedit_midi_in_log) button_midi_in_clear.clicked.connect(lambda: textedit_midi_in_log.clear()) container_midi_in_log.setVisible(False) #MARK: MIDI OUT LOG CONTAINER hstack_midi_out_log_header.addWidget(label_midi_out_log_title) hstack_midi_out_log_header.addStretch() hstack_midi_out_log_header.addWidget(button_midi_out_clear) vstack_midi_out_log.addLayout(hstack_midi_out_log_header) vstack_midi_out_log.addWidget(textedit_midi_out_log) button_midi_out_clear.clicked.connect(lambda: textedit_midi_out_log.clear()) container_midi_out_log.setVisible(False) #MARK: WS IN LOG CONTAINER hstack_ws_in_log_header.addWidget(label_ws_in_log_title) hstack_ws_in_log_header.addStretch() hstack_ws_in_log_header.addWidget(button_ws_in_clear) vstack_ws_in_log.addLayout(hstack_ws_in_log_header) vstack_ws_in_log.addWidget(textedit_ws_in_log) button_ws_in_clear.clicked.connect(lambda: textedit_ws_in_log.clear()) container_ws_in_log.setVisible(False) #MARK: WS OUT LOG CONTAINER hstack_ws_out_log_header.addWidget(label_ws_out_log_title) hstack_ws_out_log_header.addStretch() hstack_ws_out_log_header.addWidget(button_ws_out_clear) vstack_ws_out_log.addLayout(hstack_ws_out_log_header) vstack_ws_out_log.addWidget(textedit_ws_out_log) button_ws_out_clear.clicked.connect(lambda: textedit_ws_out_log.clear()) container_ws_out_log.setVisible(False) #MARK: TABLET LOG CONTAINER hstack_tablet_log_header.addWidget(label_tablet_log_title) hstack_tablet_log_header.addStretch() hstack_tablet_log_header.addWidget(button_tablet_log_clear) vstack_tablet_log.addLayout(hstack_tablet_log_header) vstack_tablet_log.addWidget(textedit_tablet_log) button_tablet_log_clear.clicked.connect(lambda: textedit_tablet_log.clear()) container_tablet_log.setVisible(False) button_tablet_server.clicked.connect(toggle_tablet_server) button_tablet_log.clicked.connect(tablet_log_show_hide) button_ws_log.clicked.connect(ws_log_show_hide) #MARK: EXTENSION LOG CONTAINER hstack_extension_log_header.addWidget(label_extension_log_title) hstack_extension_log_header.addStretch() hstack_extension_log_header.addWidget(button_extension_log_clear) vstack_extension_log.addLayout(hstack_extension_log_header) vstack_extension_log.addWidget(textedit_extension_log) button_extension_log_clear.clicked.connect(lambda: textedit_extension_log.clear()) container_extension_log.setVisible(False) button_extension_toggle.clicked.connect(toggle_extension_connection) button_extension_log.clicked.connect(extension_log_show_hide) #MARK: LAYOUT PANEL container_layout_panel.setVisible(True) button_layout_panel.setChecked(True) #MARK: FOCUS PANEL container_focus_panel.setVisible(False) container_transport_row = QWidget() _vbox_transport_row = QVBoxLayout(container_transport_row) _vbox_transport_row.setContentsMargins(0, 0, 0, 0) _vbox_transport_row.setSpacing(0) _vbox_transport_row.addWidget(make_separator()) _vbox_transport_row.addSpacing(4) # MARK: TRANSPORT ROW CONTROLS global hstack_row_transport_control_buttons global button_transport_show_hide_config hstack_row_transport_control_buttons.addWidget(button_transport_show_hide_config) hstack_row_transport_control_buttons.addStretch() button_transport_show_hide_config.clicked.connect(transport_show_hide_config_toggle) _vbox_transport_row.addLayout(hstack_row_transport_control_buttons) #MARK: TRANSPORT ROW self._capturing_key_for = None # Globals for mutation safety global hstack_full_transport global hstack_transport_zone_left global vsep_transport_left global hstack_transport_center global vsep_transport_right global hstack_transport_zone_right # assemble full transport row hstack_full_transport.addLayout(hstack_transport_zone_left) hstack_full_transport.addWidget(vsep_transport_left) hstack_full_transport.addLayout(hstack_transport_center) hstack_full_transport.addWidget(vsep_transport_right) hstack_full_transport.addLayout(hstack_transport_zone_right) _vbox_transport_row.addLayout(hstack_full_transport) main_layout.addWidget(container_transport_row) chk_section_transport_row.toggled.connect(container_transport_row.setVisible) #MARK: PRESET BROWSER CONTAINER global container_preset_browser global label_preset_browser_title global hstack_preset_browser container_preset_browser = QWidget() vstack_preset_browser_container = QVBoxLayout(container_preset_browser) vstack_preset_browser_container.setContentsMargins(0, 0, 0, 0) hstack_preset_browser.addWidget(preset_browser_back) hstack_preset_browser.addWidget(preset_browser_fwd) hstack_preset_browser.addStretch(1) hstack_preset_browser_controls = QHBoxLayout() hstack_preset_browser_controls.addWidget(button_preset_browser_show_hide_config) hstack_preset_browser_controls.addStretch() button_preset_browser_show_hide_config.clicked.connect(preset_browser_show_hide_config_toggle) vstack_preset_browser_container.addWidget(make_separator()) vstack_preset_browser_container.addLayout(hstack_preset_browser_controls) vstack_preset_browser_container.addWidget(label_preset_browser_title) vstack_preset_browser_container.addLayout(hstack_preset_browser) main_layout.addWidget(container_preset_browser) main_layout.addWidget(make_separator()) chk_section_preset_browser.toggled.connect(container_preset_browser.setVisible) # MARK: CONTAINER ROWS AND WIDGETS (fader + toggle only) global container_rows_and_widgets global view_uuid_error_overlay global label_overlay_message global big_title_edit container_rows_and_widgets = QWidget() controls_layout = QVBoxLayout(container_rows_and_widgets) controls_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(container_rows_and_widgets) container_fader_row = QWidget() _vbox_fader_row = QVBoxLayout(container_fader_row) _vbox_fader_row.setContentsMargins(0, 0, 0, 0) _vbox_fader_row.setSpacing(0) # MARK: FADER ROW CONTROLS global hstack_row_fader_control_buttons global button_fader_show_hide_config hstack_row_fader_control_buttons.addWidget(button_fader_show_hide_config) hstack_row_fader_control_buttons.addWidget(button_fader_clear_all) hstack_row_fader_control_buttons.addStretch() button_fader_show_hide_config.clicked.connect(fader_show_hide_config_toggle) button_fader_clear_all.clicked.connect(clear_all_faders) _vbox_fader_row.addLayout(hstack_row_fader_control_buttons) # MARK: BIG TITLE big_title_edit = QLineEdit("") big_title_edit.setPlaceholderText("Title") big_title_edit.setAlignment(Qt.AlignmentFlag.AlignCenter) big_title_edit.setStyleSheet( "QLineEdit { background: transparent; border: none;" " color: #ffffff; font-size: 28px; font-weight: bold; padding: 4px 0; }" "QLineEdit:focus { border-bottom: 1px solid #00e676; }" ) _vbox_fader_row.addWidget(big_title_edit) chk_section_big_title.toggled.connect(big_title_edit.setVisible) #MARK: FADERS ROW global hstack_full_fader global hstack_fader_zone_left global separator_vertical_fader_zone_left global hstack_fader_center global separator_vertical_fader_zone_right global hstack_fader_zone_right # assemble full fader row hstack_full_fader.addLayout(hstack_fader_zone_left) hstack_full_fader.addWidget(separator_vertical_fader_zone_left) hstack_full_fader.addLayout(hstack_fader_center) hstack_full_fader.addWidget(separator_vertical_fader_zone_right) hstack_full_fader.addLayout(hstack_fader_zone_right) _vbox_fader_row.addLayout(hstack_full_fader) global placeholder_fader_empty placeholder_fader_empty = QWidget() _pf_layout = QVBoxLayout(placeholder_fader_empty) _pf_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) _pf_lbl = QLabel("No fader strips yet") _pf_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) _pf_lbl.setStyleSheet("color: #666; font-size: 13px;") _pf_btn = QPushButton("+ Add Fader") _pf_btn.setFixedWidth(160) _pf_btn.setFixedHeight(48) _pf_btn.setStyleSheet("QPushButton { background: transparent; border: 1px solid #555; color: #aaa; border-radius: 4px; font-size: 13px; } QPushButton:hover { border-color: #aaa; color: #fff; }") _pf_btn.clicked.connect(fader_add) _pf_layout.addWidget(_pf_btn, alignment=Qt.AlignmentFlag.AlignCenter) _pf_layout.addWidget(_pf_lbl) _vbox_fader_row.addWidget(placeholder_fader_empty) controls_layout.addWidget(container_fader_row) chk_section_fader_row.toggled.connect(container_fader_row.setVisible) controls_layout.addWidget(make_separator()) # Globals for mutation safety global hstack_full_toggle global hstack_toggle_zone_left global separator_vertical_toggle_zone_left global hstack_toggle_center global separator_vertical_toggle_zone_right global hstack_toggle_zone_right container_toggle_row = QWidget() _vbox_toggle_row = QVBoxLayout(container_toggle_row) _vbox_toggle_row.setContentsMargins(0, 0, 0, 0) _vbox_toggle_row.setSpacing(0) # MARK: TOGGLE ROW CONTROLS global hstack_row_toggle_control_buttons global button_toggle_show_hide_config hstack_row_toggle_control_buttons.addWidget(button_toggle_show_hide_config) hstack_row_toggle_control_buttons.addWidget(button_toggle_clear_all) hstack_row_toggle_control_buttons.addStretch() button_toggle_show_hide_config.clicked.connect(toggle_show_hide_config_toggle) button_toggle_clear_all.clicked.connect(clear_all_toggles) _vbox_toggle_row.addLayout(hstack_row_toggle_control_buttons) #MARK: TOGGLES ROW # assemble full toggle row hstack_full_toggle.addLayout(hstack_toggle_zone_left) hstack_full_toggle.addWidget(separator_vertical_toggle_zone_left) hstack_full_toggle.addLayout(hstack_toggle_center) hstack_full_toggle.addWidget(separator_vertical_toggle_zone_right) hstack_full_toggle.addLayout(hstack_toggle_zone_right) _vbox_toggle_row.addLayout(hstack_full_toggle) global placeholder_toggle_empty placeholder_toggle_empty = QWidget() _pt_layout = QVBoxLayout(placeholder_toggle_empty) _pt_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) _pt_lbl = QLabel("No toggle strips yet") _pt_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) _pt_lbl.setStyleSheet("color: #666; font-size: 13px;") _pt_btn = QPushButton("+ Add Toggle") _pt_btn.setFixedWidth(160) _pt_btn.setFixedHeight(48) _pt_btn.setStyleSheet("QPushButton { background: transparent; border: 1px solid #555; color: #aaa; border-radius: 4px; font-size: 13px; } QPushButton:hover { border-color: #aaa; color: #fff; }") _pt_btn.clicked.connect(toggle_add) _pt_layout.addWidget(_pt_btn, alignment=Qt.AlignmentFlag.AlignCenter) _pt_layout.addWidget(_pt_lbl) _vbox_toggle_row.addWidget(placeholder_toggle_empty) controls_layout.addWidget(container_toggle_row) chk_section_toggle_row.toggled.connect(container_toggle_row.setVisible) controls_layout.addStretch() # MARK: UUID ERROR OVERLAY view_uuid_error_overlay = QWidget() vstack_overlay = QVBoxLayout(view_uuid_error_overlay) vstack_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter) label_overlay_message = QLabel("") label_overlay_message.setAlignment(Qt.AlignmentFlag.AlignCenter) vstack_overlay.addWidget(label_overlay_message) view_uuid_error_overlay.setVisible(False) main_layout.addWidget(view_uuid_error_overlay) main_layout.addStretch() #MARK: SET DEFAULTS / LOAD PRESETS # Add initial 2 faders, 1 toggle, 4 transport strips loaded_presets = ensure_preset_structure(loaded_presets) # Migration: seed transport_preset from last used preset if not yet set if not loaded_presets.get("transport_preset"): last = loaded_presets.get("__last_used__") if last and last in loaded_presets["presets"]: seed = loaded_presets["presets"][last].get("transports", []) if seed: loaded_presets["transport_preset"] = seed save_presets_file(loaded_presets) global ready ready = True populate_preset_dropdown_menu() check_preset_previous_and_assign() load_transport_preset() load_io_config() load_focus_faders_state() load_focus_toggles_state() load_focus_feedbacks_state() transport_show_hide_config_set(loaded_presets.get("transport_show_hide", False)) check_and_index_strip_labels() check_midi_port_availability() midi_receiver.midi_in_signal.connect(lambda msg: make_activity_blink(activity_indicator_midi_in)) midi_receiver.midi_in_signal.connect(log_midi_incoming) midi_receiver.midi_out_signal.connect(lambda _: make_activity_blink(activity_indicator_midi_out)) midi_receiver.midi_out_signal.connect(log_midi_outgoing) midi_receiver.transport_out_signal.connect(lambda: make_activity_blink(activity_indicator_transport_out)) midi_receiver.hw_cc_signal.connect(midi_message_route_hw_to_output_cc) midi_receiver.controller_in_signal.connect(lambda msg: make_activity_blink(activity_indicator_midi_controller_in)) midi_receiver.controller_in_signal.connect(log_midi_incoming) # OSC signals osc_receiver.track_name_signal.connect(update_ui_on_osc_track_received) osc_receiver.preset_name_signal.connect(update_ui_on_osc_fx_info_received) osc_receiver.bar_signal.connect(update_ui_on_osc_bar_received) osc_receiver.position_signal.connect(update_ui_on_osc_playhead_received) osc_receiver.play_signal.connect(update_ui_on_osc_play_transport_received) osc_receiver.record_signal.connect(update_ui_on_osc_record_transport_received) osc_receiver.raw_message_signal.connect(log_osc_incoming) osc_receiver.raw_message_signal.connect(_on_osc_learn_message) osc_receiver.raw_message_signal.connect(_on_osc_focus_fader_feedback) osc_receiver.raw_message_signal.connect(_on_osc_focus_fader_name_feedback) osc_receiver.raw_message_signal.connect(_on_osc_focus_toggles_feedback) osc_receiver.raw_message_signal.connect(_on_osc_focus_feedbacks_feedback) osc_receiver.osc_out_signal.connect(log_osc_outgoing) osc_receiver.osc_out_signal.connect(lambda *_: make_activity_blink(activity_indicator_osc)) osc_receiver.uuid_preset.connect(switch_preset_by_uuid) global uuid_presets_batch_timer uuid_presets_batch_timer = QTimer() uuid_presets_batch_timer.setSingleShot(True) uuid_presets_batch_timer.timeout.connect(resolve_uuid_batch) QTimer.singleShot(0, _auto_start_osc_server) QTimer.singleShot(0, _auto_start_tablet_server) QTimer.singleShot(0, _auto_start_extension) def resizeEvent(self, event): super().resizeEvent(event) if container_osc_in_log.isVisible(): log_ui_window_calc() if container_layout_panel.isVisible(): layout_panel_window_calc() if container_focus_panel.isVisible(): focus_panel_window_calc() if container_tablet_log.isVisible(): tablet_log_window_calc() def moveEvent(self, event): super().moveEvent(event) if container_osc_in_log.isVisible(): log_ui_window_calc() if container_layout_panel.isVisible(): layout_panel_window_calc() if container_focus_panel.isVisible(): focus_panel_window_calc() if container_tablet_log.isVisible(): tablet_log_window_calc() def make_separator(): sep = QWidget() sep.setFixedHeight(1) sep.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) sep.setStyleSheet("background-color: #3a3a3a;") return sep def make_activity_blink(led): led.setStyleSheet("color: #2ecc71; font-size: 14px;") QTimer.singleShot(80, lambda: led.setStyleSheet("color: #444; font-size: 14px;")) def populate_preset_dropdown_menu(): global loaded_presets global menuPresets menuPresets.blockSignals(True) menuPresets.clear() menuPresets.addItems(sorted(loaded_presets["presets"].keys())) menuPresets.blockSignals(False) def clear_all_faders(): for f in list(faders): hstack_fader_center.removeWidget(f) f.deleteLater() faders.clear() update_window_width() _update_fader_placeholder() def clear_all_toggles(): for t in list(toggles): hstack_toggle_center.removeWidget(t) t.deleteLater() toggles.clear() update_window_width() _update_toggle_placeholder() def clear_all_strips(): clear_all_faders() clear_all_toggles() def on_preset_deleted(): populate_preset_dropdown_menu() if not loaded_presets.get("presets"): clear_all_strips() def new_blank_preset(): from PyQt6.QtWidgets import QInputDialog name, ok = QInputDialog.getText(app_window, "New Preset", "Preset name:") if not ok or not name.strip(): return name = name.strip() loaded_presets["presets"][name] = {"faders": [], "toggles": [], "preset_uuid": "", "browser": {"back": preset_browser_back.get_state(), "fwd": preset_browser_fwd.get_state()}, "show_hide": {}, "big_title": ""} loaded_presets["__last_used__"] = name save_presets_file(loaded_presets) populate_preset_dropdown_menu() menuPresets.setCurrentText(name) clear_all_strips() big_title_edit.setText("") chk_section_preset_browser.setChecked(True) chk_section_big_title.setChecked(True) chk_section_fader_row.setChecked(True) chk_section_toggle_row.setChecked(True) chk_section_transport_row.setChecked(True) show_save_status() def rename_preset(): from PyQt6.QtWidgets import QInputDialog current_name = menuPresets.currentText() if not current_name: return new_name, ok = QInputDialog.getText(app_window, "Rename Preset", "New name:", text=current_name) if not ok or not new_name.strip() or new_name.strip() == current_name: return new_name = new_name.strip() loaded_presets["presets"][new_name] = loaded_presets["presets"].pop(current_name) if loaded_presets.get("__last_used__") == current_name: loaded_presets["__last_used__"] = new_name save_presets_file(loaded_presets) populate_preset_dropdown_menu() menuPresets.setCurrentText(new_name) show_save_status() def duplicate_preset(): from PyQt6.QtWidgets import QInputDialog current_name = menuPresets.currentText() if not current_name: return new_name, ok = QInputDialog.getText(app_window, "Duplicate Preset", "Name for duplicate:", text=f"{current_name} Copy") if not ok or not new_name.strip(): return new_name = new_name.strip() snapshot = preset_build_snapshot(current_name) snapshot["preset_uuid"] = str(uuid.uuid4()) loaded_presets["presets"][new_name] = snapshot loaded_presets["__last_used__"] = new_name save_presets_file(loaded_presets) populate_preset_dropdown_menu() menuPresets.setCurrentText(new_name) label_preset_uuid.setText(snapshot["preset_uuid"]) show_save_status() def show_save_status(): global labelStatus from datetime import datetime, date now = datetime.now() today = date.today() time_part = now.strftime('%I:%M:%S %p').lstrip('0') if now.date() == today: time_str = f"Today {time_part}" elif (today - now.date()).days == 1: time_str = f"Yesterday {time_part}" else: time_str = f"{now.strftime('%b')} {now.day} {time_part}" labelStatus.setText(f"[ Saved {time_str} ]") QTimer.singleShot(4000, lambda: labelStatus.setText("")) def check_midi_port_availability(): out_port = picker_midi_out.currentText() t_port = picker_transport_out.currentText() for i in range(picker_midi_out.model().rowCount()): item = picker_midi_out.model().item(i) base = item.text().replace(" [in use]", "") if base != "Not Connected" and base == t_port and base != out_port: item.setText(f"{base} [in use]") item.setEnabled(False) item.setForeground(QColor('#888888')) else: item.setText(base) item.setEnabled(True) item.setForeground(QColor('#ffffff')) for i in range(picker_transport_out.model().rowCount()): item = picker_transport_out.model().item(i) base = item.text().replace(" [in use]", "") if base != "Not Connected" and base == out_port and base != t_port: item.setText(f"{base} [in use]") item.setEnabled(False) item.setForeground(QColor('#888888')) else: item.setText(base) item.setEnabled(True) item.setForeground(QColor('#ffffff')) def set_port_midi_output(port_name): if midi_out.is_port_open(): midi_out.close_port() if port_name == "Not Connected" or port_name not in available_ports: label_connection_midi_out.setText("Not Connected") label_connection_midi_out.setStyleSheet("color: #888;") save_io_config() return port_index = available_ports.index(port_name) midi_out.open_port(port_index) label_connection_midi_out.setText("Connected") label_connection_midi_out.setStyleSheet("color: #27ae60;") print(f"Opened output port: {port_name}") check_midi_port_availability() save_io_config() def set_port_transport_output(port_name): if transport_midi_out.is_port_open(): transport_midi_out.close_port() if port_name == "Not Connected" or not port_name or port_name not in available_ports: label_connection_transport_out.setText("Not Connected") label_connection_transport_out.setStyleSheet("color: #888;") save_io_config() return port_index = available_ports.index(port_name) transport_midi_out.open_port(port_index) label_connection_transport_out.setText("Connected") label_connection_transport_out.setStyleSheet("color: #27ae60;") print(f"Opened transport output port: {port_name}") check_midi_port_availability() save_io_config() def _append_log(textedit, line): textedit.append(line) if textedit.document().blockCount() > 200: cursor = textedit.textCursor() cursor.movePosition(cursor.MoveOperation.Start) cursor.select(cursor.SelectionType.LineUnderCursor) cursor.removeSelectedText() cursor.deleteChar() _NOTE_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"] def _midi_msg_str(msg): if len(msg) >= 3: status = msg[0] & 0xF0 ch = (msg[0] & 0x0F) + 1 if status == 0xB0: return f"CC{msg[1]} CH{ch} [{msg[2]}]" if status in (0x80, 0x90): note = msg[1] vel = msg[2] name = _NOTE_NAMES[note % 12] + str((note // 12) - 1) kind = "Note On" if status == 0x90 else "Note Off" return f"{kind} {name} ({note}) vel {vel} CH{ch}" return " ".join(f"{b:02X}" for b in msg) def log_midi_incoming(msg): from datetime import datetime ts = datetime.now().strftime("%H:%M:%S") line = f"{ts} {_midi_msg_str(msg)}" _append_log(textedit_midi_in_log, line) def log_midi_outgoing(msg): from datetime import datetime ts = datetime.now().strftime("%H:%M:%S") line = f"{ts} {_midi_msg_str(msg)}" _append_log(textedit_midi_out_log, line) def log_osc_incoming(addr, val): from datetime import datetime ts = datetime.now().strftime("%H:%M:%S") line = f"{ts} {addr} {val}" _append_log(textedit_osc_in_log, line) def log_osc_outgoing(addr, val): from datetime import datetime ts = datetime.now().strftime("%H:%M:%S") line = f"{ts} {addr} {val}" _append_log(textedit_osc_out_log, line) def log_ws_incoming(message): import json from datetime import datetime ts = datetime.now().strftime("%H:%M:%S") try: obj = json.loads(message) pretty = json.dumps(obj, indent=2) except Exception: pretty = message header = f"{ts} ◀ WS In" body = f"
{pretty}"
textedit_ws_in_log.append(header)
textedit_ws_in_log.append(body)
textedit_ws_in_log.append("───────────────────")
def log_ws_outgoing(data):
import json
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
pretty = json.dumps(data, indent=2)
header = f"{ts} ▶ WS Out"
body = f"{pretty}"
textedit_ws_out_log.append(header)
textedit_ws_out_log.append(body)
textedit_ws_out_log.append("───────────────────")
def update_osc_server_ip(ip):
set_osc_target(ip=ip.strip() or "127.0.0.1")
save_io_config()
def update_osc_server_port(port):
set_osc_target(port=port)
save_io_config()
def _auto_start_osc_server():
should_start = loaded_presets.get("io_config", {}).get("osc_running", True)
if should_start:
start_stop_osc_server()
def is_osc_port_in_use(port):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
try:
s.bind(("0.0.0.0", port))
return False
except OSError:
return True
def start_stop_osc_server():
if _osc_server is not None:
stop_osc_server()
label_connection_osc.setText("Not Listening")
label_connection_osc.setStyleSheet("color: #888;")
button_osc_toggle.setText("Start OSC")
activity_indicator_osc.setStyleSheet("color: #444; font-size: 14px;")
container_osc_feedback.setVisible(False)
save_osc_running_state(False)
else:
port = spinner_osc_listen_port.value()
if is_osc_port_in_use(port):
label_connection_osc.setText(f"Port {port} in use")
label_connection_osc.setStyleSheet("color: #e67e22;")
return
ok = start_osc_server(port)
if ok:
label_connection_osc.setText(f"Listening :{port}")
label_connection_osc.setStyleSheet("color: #27ae60;")
button_osc_toggle.setText("Stop OSC")
activity_indicator_osc.setStyleSheet("color: #2ecc71; font-size: 14px;")
container_osc_feedback.setVisible(True)
save_osc_running_state(True)
else:
label_connection_osc.setText("Failed")
label_connection_osc.setStyleSheet("color: #c0392b;")
def _check_track_uuid_arrived():
global _track_uuid_check_pending
if _track_uuid_check_pending:
_track_uuid_check_pending = False
label_osc_status.setText("No preset linked to this track")
label_osc_status.setStyleSheet("color: #f0c040; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
ui_show_uuid_error_overlay("No preset linked to this track", "#f0c040")
def update_ui_on_osc_track_received(name):
global _track_uuid_check_pending
label_osc_track.setText(name)
make_activity_blink(activity_indicator_osc)
label_osc_status.setText("")
_track_uuid_check_pending = True
QTimer.singleShot(400, _check_track_uuid_arrived)
if ws_server:
ws_server.broadcast_daw_state(track=name)
def update_ui_on_osc_fx_info_received(name):
label_osc_preset.setText(name)
make_activity_blink(activity_indicator_osc)
def update_ui_on_osc_bar_received(bar):
label_osc_bar.setText(f"Bar: {bar}")
if ws_server:
ws_server.broadcast_daw_state(bar=bar)
def update_ui_on_osc_playhead_received(pos):
label_osc_position.setText(f"Time: {pos}")
def update_ui_on_osc_play_transport_received(state):
if state:
label_osc_play_state.setText("▶ PLAYING")
label_osc_play_state.setStyleSheet("color: #00e676; font-size: 11px; font-weight: bold;")
else:
label_osc_play_state.setText("■ STOPPED")
label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
if ws_server:
ws_server.broadcast_daw_state(playing=bool(state))
def update_ui_on_osc_record_transport_received(state):
if state:
label_osc_record_state.setText("● REC")
label_osc_record_state.setStyleSheet("color: #e53935; font-size: 11px; font-weight: bold;")
else:
label_osc_record_state.setText("● REC")
label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
if ws_server:
ws_server.broadcast_daw_state(recording=bool(state))
def check_and_start_midi_learn(strip):
global learning_strip
if learning_strip and learning_strip != strip:
learning_strip.stop_learn()
learning_strip = strip
strip.start_learn()
def check_and_start_focus_hw_learn(widget):
"""Same shared learning_strip mechanism as check_and_start_midi_learn, but
for Channel Focus faders/toggles — their start_learn/stop_learn names are
already used for OSC address learn, so this calls the differently-named
start_hw_learn/stop_hw_learn instead, to bind the optional Hardware CC."""
global learning_strip
if learning_strip and learning_strip != widget:
if hasattr(learning_strip, "stop_hw_learn"):
learning_strip.stop_hw_learn()
else:
learning_strip.stop_learn()
learning_strip = widget
widget.start_hw_learn()
preset_browser_back.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(preset_browser_back))
preset_browser_fwd.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(preset_browser_fwd))
def update_window_width():
nf = len(faders)
nt = len(toggles)
ntr = len(transports)
fader_w = (nf * strip_width) + ((nf - 1) * spacing) + padding
toggle_w = (nt * toggle_strip_width) + ((nt - 1) * spacing) + padding
transport_w = (ntr * strip_width) + ((ntr - 1) * spacing) + padding
w = max(fader_w, toggle_w, transport_w, 300)
app_window.resize(w, app_window.height())
def check_and_index_strip_labels():
center = [f for f in faders if not f.zone]
for i, f in enumerate(center):
f.update_id_label(None, i)
left = sorted([f for f in faders if f.zone == "left"], key=lambda f: f.zone_index or 0)
for i, f in enumerate(left):
f.update_id_label("left", i)
right = sorted([f for f in faders if f.zone == "right"], key=lambda f: f.zone_index or 0)
for i, f in enumerate(right):
f.update_id_label("right", i)
t_center = [t for t in toggles if not t.zone]
for i, t in enumerate(t_center):
t.update_id_label(None, i)
t_left = sorted([t for t in toggles if t.zone == "left"], key=lambda t: t.zone_index or 0)
for i, t in enumerate(t_left):
t.update_id_label("left", i)
t_right = sorted([t for t in toggles if t.zone == "right"], key=lambda t: t.zone_index or 0)
for i, t in enumerate(t_right):
t.update_id_label("right", i)
tr_center = [t for t in transports if not t.zone]
for i, t in enumerate(tr_center):
t.update_id_label(None, i)
tr_left = sorted([t for t in transports if t.zone == "left"], key=lambda t: t.zone_index or 0)
for i, t in enumerate(tr_left):
t.update_id_label("left", i)
tr_right = sorted([t for t in transports if t.zone == "right"], key=lambda t: t.zone_index or 0)
for i, t in enumerate(tr_right):
t.update_id_label("right", i)
separator_vertical_fader_zone_left.setVisible(any(f.zone == "left" for f in faders))
separator_vertical_fader_zone_right.setVisible(any(f.zone == "right" for f in faders))
separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in toggles))
separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in toggles))
def clear_all_selections():
global selected_strips, last_clicked_strip
for s in selected_strips:
s.set_selected(False)
selected_strips = []
last_clicked_strip = None
label_selected_channel.setText("")
def _get_zone_strips(strip):
if isinstance(strip, FaderWidget):
pool = faders
elif isinstance(strip, TransportWidget):
pool = transports
else:
pool = toggles
same_zone = [s for s in pool if s.zone == strip.zone]
if strip.zone is not None:
same_zone.sort(key=lambda s: s.zone_index or 0)
return same_zone
def check_and_update_selected_strip(strip):
global selected_strips, last_clicked_strip
from PyQt6.QtWidgets import QApplication
mods = QApplication.queryKeyboardModifiers()
ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier)
shift = bool(mods & Qt.KeyboardModifier.ShiftModifier)
if shift and last_clicked_strip and last_clicked_strip != strip:
if type(strip) is not type(last_clicked_strip):
return
if strip.zone != last_clicked_strip.zone:
return
zone_strips = _get_zone_strips(strip)
if last_clicked_strip not in zone_strips or strip not in zone_strips:
return
a, b = zone_strips.index(last_clicked_strip), zone_strips.index(strip)
new_range = zone_strips[min(a,b):max(a,b)+1]
for s in selected_strips:
s.set_selected(False)
selected_strips = new_range
for s in selected_strips:
s.set_selected(True)
elif ctrl:
if selected_strips:
if type(strip) is not type(selected_strips[0]):
return
if strip.zone != selected_strips[0].zone:
return
if strip in selected_strips:
selected_strips.remove(strip)
strip.set_selected(False)
else:
selected_strips.append(strip)
strip.set_selected(True)
last_clicked_strip = strip
else:
for s in selected_strips:
s.set_selected(False)
selected_strips = [strip]
strip.set_selected(True)
last_clicked_strip = strip
if selected_strips:
s = selected_strips[0]
if isinstance(s, FaderWidget):
kind = "fader" if len(selected_strips) == 1 else "faders"
elif isinstance(s, ToggleWidget):
kind = "toggle" if len(selected_strips) == 1 else "toggles"
elif isinstance(s, TransportWidget):
kind = "transport toggle" if len(selected_strips) == 1 else "transport toggles"
else:
kind = "strip" if len(selected_strips) == 1 else "strips"
label_selected_channel.setText(f"{len(selected_strips)} {kind} selected")
else:
label_selected_channel.setText("")
def strip_context_menu(strip, global_pos):
if strip not in selected_strips:
clear_all_selections()
return
from PyQt6.QtWidgets import QMenu, QInputDialog
menu = QMenu()
action_cascade = menu.addAction("Cascade CC")
action_channel = menu.addAction("Assign Channel")
menu.addSeparator()
action_deselect = menu.addAction("Deselect All")
action = menu.exec(global_pos)
if action == action_cascade:
start, ok = QInputDialog.getInt(app_window, "Cascade CC", "Starting CC:", 0, 0, 127)
if ok:
for i, s in enumerate(selected_strips):
s.cc_spin.setValue((start + i) % 128)
elif action == action_channel:
ch, ok = QInputDialog.getInt(app_window, "Assign Channel", "Channel (1–16):", 1, 1, 16)
if ok:
for s in selected_strips:
s.ch_spin.setValue(ch)
elif action == action_deselect:
clear_all_selections()
def handle_toggle_zone_change(toggle):
if toggle.zone is not None and toggle.center_index is None:
if toggle in toggles:
toggle.center_index = toggles.index(toggle)
for layout in [hstack_toggle_center, hstack_toggle_zone_left, hstack_toggle_zone_right]:
for i in range(layout.count()):
item = layout.itemAt(i)
if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == toggle.uid:
layout.removeWidget(toggle)
toggle.setParent(None)
break
if toggle.zone == "left":
toggle.zone_index = hstack_toggle_zone_left.count() - 1
hstack_toggle_zone_left.addWidget(toggle)
separator_vertical_toggle_zone_left.setVisible(True)
elif toggle.zone == "right":
toggle.zone_index = hstack_toggle_zone_right.count() - 1
hstack_toggle_zone_right.addWidget(toggle)
separator_vertical_toggle_zone_right.setVisible(True)
else:
idx = toggle.center_index if toggle.center_index is not None else hstack_toggle_center.count()
idx = min(idx, hstack_toggle_center.count())
hstack_toggle_center.insertWidget(idx, toggle)
toggle.center_index = None
toggle.zone_index = None
separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in toggles))
separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in toggles))
update_window_width()
check_and_index_strip_labels()
def handle_fader_zone_change(fader):
if fader.zone is not None and fader.center_index is None:
if fader in faders:
fader.center_index = faders.index(fader)
for layout in [hstack_fader_center, hstack_fader_zone_left, hstack_fader_zone_right]:
for i in range(layout.count()):
item = layout.itemAt(i)
if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == fader.uid:
layout.removeWidget(fader)
fader.setParent(None)
break
if fader.zone == "left":
fader.zone_index = hstack_fader_zone_left.count() - 1
hstack_fader_zone_left.addWidget(fader)
separator_vertical_fader_zone_left.setVisible(True)
elif fader.zone == "right":
fader.zone_index = hstack_fader_zone_right.count() - 1
hstack_fader_zone_right.addWidget(fader)
separator_vertical_fader_zone_right.setVisible(True)
else:
idx = fader.center_index if fader.center_index is not None else hstack_fader_center.count()
idx = min(idx, hstack_fader_center.count())
hstack_fader_center.insertWidget(idx, fader)
fader.center_index = None
fader.zone_index = None
separator_vertical_fader_zone_left.setVisible(any(f.zone == "left" for f in faders))
separator_vertical_fader_zone_right.setVisible(any(f.zone == "right" for f in faders))
update_window_width()
check_and_index_strip_labels()
def handle_transport_zone_change(transport):
if transport.zone is not None and transport.center_index is None:
if transport in transports:
transport.center_index = transports.index(transport)
for layout in [hstack_transport_center, hstack_transport_zone_left, hstack_transport_zone_right]:
for i in range(layout.count()):
item = layout.itemAt(i)
if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == transport.uid:
layout.removeWidget(transport)
transport.setParent(None)
break
if transport.zone == "left":
transport.zone_index = hstack_transport_zone_left.count()
hstack_transport_zone_left.addWidget(transport)
vsep_transport_left.setVisible(True)
elif transport.zone == "right":
transport.zone_index = hstack_transport_zone_right.count()
hstack_transport_zone_right.addWidget(transport)
vsep_transport_right.setVisible(True)
else:
idx = transport.center_index if transport.center_index is not None else hstack_transport_center.count()
hstack_transport_center.insertWidget(min(idx, hstack_transport_center.count()), transport)
transport.center_index = None
transport.zone_index = None
vsep_transport_left.setVisible(any(t.zone == "left" for t in transports))
vsep_transport_right.setVisible(any(t.zone == "right" for t in transports))
check_and_index_strip_labels()
def _update_fader_placeholder():
if placeholder_fader_empty is not None:
placeholder_fader_empty.setVisible(len(faders) == 0)
def _update_toggle_placeholder():
if placeholder_toggle_empty is not None:
placeholder_toggle_empty.setVisible(len(toggles) == 0)
def toggle_add(insert_after=None):
n = len(toggles) + 1
toggle = ToggleWidget(f"Toggle {n}", default_cc=20)
if insert_after is not None and insert_after in toggles:
idx = toggles.index(insert_after)
toggles.insert(idx + 1, toggle)
hstack_toggle_center.insertWidget(idx + 1, toggle)
else:
toggles.append(toggle)
hstack_toggle_center.addWidget(toggle)
toggle.plus_btn.clicked.connect(lambda: toggle_add(insert_after=toggle))
toggle.minus_btn.clicked.connect(lambda: toggle_remove(toggle))
toggle.on_select_callback = check_and_update_selected_strip
toggle.on_context_menu_callback = strip_context_menu
toggle.on_zone_change_callback = handle_toggle_zone_change
toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle))
toggle.remote_checkbox.clicked.connect(lambda: _on_remote_checkbox_changed())
toggle.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
toggle.title_edit.editingFinished.connect(_broadcast_preset_update)
update_window_width()
check_and_index_strip_labels()
_update_toggle_placeholder()
def toggle_remove(toggle):
if len(toggles) <= 0:
return
if toggle in selected_strips:
clear_all_selections()
label_selected_channel.setText("")
hstack_toggle_center.removeWidget(toggle)
toggle.deleteLater()
toggles.remove(toggle)
update_window_width()
check_and_index_strip_labels()
_update_toggle_placeholder()
def fader_add(insert_after=None):
n = len(faders) + 1
fader = FaderWidget(f"Fader {n}", default_cc=1)
if insert_after is not None and insert_after in faders:
idx = faders.index(insert_after)
faders.insert(idx + 1, fader)
hstack_fader_center.insertWidget(idx + 1, fader)
else:
faders.append(fader)
hstack_fader_center.addWidget(fader)
fader.plus_btn.clicked.connect(lambda: fader_add(insert_after=fader))
fader.minus_btn.clicked.connect(lambda: fader_remove(fader))
fader.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(fader))
fader.on_select_callback = check_and_update_selected_strip
fader.on_context_menu_callback = strip_context_menu
fader.on_zone_change_callback = handle_fader_zone_change
fader.remote_checkbox.clicked.connect(lambda: _on_remote_checkbox_changed())
fader.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
fader.title_edit.editingFinished.connect(_broadcast_preset_update)
update_window_width()
check_and_index_strip_labels()
_update_fader_placeholder()
def fader_remove(fader):
if len(faders) <= 0:
return
if fader in selected_strips:
clear_all_selections()
hstack_fader_center.removeWidget(fader)
fader.deleteLater()
faders.remove(fader)
update_window_width()
check_and_index_strip_labels()
_update_fader_placeholder()
def focus_fader_add(insert_after=None):
n = len(focus_faders) + 1
fader = FocusFaderWidget(f"Focus {n}")
if insert_after is not None and insert_after in focus_faders:
idx = focus_faders.index(insert_after)
focus_faders.insert(idx + 1, fader)
hstack_focus_faders.insertWidget(idx + 1, fader)
else:
focus_faders.append(fader)
hstack_focus_faders.addWidget(fader)
_wire_focus_fader(fader)
update_focus_panel_width()
def focus_fader_remove(fader):
if len(focus_faders) <= 1:
return
hstack_focus_faders.removeWidget(fader)
fader.deleteLater()
focus_faders.remove(fader)
update_focus_panel_width()
def focus_toggle_add(insert_after=None):
n = len(focus_toggles) + 1
toggle = FocusToggleWidget(f"Toggle {n}")
if insert_after is not None and insert_after in focus_toggles:
idx = focus_toggles.index(insert_after)
focus_toggles.insert(idx + 1, toggle)
hstack_focus_toggles.insertWidget(idx + 1, toggle)
else:
focus_toggles.append(toggle)
hstack_focus_toggles.addWidget(toggle)
_wire_focus_toggle(toggle)
update_focus_panel_width()
def focus_toggle_remove(toggle):
if len(focus_toggles) <= 0:
return
hstack_focus_toggles.removeWidget(toggle)
toggle.deleteLater()
focus_toggles.remove(toggle)
update_focus_panel_width()
def focus_feedback_add(insert_after=None):
n = len(focus_feedbacks) + 1
widget = FocusFeedbackWidget(f"Feedback {n}")
if insert_after is not None and insert_after in focus_feedbacks:
idx = focus_feedbacks.index(insert_after)
focus_feedbacks.insert(idx + 1, widget)
hstack_focus_feedbacks.insertWidget(idx + 1, widget)
else:
focus_feedbacks.append(widget)
hstack_focus_feedbacks.addWidget(widget)
_wire_focus_feedback(widget)
update_focus_panel_width()
def focus_feedback_remove(widget):
if len(focus_feedbacks) <= 0:
return
hstack_focus_feedbacks.removeWidget(widget)
widget.deleteLater()
focus_feedbacks.remove(widget)
update_focus_panel_width()
def transport_toggle_add(label, default_cc, insert_after=None):
t = TransportWidget(label, default_cc)
t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
t.on_context_menu_callback = strip_context_menu
t.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(t))
t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t))
t.minus_btn.clicked.connect(lambda: transport_remove(t))
t.remote_checkbox.clicked.connect(lambda: _on_remote_checkbox_changed())
t.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
t.title_edit.editingFinished.connect(_broadcast_preset_update)
params_visible = not transport_show_hide_config
for attr in ['grp_learn', 'arrow_lbl', 'grp_cc', 'grp_remote',
'zone_btn', 'minus_btn', 'plus_btn', 'title_edit', 'id_sep', 'id_label', 'color_swatch_btn']:
getattr(t, attr).setVisible(params_visible)
if insert_after is not None and insert_after in transports:
idx = transports.index(insert_after)
transports.insert(idx + 1, t)
update_window_width()
hstack_transport_center.insertWidget(idx + 1, t)
else:
transports.append(t)
update_window_width()
hstack_transport_center.addWidget(t)
check_and_index_strip_labels()
def transport_remove(t):
if len(transports) <= 1:
return
if t in selected_strips:
clear_all_selections()
for layout in [hstack_transport_zone_left, hstack_transport_center, hstack_transport_zone_right]:
layout.removeWidget(t)
t.deleteLater()
transports.remove(t)
vsep_transport_left.setVisible(any(tr.zone == "left" for tr in transports))
vsep_transport_right.setVisible(any(tr.zone == "right" for tr in transports))
update_window_width()
check_and_index_strip_labels()
def load_transport_preset():
states = loaded_presets.get("transport_preset", [])
if not states:
return
for t in list(transports):
for layout in [hstack_transport_zone_left, hstack_transport_center, hstack_transport_zone_right]:
layout.removeWidget(t)
t.deleteLater()
transports.clear()
for state in states:
transport_toggle_add(state.get("label", "T"), state.get("cc", 20))
transports[-1].set_state(state)
handle_transport_zone_change(transports[-1])
def save_transport_preset():
loaded_presets["transport_preset"] = [t.get_state() for t in transports]
def load_focus_faders_state():
states = loaded_presets.get("focus_faders")
if not states:
legacy = loaded_presets.get("focus_fader") # pre-multi-fader single-widget format
states = [legacy] if legacy else None
if not states:
return
for f in list(focus_faders):
hstack_focus_faders.removeWidget(f)
f.deleteLater()
focus_faders.clear()
for state in states:
focus_fader_add()
focus_faders[-1].set_state(state)
update_focus_panel_width()
def save_focus_faders_state():
loaded_presets["focus_faders"] = [f.get_state() for f in focus_faders]
loaded_presets.pop("focus_fader", None)
def load_focus_toggles_state():
states = loaded_presets.get("focus_toggles", [])
if not states:
return
for t in list(focus_toggles):
hstack_focus_toggles.removeWidget(t)
t.deleteLater()
focus_toggles.clear()
for state in states:
focus_toggle_add()
focus_toggles[-1].set_state(state)
update_focus_panel_width()
def save_focus_toggles_state():
loaded_presets["focus_toggles"] = [t.get_state() for t in focus_toggles]
def load_focus_feedbacks_state():
states = loaded_presets.get("focus_feedbacks", [])
if not states:
return
for w in list(focus_feedbacks):
hstack_focus_feedbacks.removeWidget(w)
w.deleteLater()
focus_feedbacks.clear()
for state in states:
focus_feedback_add()
focus_feedbacks[-1].set_state(state)
update_focus_panel_width()
def save_focus_feedbacks_state():
loaded_presets["focus_feedbacks"] = [w.get_state() for w in focus_feedbacks]
def save_io_config():
loaded_presets["io_config"] = {
"midi_out": picker_midi_out.currentText(),
"midi_in": picker_midi_in.currentText(),
"midi_controller_in": picker_midi_controller_in.currentText(),
"transport_out": picker_transport_out.currentText(),
"osc_ip": textbox_osc_daw_ip.text(),
"osc_listen_port": spinner_osc_listen_port.value(),
"osc_send_port": spinner_osc_send_port.value(),
# Preserve whatever running-state was last explicitly recorded by
# start_stop_osc_server() rather than recomputing it here — this function
# also runs while fields are being restored at load time, before the
# server has had a chance to auto-start.
"osc_running": loaded_presets.get("io_config", {}).get("osc_running", True),
"tablet_running": loaded_presets.get("io_config", {}).get("tablet_running", True),
"extension_ip": textbox_extension_ip.text(),
"extension_port": spinner_extension_port.value(),
"extension_running": loaded_presets.get("io_config", {}).get("extension_running", True),
}
save_presets_file(loaded_presets)
def save_osc_running_state(running):
loaded_presets.setdefault("io_config", {})["osc_running"] = running
save_io_config()
def save_tablet_running_state(running):
loaded_presets.setdefault("io_config", {})["tablet_running"] = running
save_io_config()
def save_extension_running_state(running):
loaded_presets.setdefault("io_config", {})["extension_running"] = running
save_io_config()
def load_io_config():
global available_ports_midi_in
global available_ports_midi_controller_in
cfg = loaded_presets.get("io_config", {})
if not cfg:
return
midi_out = cfg.get("midi_out", "")
picker_midi_out.setCurrentText(midi_out if midi_out in available_ports else "Not Connected")
transport_out = cfg.get("transport_out", "")
picker_transport_out.setCurrentText(transport_out if transport_out in available_ports else "Not Connected")
midi_in = cfg.get("midi_in", "")
input_names = [entry[0] for entry in available_ports_midi_in]
picker_midi_in.setCurrentText(midi_in if midi_in in input_names else "Not Connected")
midi_controller_in = cfg.get("midi_controller_in", "")
controller_input_names = [entry[0] for entry in available_ports_midi_controller_in]
picker_midi_controller_in.setCurrentText(midi_controller_in if midi_controller_in in controller_input_names else "Not Connected")
ip = cfg.get("osc_ip", "")
if ip:
textbox_osc_daw_ip.setText(ip)
listen = cfg.get("osc_listen_port", 0)
if listen:
spinner_osc_listen_port.setValue(listen)
send = cfg.get("osc_send_port", 0)
if send:
spinner_osc_send_port.setValue(send)
ext_ip = cfg.get("extension_ip", "")
if ext_ip:
textbox_extension_ip.setText(ext_ip)
ext_port = cfg.get("extension_port", 0)
if ext_port:
spinner_extension_port.setValue(ext_port)
def preset_build_snapshot(name):
save_transport_preset()
save_focus_faders_state()
save_focus_toggles_state()
save_focus_feedbacks_state()
return {
"faders": [f.get_state() for f in faders],
"toggles": [t.get_state() for t in toggles],
"browser": {
"back": preset_browser_back.get_state(),
"fwd": preset_browser_fwd.get_state(),
},
"big_title": big_title_edit.text(),
"preset_uuid": label_preset_uuid.text().strip(),
"show_hide": {
"fader": fader_show_hide_config,
"toggle": toggle_show_hide_config,
"transport": transport_show_hide_config,
"browser": preset_browser_show_hide_config,
},
"sections": {
"preset_browser": chk_section_preset_browser.isChecked(),
"big_title": chk_section_big_title.isChecked(),
"fader_row": chk_section_fader_row.isChecked(),
"toggle_row": chk_section_toggle_row.isChecked(),
"transport_row": chk_section_transport_row.isChecked(),
}
}
def _build_tablet_snapshot():
name = menuPresets.currentText()
saved_layout = loaded_presets.get("presets", {}).get(name, {}).get("tablet_layout", {})
snapshot = {
"preset_uuid": label_preset_uuid.text().strip(),
"preset_name": name,
"big_title": big_title_edit.text() if big_title_edit else "",
"faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "dest_id": f.dest_spin.value() if f.remote_checkbox.isChecked() else 0, "color": PALETTE_HEX.get(f.color_name, "#00e676")} for f in faders],
"toggles": [{"uid": t.uid, "label": t.title_edit.text(), "state": t.toggle_state, "dest_id": t.dest_spin.value() if t.remote_checkbox.isChecked() else 0, "color": PALETTE_HEX.get(t.color_name, "#00e676"), "color_name": t.color_name} for t in toggles],
"transports": [{"uid": t.uid, "label": t.title_edit.text(), "dest_id": t.dest_spin.value() if t.remote_checkbox.isChecked() else 0, "color": PALETTE_HEX.get(t.color_name, "#00e676")} for t in transports],
"focus_faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "color": PALETTE_HEX.get(f.color_name, "#00e676")} for f in focus_faders],
"focus_toggles": [{"uid": t.uid, "label": t.title_edit.text(), "state": t.toggle_state, "color": PALETTE_HEX.get(t.color_name, "#00e676"), "color_name": t.color_name, "trigger_mode": t.trigger_mode} for t in focus_toggles],
"focus_feedbacks": [{"uid": w.uid, "label": w.title_edit.text(), "text": w.readout_lbl.text(), "color": PALETTE_HEX.get(w.color_name, "#00e676")} for w in focus_feedbacks],
}
return snapshot, saved_layout
def _on_tablet_connected():
if ws_server:
snapshot, layout = _build_tablet_snapshot()
ws_server.broadcast_preset(snapshot, layout)
log_ws_outgoing(snapshot)
def _on_widget_visibility_from_tablet(uid, dest_id):
for w in faders + toggles + transports:
if w.uid == uid:
w.remote_checkbox.blockSignals(True)
w.remote_checkbox.setChecked(dest_id > 0)
w.remote_checkbox.blockSignals(False)
w.dest_spin.blockSignals(True)
if dest_id > 0:
w.dest_spin.setValue(dest_id)
w.dest_spin.setEnabled(dest_id > 0)
w.dest_spin.blockSignals(False)
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
loaded_presets["presets"][name].update(preset_build_snapshot(name))
save_presets_file(loaded_presets)
show_save_status()
break
def _broadcast_preset_update():
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
loaded_presets["presets"][name].update(preset_build_snapshot(name))
save_presets_file(loaded_presets)
if ws_server and ws_server.has_clients:
snapshot, layout = _build_tablet_snapshot()
ws_server.broadcast_preset(snapshot, layout)
log_ws_outgoing(snapshot)
# Channel Focus mode ignores "preset" events on the iPad, so labels
# (e.g. edited titles) need their own explicit push to stay in sync.
for f in focus_faders:
ws_server.broadcast_widget_label(f.uid, f.title_edit.text())
for t in focus_toggles:
ws_server.broadcast_widget_label(t.uid, t.title_edit.text())
for w in focus_feedbacks:
ws_server.broadcast_widget_label(w.uid, w.title_edit.text())
def _on_remote_checkbox_changed():
_broadcast_preset_update()
def _on_tablet_layout_saved(preset_uuid, layout):
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
if loaded_presets["presets"][name].get("preset_uuid") == preset_uuid:
loaded_presets["presets"][name]["tablet_layout"] = layout
save_presets_file(loaded_presets)
def preset_load(preset):
global selected_strips
selected_strips = []
fader_states = preset.get("faders", [])
while len(faders) < len(fader_states):
fader_add()
while len(faders) > len(fader_states):
widget = faders.pop()
hstack_fader_center.removeWidget(widget)
widget.deleteLater()
update_window_width()
_update_fader_placeholder()
for i, widget in enumerate(faders):
widget.set_state(fader_states[i])
center_faders = sorted(
[f for f in faders if not f.zone],
key=lambda f: f.center_index if f.center_index is not None else 999
)
for f in center_faders:
hstack_fader_center.removeWidget(f)
f.setParent(None)
for f in center_faders:
hstack_fader_center.addWidget(f)
left_faders = sorted(
[f for f in faders if f.zone == "left"],
key=lambda f: f.zone_index if f.zone_index is not None else 999
)
right_faders = sorted(
[f for f in faders if f.zone == "right"],
key=lambda f: f.zone_index if f.zone_index is not None else 999
)
for f in left_faders:
hstack_fader_center.removeWidget(f)
f.setParent(None)
hstack_fader_zone_left.addWidget(f)
for f in right_faders:
hstack_fader_center.removeWidget(f)
f.setParent(None)
hstack_fader_zone_right.addWidget(f)
update_window_width()
if "toggles" in preset:
toggle_states = preset["toggles"]
elif "toggle" in preset:
toggle_states = [preset["toggle"]]
else:
toggle_states = []
while len(toggles) < len(toggle_states):
toggle_add()
while len(toggles) > len(toggle_states):
widget = toggles.pop()
hstack_toggle_center.removeWidget(widget)
widget.deleteLater()
update_window_width()
_update_toggle_placeholder()
for i, widget in enumerate(toggles):
widget.set_state(toggle_states[i])
center_toggles = sorted(
[t for t in toggles if not t.zone],
key=lambda t: t.center_index if t.center_index is not None else 999
)
for t in center_toggles:
hstack_toggle_center.removeWidget(t)
t.setParent(None)
for t in center_toggles:
hstack_toggle_center.addWidget(t)
left_toggles = sorted(
[t for t in toggles if t.zone == "left"],
key=lambda t: t.zone_index if t.zone_index is not None else 999
)
right_toggles = sorted(
[t for t in toggles if t.zone == "right"],
key=lambda t: t.zone_index if t.zone_index is not None else 999
)
for t in left_toggles:
hstack_toggle_center.removeWidget(t)
t.setParent(None)
hstack_toggle_zone_left.addWidget(t)
for t in right_toggles:
hstack_toggle_center.removeWidget(t)
t.setParent(None)
hstack_toggle_zone_right.addWidget(t)
browser = preset.get("browser", {})
# migrate old format that only stored cc numbers
if "back" not in browser and "back_cc" in browser:
browser = {
"back": {"label": "Back", "cc": browser.get("back_cc", 22), "ch": 1},
"fwd": {"label": "Fwd", "cc": browser.get("fwd_cc", 23), "ch": 1},
}
preset_browser_back.set_state(browser.get("back", {"label": "Back", "cc": 22, "ch": 1}))
preset_browser_fwd.set_state(browser.get("fwd", {"label": "Fwd", "cc": 23, "ch": 1}))
show_hide = preset.get("show_hide", {})
fader_show_hide_config_set(show_hide.get("fader", False))
button_fader_show_hide_config.setText("Show Parameters" if show_hide.get("fader", False) else "Hide Parameters")
button_fader_show_hide_config.setChecked(show_hide.get("fader", False))
toggle_show_hide_config_set(show_hide.get("toggle", False))
button_toggle_show_hide_config.setText("Show Parameters" if show_hide.get("toggle", False) else "Hide Parameters")
button_toggle_show_hide_config.setChecked(show_hide.get("toggle", False))
preset_browser_show_hide_config_set(show_hide.get("browser", False))
button_preset_browser_show_hide_config.setText("Show Parameters" if show_hide.get("browser", False) else "Hide Parameters")
button_preset_browser_show_hide_config.setChecked(show_hide.get("browser", False))
big_title_edit.setText(preset.get("big_title", ""))
label_preset_uuid.setText(preset.get("preset_uuid", ""))
sections = preset.get("sections", {})
chk_section_preset_browser.setChecked(sections.get("preset_browser", True))
chk_section_big_title.setChecked(sections.get("big_title", True))
chk_section_fader_row.setChecked(sections.get("fader_row", True))
chk_section_toggle_row.setChecked(sections.get("toggle_row", True))
chk_section_transport_row.setChecked(sections.get("transport_row", True))
check_and_index_strip_labels()
if ws_server and ws_server.has_clients:
snapshot, layout = _build_tablet_snapshot()
ws_server.broadcast_preset(snapshot, layout)
log_ws_outgoing(snapshot)
def export_preset():
import json as _json
from PyQt6.QtWidgets import QFileDialog, QMessageBox
msg = QMessageBox(app_window)
msg.setWindowTitle("Export")
msg.setText("Export current preset or all presets?")
msg.setIcon(QMessageBox.Icon.NoIcon)
btn_current = msg.addButton("Current", QMessageBox.ButtonRole.AcceptRole)
btn_all = msg.addButton("All", QMessageBox.ButtonRole.AcceptRole)
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
msg.exec()
clicked = msg.clickedButton()
if clicked is btn_current:
name = menuPresets.currentText()
if not name or name not in loaded_presets["presets"]:
return
default_path = os.path.join(os.path.expanduser("~"), f"{name}.json")
path, _ = QFileDialog.getSaveFileName(app_window, "Export Preset", default_path, "JSON Files (*.json)")
if not path:
return
with open(path, "w") as f:
_json.dump(loaded_presets["presets"][name], f, indent=2)
show_save_status()
elif clicked is btn_all:
default_path = os.path.join(os.path.expanduser("~"), "presets.json")
path, _ = QFileDialog.getSaveFileName(app_window, "Export All Presets", default_path, "JSON Files (*.json)")
if not path:
return
with open(path, "w") as f:
_json.dump(loaded_presets["presets"], f, indent=2)
show_save_status()
def import_preset():
import json as _json
from PyQt6.QtWidgets import QFileDialog, QInputDialog
path, _ = QFileDialog.getOpenFileName(app_window, "Import Preset", "", "JSON Files (*.json)")
if not path:
return
with open(path, "r") as f:
data = _json.load(f)
default_name = os.path.splitext(os.path.basename(path))[0]
name, ok = QInputDialog.getText(app_window, "Import Preset", "Preset name:", text=default_name)
if not ok or not name.strip():
return
name = name.strip()
loaded_presets["presets"][name] = data
loaded_presets["__last_used__"] = name
save_presets_file(loaded_presets)
populate_preset_dropdown_menu()
menuPresets.setCurrentText(name)
preset_load(data)
show_save_status()
def generate_preset_uuid():
label_preset_uuid.setText(str(uuid.uuid4()))
save_preset(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_build=preset_build_snapshot, on_save_as=lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot))
def copy_preset_uuid():
text = label_preset_uuid.text().strip()
if text:
QApplication.clipboard().setText(text)
def switch_preset_by_uuid(uuid_str):
global uuid_presets_matched, uuid_presets_received_values, _track_uuid_check_pending
if not uuid_str.strip():
return
_track_uuid_check_pending = False
if not uuid_presets_received_values:
labelStatus.setText("")
labelStatus.setStyleSheet("color: #00e676; font-style: italic;")
uuid_presets_received_values.append(uuid_str.strip())
for name, preset in loaded_presets["presets"].items():
if preset.get("preset_uuid", "") == uuid_str.strip():
uuid_presets_matched.append(name)
break
uuid_presets_batch_timer.start(200)
def resolve_uuid_batch():
global uuid_presets_matched, uuid_presets_received_values
uuid_presets_matched = list(dict.fromkeys(uuid_presets_matched))
if len(uuid_presets_matched) == 1:
track_name = label_osc_track.text()
label_osc_preset.setText(uuid_presets_matched[0])
label_osc_status.setText(f"DAW Track Selected: {track_name}")
label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
menuPresets.setCurrentText(uuid_presets_matched[0])
ui_show_rows_and_widgets()
elif len(uuid_presets_matched) > 1:
label_osc_status.setText("MULTIPLE PRESET IDs DETECTED")
label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
ui_show_uuid_error_overlay("MULTIPLE PRESET IDs DETECTED", "#ff4444")
QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;")))
else:
label_osc_status.setText("NO MATCHING ID IN PRESET LIST")
label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
ui_show_uuid_error_overlay("NO MATCHING ID IN PRESET LIST", "#ff4444")
QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;")))
uuid_presets_matched.clear()
uuid_presets_received_values.clear()
def check_preset_previous_and_assign():
last = loaded_presets.get("__last_used__")
if last and last in loaded_presets["presets"]:
menuPresets.blockSignals(True)
menuPresets.setCurrentText(last)
menuPresets.blockSignals(False)
preset_load(loaded_presets["presets"][last])
print(f"Restored last used preset: {last}")
def check_midi_port_avail_and_assign(display_name):
global available_ports_midi_in
global midi_in_port
if not display_name or display_name == "Not Connected":
if midi_in_port is not None:
close_midi_input_port(midi_in_port)
midi_in_port = None
label_connection_midi_in.setText("Not Connected")
label_connection_midi_in.setStyleSheet("color: #888;")
save_io_config()
return
port_entry = next((entry for entry in available_ports_midi_in if entry[0] == display_name), None)
if port_entry is None:
return
display_name, original_index, available = port_entry
if not available:
label_connection_midi_in.setText("In use by another program")
label_connection_midi_in.setStyleSheet("color: #e67e22;")
return
if midi_in_port is not None:
close_midi_input_port(midi_in_port)
midi_in_port = None
try:
midi_in_port = open_midi_input_port(original_index, route_midi_message_input)
label_connection_midi_in.setText("Connected")
label_connection_midi_in.setStyleSheet("color: #27ae60;")
print(f"Opened input port: {display_name}")
save_io_config()
except Exception as e:
print(f"Failed to open port {display_name}: {e}")
label_connection_midi_in.setText("Failed to open")
label_connection_midi_in.setStyleSheet("color: #c0392b;")
def check_controller_in_port_avail_and_assign(display_name):
global available_ports_midi_controller_in
global midi_controller_in_port
if not display_name or display_name == "Not Connected":
if midi_controller_in_port is not None:
close_midi_input_port(midi_controller_in_port)
midi_controller_in_port = None
label_connection_midi_controller_in.setText("Not Connected")
label_connection_midi_controller_in.setStyleSheet("color: #888;")
save_io_config()
return
port_entry = next((entry for entry in available_ports_midi_controller_in if entry[0] == display_name), None)
if port_entry is None:
return
display_name, original_index, available = port_entry
if not available:
label_connection_midi_controller_in.setText("In use by another program")
label_connection_midi_controller_in.setStyleSheet("color: #e67e22;")
return
if midi_controller_in_port is not None:
close_midi_input_port(midi_controller_in_port)
midi_controller_in_port = None
try:
midi_controller_in_port = open_midi_input_port(original_index, route_midi_controller_message_input)
label_connection_midi_controller_in.setText("Connected")
label_connection_midi_controller_in.setStyleSheet("color: #27ae60;")
print(f"Opened controller input port: {display_name}")
save_io_config()
except Exception as e:
print(f"Failed to open port {display_name}: {e}")
label_connection_midi_controller_in.setText("Failed to open")
label_connection_midi_controller_in.setStyleSheet("color: #c0392b;")
def _route_hw_cc_bind(status, channel, cc_num, cc_val):
"""Shared CC learn/bind/route pipeline for both MIDI In and MIDI Controller
In — a fader/toggle/transport/preset-browser bind is keyed on CC number +
channel, not on which physical input port it arrived on. Returns True if
the message was consumed (learned, or routed to a bound widget)."""
global learning_strip
# Touch sense (JL Cooper CC): touch on/off arrives on the SAME CC as the
# fader's value data, just on hw_channel - 1 (e.g. value on CH16, touch on
# CH15). Checked first, ahead of the generic CC-only matching below, since
# that matching would otherwise treat a touch message as a real value
# update for any fader with a matching hw_cc. "off" leaves this untouched
# entirely (channel-blind, for non-touch-sensing hardware).
if status == 0xB0:
for fader in faders + focus_faders:
if (getattr(fader, "touch_sense_mode", "off") == "jl_cooper"
and fader.hw_cc == cc_num
and fader.hw_channel is not None
and channel == fader.hw_channel - 1):
fader.is_touching = cc_val >= 64
return True
bound_fader = None
bound_transport = None
bound_browser = None
if status == 0xB0:
for fader in faders:
if fader.hw_cc == cc_num:
bound_fader = fader
break
if not bound_fader:
for toggle in toggles:
if hasattr(toggle, 'hw_cc') and toggle.hw_cc == cc_num:
bound_fader = toggle
break
if not bound_fader:
for t in transports:
if hasattr(t, 'hw_cc') and t.hw_cc == cc_num:
bound_transport = t
break
if not bound_fader and not bound_transport:
for w in preset_browsers:
if w.hw_cc == cc_num:
bound_browser = w
break
# Channel Focus widgets are opt-in via their own "Hardware" checkbox —
# checked last so a regular preset binding on the same CC always wins.
if not bound_fader and not bound_transport and not bound_browser:
for f in focus_faders:
if f.hardware_checkbox.isChecked() and f.hw_cc == cc_num:
bound_fader = f
break
if not bound_fader:
for t in focus_toggles:
if t.hardware_checkbox.isChecked() and t.hw_cc == cc_num:
bound_fader = t
break
if status == 0xB0 and learning_strip:
learning_strip.bind_hw_cc(cc_num, channel=channel)
learning_strip = None
return True
if bound_browser:
bound_browser.on_hw_trigger(cc_val)
return True
elif bound_transport:
print(f"[transport hw] CC{cc_num} val={cc_val} → uid={bound_transport.uid}")
midi_receiver.hw_cc_signal.emit(bound_transport.uid, cc_val)
return True
elif bound_fader:
print(f"[hw] CC{cc_num} val={cc_val} → uid={bound_fader.uid}")
midi_receiver.hw_cc_signal.emit(bound_fader.uid, cc_val)
return True
return False
def route_midi_message_input(event, data=None):
message, _ = event
status = message[0] & 0xF0
channel = (message[0] & 0x0F) + 1
cc_num = message[1] if len(message) >= 2 else None
cc_val = message[2] if len(message) >= 3 else None
handled = _route_hw_cc_bind(status, channel, cc_num, cc_val)
if not handled:
if status in (0x80, 0x90):
if midi_out.is_port_open():
midi_out.send_message(list(message))
midi_receiver.midi_out_signal.emit(list(message))
else:
print(f"[unbound] CC{cc_num} val={cc_val}")
midi_receiver.midi_in_signal.emit(list(message))
def route_midi_controller_message_input(event, data=None):
"""Controller-only input (e.g. JL Cooper fader bank) — same CC learn/bind
pipeline as route_midi_message_input, but Notes are never passed through
to MIDI Out; this port is for controllers only."""
message, _ = event
status = message[0] & 0xF0
channel = (message[0] & 0x0F) + 1
cc_num = message[1] if len(message) >= 2 else None
cc_val = message[2] if len(message) >= 3 else None
handled = _route_hw_cc_bind(status, channel, cc_num, cc_val)
if not handled and status not in (0x80, 0x90):
print(f"[unbound ctrl] CC{cc_num} val={cc_val}")
midi_receiver.controller_in_signal.emit(list(message))
def midi_message_route_hw_to_output_cc(fader_uid, value):
for fader in faders:
if fader.uid == fader_uid:
out_cc = fader.cc_spin.value()
out_ch = fader.ch_spin.value()
print(f"[convert] HW CC{fader.hw_cc} CH{fader.hw_channel} ({value}) → CC{out_cc} CH{out_ch} ({value})")
fader.fader.blockSignals(True)
fader.fader.setValue(value)
fader.fader.blockSignals(False)
fader.cc_input_lbl.setText(fader._cc_input_label(value))
fader.cc_output_lbl.setText(fader._cc_output_label(value))
fader.readout.setText(f"CC {out_cc} → {value}")
fader.last_sent = value
fader.prev_position = value
send_cc(out_cc, value, channel=out_ch)
if ws_server:
ws_server.broadcast_widget_update(fader_uid, value)
return
for toggle in toggles:
if toggle.uid == fader_uid:
out_cc = toggle.cc_spin.value()
out_ch = toggle.ch_spin.value()
print(f"[toggle convert] HW CC{toggle.hw_cc} CH{toggle.hw_channel} ({value}) → CC{out_cc} CH{out_ch}")
toggle.cc_input_lbl.setText(toggle._cc_input_label(value))
if toggle.trigger_mode == "momentary":
send_cc(out_cc, 127, channel=out_ch)
toggle.cc_output_lbl.setText(toggle._cc_output_label(127))
toggle.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
QTimer.singleShot(150, lambda: toggle.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
else:
should_be_on = value > 63
if toggle.toggle_state != should_be_on:
toggle.toggle_state = should_be_on
out_val = 127 if should_be_on else 0
send_cc(out_cc, out_val, channel=out_ch)
toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val))
toggle._update_btn_style()
if ws_server:
ws_server.broadcast_widget_update(fader_uid, out_val)
return
for t in transports:
if t.uid == fader_uid:
t.cc_input_lbl.setText(f"CC{t.hw_cc} [{value}]")
if t.output_mode == "osc":
addr = t.osc_addr_edit.text().strip() or "/play"
send_osc_message(addr, 1.0)
t.cc_output_lbl.setText(f"{addr} [1.0]")
t.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
QTimer.singleShot(150, lambda: t.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
else:
out_cc = t.cc_spin.value()
if t.trigger_mode == "momentary":
send_transport_cc(out_cc, 127)
t.cc_output_lbl.setText(f"CC{out_cc} [127]")
t.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
QTimer.singleShot(150, lambda: t.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
else:
should_be_on = value > 63
if t.toggle_state != should_be_on:
t.toggle_state = should_be_on
out_val = 127 if should_be_on else 0
send_transport_cc(out_cc, out_val)
t.cc_output_lbl.setText(f"CC{out_cc} [{out_val}]")
t._update_btn_style()
return
# Channel Focus fader — hardware drives the same OSC output as the app
# would (see _on_focus_fader_control_f); optional CC feedback (e.g. for a
# motorized fader) goes out via _send_hw_feedback if that's also enabled.
for f in focus_faders:
if f.uid == fader_uid:
addr = f.osc_addr_out_edit.text().strip() or "/track/volume"
f.hw_cc_input_lbl.setText(f._hw_cc_input_label(value))
f.fader.blockSignals(True)
f.fader.setValue(value)
f.fader.blockSignals(False)
f.last_sent = value
f.prev_position = value
f.cc_output_lbl.setText(f._osc_output_label(value))
send_osc_message(addr, value / 127.0)
f._send_hw_feedback(value)
if ws_server:
ws_server.broadcast_widget_update_f(fader_uid, value / 127.0)
return
# Channel Focus toggle — same idea, respecting its own Momentary/Toggle mode.
for t in focus_toggles:
if t.uid == fader_uid:
addr = t.osc_addr_out_edit.text().strip() or "/track/mute"
t.hw_cc_input_lbl.setText(t._hw_cc_input_label(value))
if t.trigger_mode == "momentary":
send_osc_message(addr, 1.0)
t.cc_output_lbl.setText(t._osc_output_label(1.0))
t.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
QTimer.singleShot(150, lambda: t.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
t._send_hw_feedback(127)
if ws_server:
ws_server.broadcast_widget_update(fader_uid, 127)
else:
should_be_on = value > 63
if t.toggle_state != should_be_on:
t.toggle_state = should_be_on
out_val = 1.0 if should_be_on else 0.0
send_osc_message(addr, out_val)
t.cc_output_lbl.setText(t._osc_output_label(out_val))
t._update_btn_style()
t._send_hw_feedback(127 if should_be_on else 0)
if ws_server:
ws_server.broadcast_widget_update(fader_uid, 127 if should_be_on else 0)
return
for f in focus_faders:
if f.uid == fader_uid:
addr = f.osc_addr_out_edit.text().strip() or "/track/volume"
f.fader.blockSignals(True)
f.fader.setValue(value)
f.fader.blockSignals(False)
f.last_sent = value
f.prev_position = value
f.cc_output_lbl.setText(f._osc_output_label(value))
f.readout.setText(f"{addr} → {value}")
send_osc_message(addr, value / 127.0)
if ws_server:
ws_server.broadcast_widget_update(fader_uid, value)
return
for toggle in focus_toggles:
if toggle.uid == fader_uid:
addr = toggle.osc_addr_out_edit.text().strip() or "/track/mute"
if toggle.trigger_mode == "momentary":
send_osc_message(addr, 1.0)
toggle.cc_output_lbl.setText(toggle._osc_output_label(1.0))
toggle.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
QTimer.singleShot(150, lambda: toggle.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
else:
should_be_on = value > 63
if toggle.toggle_state != should_be_on:
toggle.toggle_state = should_be_on
out_val = 1.0 if should_be_on else 0.0
send_osc_message(addr, out_val)
toggle.cc_output_lbl.setText(toggle._osc_output_label(out_val))
toggle._update_btn_style()
if ws_server:
ws_server.broadcast_widget_update(fader_uid, 127 if should_be_on else 0)
return
def log_ui_show_hide(checked):
container_osc_in_log.setVisible(checked)
container_osc_out_log.setVisible(checked)
container_midi_in_log.setVisible(checked)
container_midi_out_log.setVisible(checked)
log_ui_window_calc()
def log_ui_window_calc():
geo = app_window.frameGeometry()
h = geo.height()
half = h // 2 - 2
# Left column: OSC In (top) + OSC Out (bottom)
container_osc_in_log.setFixedWidth(360)
container_osc_in_log.setFixedHeight(half)
container_osc_in_log.move(geo.right() + 4, geo.top())
container_osc_out_log.setFixedWidth(360)
container_osc_out_log.setFixedHeight(half)
container_osc_out_log.move(geo.right() + 4, geo.top() + half + 4)
# Right column: MIDI In (top) + MIDI Out (bottom)
container_midi_in_log.setFixedWidth(360)
container_midi_in_log.setFixedHeight(half)
container_midi_in_log.move(geo.right() + 368, geo.top())
container_midi_out_log.setFixedWidth(360)
container_midi_out_log.setFixedHeight(half)
container_midi_out_log.move(geo.right() + 368, geo.top() + half + 4)
def layout_panel_show_hide(checked):
container_layout_panel.setVisible(checked)
if checked:
layout_panel_window_calc()
def layout_panel_window_calc():
geo = app_window.frameGeometry()
container_layout_panel.setFixedWidth(220)
container_layout_panel.setFixedHeight(geo.height())
container_layout_panel.move(geo.left() - 224, geo.top())
def focus_panel_show_hide(checked):
container_focus_panel.setVisible(checked)
if checked:
focus_panel_window_calc()
def focus_panel_window_calc():
geo = app_window.frameGeometry()
container_focus_panel.setFixedHeight(geo.height())
container_focus_panel.move(geo.left() - 224, geo.top())
def update_focus_panel_width():
k = len(focus_faders)
faders_width = (k * 115) + max(0, k - 1) * 6 if k > 0 else 0
n = len(focus_toggles)
toggles_width = (n * 115) + max(0, n - 1) * 6 if n > 0 else 0
m = len(focus_feedbacks)
feedbacks_width = (m * 115) + max(0, m - 1) * 6 if m > 0 else 0
width = max(220, faders_width + 20, toggles_width + 20, feedbacks_width + 20)
container_focus_panel.setFixedWidth(width)
if container_focus_panel.isVisible():
focus_panel_window_calc()
def tablet_log_show_hide(checked):
container_tablet_log.setVisible(checked)
if checked:
tablet_log_window_calc()
def tablet_log_window_calc():
geo = app_window.frameGeometry()
container_tablet_log.setFixedWidth(360)
container_tablet_log.setFixedHeight(geo.height())
container_tablet_log.move(geo.right() + 732, geo.top())
def ws_log_show_hide(checked):
container_ws_in_log.setVisible(checked)
container_ws_out_log.setVisible(checked)
if checked:
ws_log_window_calc()
def ws_log_window_calc():
geo = app_window.frameGeometry()
half = geo.height() // 2 - 2
container_ws_in_log.setFixedWidth(360)
container_ws_in_log.setFixedHeight(half)
container_ws_in_log.move(geo.right() + 4, geo.top())
container_ws_out_log.setFixedWidth(360)
container_ws_out_log.setFixedHeight(half)
container_ws_out_log.move(geo.right() + 4, geo.top() + half + 4)
def log_tablet(line):
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
full = f"{ts} {line}"
_append_log(textedit_tablet_log, full)
def toggle_tablet_server():
if ws_server is not None:
_stop_tablet_server()
else:
_start_tablet_server()
def log_extension(line):
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
full = f"{ts} {line}"
_append_log(textedit_extension_log, full)
def extension_log_show_hide(checked):
container_extension_log.setVisible(checked)
if checked:
extension_log_window_calc()
def extension_log_window_calc():
geo = app_window.frameGeometry()
container_extension_log.setFixedWidth(360)
container_extension_log.setFixedHeight(geo.height())
container_extension_log.move(geo.right() + 1096, geo.top())
def toggle_extension_connection():
if is_extension_enabled():
set_extension_enabled(False)
button_extension_toggle.setText("Connect Extension")
label_connection_extension.setText("Not Connected")
label_connection_extension.setStyleSheet("color: #888;")
activity_indicator_extension.setStyleSheet("color: #444; font-size: 14px;")
log_extension("disconnected")
save_extension_running_state(False)
else:
ip = textbox_extension_ip.text().strip() or "127.0.0.1"
port = spinner_extension_port.value()
set_extension_target(ip=ip, port=port)
set_extension_enabled(True)
button_extension_toggle.setText("Disconnect Extension")
label_connection_extension.setText(f"Connected :{port}")
label_connection_extension.setStyleSheet("color: #27ae60;")
activity_indicator_extension.setStyleSheet("color: #2ecc71; font-size: 14px;")
log_extension(f"connected {ip}:{port}")
send_to_extension(0.0) # harmless no-op nudge, just proves reachability
save_extension_running_state(True)
def _auto_start_extension():
should_connect = loaded_presets.get("io_config", {}).get("extension_running", True)
if should_connect:
toggle_extension_connection()
def _start_tablet_server():
global ws_server
import socket as _socket
ws_server = WSServer(port=8765, parent=app_window)
ws_server.log_signal.connect(log_tablet)
ws_server.raw_in_signal.connect(log_ws_incoming)
ws_server.control_received.connect(lambda uid, val: midi_receiver.hw_cc_signal.emit(uid, val))
ws_server.control_f_received.connect(_on_focus_fader_control_f)
ws_server.layout_saved.connect(_on_tablet_layout_saved)
ws_server.widget_visibility_received.connect(_on_widget_visibility_from_tablet)
ws_server.client_connected.connect(_on_tablet_connected)
try:
s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
except Exception:
ip = "localhost"
button_tablet_server.setText("Disable Remote")
label_tablet_status.setText(f"ws://{ip}:8765")
label_connection_remote.setText("Listening :8765")
label_connection_remote.setStyleSheet("color: #27ae60;")
activity_indicator_remote.setStyleSheet("color: #2ecc71; font-size: 14px;")
log_tablet(f"WS ws://{ip}:8765")
save_tablet_running_state(True)
if ip != "localhost":
_start_bonjour_advertisement(ip, 8765)
def _auto_start_tablet_server():
should_start = loaded_presets.get("io_config", {}).get("tablet_running", True)
if should_start:
_start_tablet_server()
def _stop_tablet_server():
global ws_server
if ws_server:
ws_server.stop()
ws_server = None
button_tablet_server.setText("Enable Remote")
save_tablet_running_state(False)
label_tablet_status.setText("")
label_connection_remote.setText("Not Listening")
label_connection_remote.setStyleSheet("color: #888;")
activity_indicator_remote.setStyleSheet("color: #444; font-size: 14px;")
log_tablet("server stopped")
_stop_bonjour_advertisement()
def _start_bonjour_advertisement(ip, port):
global _bonjour_zc, _bonjour_info
_stop_bonjour_advertisement()
print(f"[Bonjour] registering {BONJOUR_SERVICE_TYPE} on {ip}:{port}")
try:
hostname = socket.gethostname().split(".")[0]
service_name = f"{hostname}.{BONJOUR_SERVICE_TYPE}"
_bonjour_info = ServiceInfo(
BONJOUR_SERVICE_TYPE,
service_name,
addresses=[socket.inet_aton(ip)],
port=port,
properties={},
)
_bonjour_zc = Zeroconf()
_bonjour_zc.register_service(_bonjour_info)
print(f"[Bonjour] registered as {service_name}")
log_tablet(f"Bonjour advertising as {service_name}")
except Exception as e:
print(f"[Bonjour] registration FAILED: {e}")
log_tablet(f"Bonjour registration failed: {e}")
_bonjour_zc = None
_bonjour_info = None
def _stop_bonjour_advertisement():
global _bonjour_zc, _bonjour_info
if _bonjour_zc:
print(f"[Bonjour] unregistering {_bonjour_info.name if _bonjour_info else '?'}")
try:
if _bonjour_info:
_bonjour_zc.unregister_service(_bonjour_info)
_bonjour_zc.close()
print("[Bonjour] unregistered")
except Exception as e:
print(f"[Bonjour] error while unregistering: {e}")
_bonjour_zc = None
_bonjour_info = None
class OSCLearnDialog(QDialog):
"""Live list of distinct OSC addresses seen since opening, for picking which one to bind."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("OSC Learn")
self.resize(360, 300)
self._seen = {}
self.selected_address = None
layout = QVBoxLayout(self)
hint = QLabel("Trigger the control in the DAW, then pick the message below:")
hint.setWordWrap(True)
layout.addWidget(hint)
self.list_widget = QListWidget()
self.list_widget.itemDoubleClicked.connect(self._on_confirm)
self.list_widget.itemSelectionChanged.connect(
lambda: self.use_btn.setEnabled(bool(self.list_widget.selectedItems()))
)
layout.addWidget(self.list_widget)
btn_row = QHBoxLayout()
self.use_btn = QPushButton("Use Selected")
self.use_btn.setEnabled(False)
self.use_btn.clicked.connect(self._on_confirm)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(self.use_btn)
btn_row.addWidget(cancel_btn)
layout.addLayout(btn_row)
def add_message(self, addr, val):
text = f"{addr} [{val}]"
if addr in self._seen:
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item.data(Qt.ItemDataRole.UserRole) == addr:
item.setText(text)
break
else:
item = QListWidgetItem(text)
item.setData(Qt.ItemDataRole.UserRole, addr)
self.list_widget.addItem(item)
self._seen[addr] = val
def _on_confirm(self):
item = self.list_widget.currentItem()
if not item:
return
self.selected_address = item.data(Qt.ItemDataRole.UserRole)
self.accept()
learning_osc_widget = None
learning_osc_target = "value"
_osc_learn_dialog = None
def _stop_osc_learn_widget(widget, target):
if target == "name":
widget.stop_learn_name()
else:
widget.stop_learn()
def start_osc_learn(widget, target="value"):
global learning_osc_widget, learning_osc_target, _osc_learn_dialog
if learning_osc_widget and learning_osc_widget != widget:
_stop_osc_learn_widget(learning_osc_widget, learning_osc_target)
if _osc_learn_dialog:
_osc_learn_dialog.close()
learning_osc_widget = widget
learning_osc_target = target
if target == "name":
widget.start_learn_name()
else:
widget.start_learn()
_osc_learn_dialog = OSCLearnDialog(app_window)
_osc_learn_dialog.finished.connect(lambda result: _on_osc_learn_dialog_closed(widget, target, result))
_osc_learn_dialog.show()
def _on_osc_learn_dialog_closed(widget, target, result):
global learning_osc_widget, learning_osc_target, _osc_learn_dialog
if result == QDialog.DialogCode.Accepted and _osc_learn_dialog is not None and _osc_learn_dialog.selected_address:
if target == "name":
widget.bind_osc_name_addr(_osc_learn_dialog.selected_address)
else:
widget.bind_osc_addr(_osc_learn_dialog.selected_address)
else:
_stop_osc_learn_widget(widget, target)
learning_osc_widget = None
_osc_learn_dialog = None
def _on_osc_learn_message(addr, val):
if learning_osc_widget and _osc_learn_dialog:
_osc_learn_dialog.add_message(addr, val)
def _on_osc_focus_fader_feedback(addr, val):
for f in focus_faders:
target_addr = f.osc_addr_in_edit.text().strip()
if not target_addr or addr != target_addr:
continue
try:
value = float(val.split()[0])
except (ValueError, IndexError):
continue
f.receive_osc_value(value)
f._send_hw_feedback(f.fader.value())
if ws_server:
# Full-resolution echo — the raw float, not the desktop QSlider's
# quantized 0-127 position, so the iPad fader doesn't lose precision.
ws_server.broadcast_widget_update_f(f.uid, value)
def _on_focus_fader_control_f(uid, value):
"""Full-resolution (0.0-1.0) touch from one of the iPad's Channel Focus faders."""
for f in focus_faders:
if f.uid != uid:
continue
value = max(0.0, min(1.0, value))
slider_value = round(value * 127)
f.fader.blockSignals(True)
f.fader.setValue(slider_value)
f.fader.blockSignals(False)
f.last_sent = slider_value
f.prev_position = slider_value
if f.output_mode == "ext":
f.cc_output_lbl.setText(f"Ext [{slider_value}]")
else:
f.cc_output_lbl.setText(f._osc_output_label(slider_value))
f.readout.setText(f"[{value:.4f}]")
f._emit_value(slider_value) # handles OSC vs Ext, and hw feedback
if ws_server:
ws_server.broadcast_widget_update_f(uid, value)
return
def _on_osc_focus_fader_name_feedback(addr, val):
for f in focus_faders:
target_addr = f.osc_addr_name_edit.text().strip()
if not target_addr or addr != target_addr:
continue
f.receive_osc_name(val)
if ws_server:
ws_server.broadcast_widget_label(f.uid, val)
def _on_osc_focus_toggles_feedback(addr, val):
for toggle in focus_toggles:
target_addr = toggle.osc_addr_in_edit.text().strip()
if not target_addr or addr != target_addr:
continue
try:
value = float(val.split()[0])
except (ValueError, IndexError):
continue
toggle.receive_osc_value(value)
toggle._send_hw_feedback(127 if toggle.toggle_state else 0)
if ws_server:
ws_server.broadcast_widget_update(toggle.uid, 127 if toggle.toggle_state else 0)
def _on_osc_focus_feedbacks_feedback(addr, val):
for widget in focus_feedbacks:
if widget.custom_checkbox.isChecked():
continue
target_addr = widget.osc_addr_in_edit.text().strip()
if not target_addr or addr != target_addr:
continue
widget.receive_osc_value(val)
if ws_server:
ws_server.broadcast_widget_feedback(widget.uid, val)
def start_osc_server(port=9000):
global _osc_server, _osc_thread
stop_osc_server()
d = osc_dispatcher.Dispatcher()
def emit_raw(addr, *args):
val_str = " ".join(str(a) for a in args) if args else ""
osc_receiver.raw_message_signal.emit(addr, val_str)
def on_play(addr, *args):
emit_raw(addr, *args)
if args: osc_receiver.play_signal.emit(int(args[0]))
def on_record(addr, *args):
emit_raw(addr, *args)
if args: osc_receiver.record_signal.emit(int(args[0]))
def on_time(addr, *args):
emit_raw(addr, *args)
if args: osc_receiver.position_signal.emit(str(args[0]))
def on_bar(addr, *args):
emit_raw(addr, *args)
if args: osc_receiver.bar_signal.emit(str(args[0]))
def on_track_name(addr, *args):
emit_raw(addr, *args)
if args: osc_receiver.track_name_signal.emit(str(args[0]))
def on_preset(addr, *args):
emit_raw(addr, *args)
if args: osc_receiver.preset_name_signal.emit(str(args[0]))
def on_default(addr, *args):
emit_raw(addr, *args)
if 'insertname' in addr and args:
osc_receiver.uuid_preset.emit(str(args[0]))
if 'trackname' in addr and args:
osc_receiver.track_name_signal.emit(str(args[0]))
d.map("/play", on_play)
d.map("/record", on_record)
d.map("/*/time", on_time)
d.map("/*/bar", on_bar)
d.map("/track/*", on_track_name)
d.map("/fx/*", on_preset)
d.map("/fxname", on_preset)
d.set_default_handler(on_default)
try:
_osc_server = osc_server.ThreadingOSCUDPServer(("0.0.0.0", port), d)
_osc_thread = threading.Thread(target=_osc_server.serve_forever, daemon=True)
_osc_thread.start()
print(f"[OSC] Listening on port {port}")
return True
except Exception as e:
print(f"[OSC] Failed to start: {e}")
_osc_server = None
return False
def ui_show_rows_and_widgets():
container_preset_browser.setVisible(True)
container_rows_and_widgets.setVisible(True)
view_uuid_error_overlay.setVisible(False)
def ui_show_uuid_error_overlay(message, color):
label_overlay_message.setText(message)
label_overlay_message.setStyleSheet(f"color: {color}; font-size: 18px; font-weight: bold;")
container_preset_browser.setVisible(False)
container_rows_and_widgets.setVisible(False)
view_uuid_error_overlay.setVisible(True)
def stop_osc_server():
global _osc_server, _osc_thread
if _osc_server:
_osc_server.shutdown()
_osc_server = None
_osc_thread = None
def set_app_window(window: QMainWindow):
global app_window
app_window = window
# MARK: FADER SHOW HIDE CONFIG
def fader_show_hide_config_set(enabled):
global fader_show_hide_config
fader_show_hide_config = enabled
for fader in faders:
if enabled:
fader.setFixedHeight(fader.height())
else:
fader.setMinimumHeight(0)
fader.setMaximumHeight(16777215)
fader.learn_btn.setVisible(not enabled)
fader.cc_spin_row.setVisible(not enabled)
fader.ch_spin_row.setVisible(not enabled)
fader.pickup_checkbox.setVisible(not enabled)
fader.grp_remote.setVisible(not enabled)
fader.zone_btn.setVisible(not enabled)
fader.plus_btn.setVisible(not enabled)
fader.minus_btn.setVisible(not enabled)
def fader_show_hide_config_toggle():
global fader_show_hide_config
enabled = not fader_show_hide_config
fader_show_hide_config_set(enabled)
button_fader_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters")
button_fader_show_hide_config.setChecked(enabled)
# MARK: TRANSPORT SHOW HIDE CONFIG
def transport_show_hide_config_set(enabled):
global transport_show_hide_config
transport_show_hide_config = enabled
loaded_presets["transport_show_hide"] = enabled
save_presets_file(loaded_presets)
button_transport_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters")
button_transport_show_hide_config.setChecked(enabled)
for t in transports:
t.grp_learn.setVisible(not enabled)
t.arrow_lbl.setVisible(not enabled)
t.grp_cc.setVisible(not enabled)
t.grp_remote.setVisible(not enabled)
t.zone_btn.setVisible(not enabled)
t.minus_btn.setVisible(not enabled)
t.plus_btn.setVisible(not enabled)
t.title_edit.setVisible(not enabled)
t.id_sep.setVisible(not enabled)
t.id_label.setVisible(not enabled)
t.color_swatch_btn.setVisible(not enabled)
def transport_show_hide_config_toggle():
global transport_show_hide_config
transport_show_hide_config_set(not transport_show_hide_config)
# MARK: TOGGLE SHOW HIDE CONFIG
def toggle_show_hide_config_set(enabled):
global toggle_show_hide_config
toggle_show_hide_config = enabled
for t in toggles:
t.grp_learn.setVisible(not enabled)
t.arrow_lbl.setVisible(not enabled)
t.grp_cc.setVisible(not enabled)
t.grp_remote.setVisible(not enabled)
t.zone_btn.setVisible(not enabled)
t.minus_btn.setVisible(not enabled)
t.plus_btn.setVisible(not enabled)
t.id_label.setVisible(not enabled)
t.color_swatch_btn.setVisible(not enabled)
def toggle_show_hide_config_toggle():
global toggle_show_hide_config
enabled = not toggle_show_hide_config
toggle_show_hide_config_set(enabled)
button_toggle_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters")
button_toggle_show_hide_config.setChecked(enabled)
# MARK: PRESET BROWSER SHOW HIDE CONFIG
def preset_browser_show_hide_config_set(enabled):
global preset_browser_show_hide_config
preset_browser_show_hide_config = enabled
for w in preset_browsers:
w.grp_learn.setVisible(not enabled)
w.arrow_lbl.setVisible(not enabled)
w.grp_cc.setVisible(not enabled)
w.title_edit.setVisible(not enabled)
w.momentary_btn.setVisible(not enabled)
w.toggle_mode_btn.setVisible(not enabled)
w.btn.setFixedHeight(24 if enabled else 40)
w.container.setStyleSheet(
"QFrame#presetContainer { background: transparent; border: none; }"
if enabled else
"QFrame#presetContainer { background-color: #4a5568; border-radius: 6px; border: 2px solid rgba(0,0,0,0.3); }"
)
def preset_browser_show_hide_config_toggle():
global preset_browser_show_hide_config
enabled = not preset_browser_show_hide_config
preset_browser_show_hide_config_set(enabled)
button_preset_browser_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters")
button_preset_browser_show_hide_config.setChecked(enabled)
# VS Code dark theme palette
dark_palette = app.palette()
from PyQt6.QtGui import QPalette
from PyQt6.QtCore import Qt
dark_palette.setColor(QPalette.ColorRole.Window, QColor('#1e1e1e'))
dark_palette.setColor(QPalette.ColorRole.WindowText, QColor('#d4d4d4'))
dark_palette.setColor(QPalette.ColorRole.Base, QColor('#252526'))
dark_palette.setColor(QPalette.ColorRole.AlternateBase, QColor('#1e1e1e'))
dark_palette.setColor(QPalette.ColorRole.ToolTipBase, QColor('#1e1e1e'))
dark_palette.setColor(QPalette.ColorRole.ToolTipText, QColor('#d4d4d4'))
dark_palette.setColor(QPalette.ColorRole.Text, QColor('#d4d4d4'))
dark_palette.setColor(QPalette.ColorRole.Button, QColor('#3c3c3c'))
dark_palette.setColor(QPalette.ColorRole.ButtonText, QColor('#d4d4d4'))
dark_palette.setColor(QPalette.ColorRole.BrightText, QColor('#ffffff'))
dark_palette.setColor(QPalette.ColorRole.Highlight, QColor('#264f78'))
dark_palette.setColor(QPalette.ColorRole.HighlightedText, QColor('#ffffff'))
app.setPalette(dark_palette)
window = MainWindow()
set_window(window)
window.show()
layout_panel_window_calc()
sys.exit(app.exec())