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 ) 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 togglewidget import ToggleWidget from faderwidget import FaderWidget 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 ws_server import WSServer from http_server import start_http_server, stop_http_server def set_window(w): global _window _window = w _osc_server = None _osc_thread = None ws_server = None _http_server = 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 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)) #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;") # 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: 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("Start Tablet") button_tablet_server.setFixedWidth(90) # Widget: button_tablet_log button_tablet_log = QPushButton("Tablet") button_tablet_log.setFixedWidth(65) button_tablet_log.setCheckable(True) # Widget: label_tablet_status label_tablet_status = QLabel("") label_tablet_status.setStyleSheet("color: #8e24aa; font-size: 11px; font-family: monospace;") # Widget: button_osc_messages_log button_osc_messages_log = QPushButton("Messages") button_osc_messages_log.setFixedWidth(80) 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) # 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_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; } """) # 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: 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) 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(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 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 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.addSpacing(16) hstack_osc_buttons.addWidget(button_tablet_server) hstack_osc_buttons.addWidget(button_tablet_log) hstack_osc_buttons.addWidget(label_tablet_status) hstack_osc_buttons.addStretch() # add all rows to 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 # connect signals textbox_osc_daw_ip.textChanged.connect(update_osc_server_ip) spinner_osc_send_port.valueChanged.connect(update_osc_server_port) 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) # add the combined stack to the row hstack_io_row.addLayout(vstack_osc) # add the combined stack to the row hstack_io_row.addLayout(vstack_osc) 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: 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) #MARK: LAYOUT PANEL container_layout_panel.setVisible(True) button_layout_panel.setChecked(True) 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() 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) # 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.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, start_stop_osc_server) 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_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_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 == 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 == 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 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 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) 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) 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() 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)) 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 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 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)) params_visible = not transport_show_hide_config for attr in ['grp_learn', 'arrow_lbl', 'grp_cc', '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 save_io_config(): loaded_presets["io_config"] = { "midi_out": picker_midi_out.currentText(), "midi_in": picker_midi_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(), } save_presets_file(loaded_presets) def load_io_config(): global available_ports_midi_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") 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) def preset_build_snapshot(name): save_transport_preset() 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, "faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "visible": True, "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, "visible": True, "color": PALETTE_HEX.get(t.color_name, "#00e676")} for t in toggles], "transports": [{"uid": t.uid, "label": t.title_edit.text(), "visible": True} for t in transports], } return snapshot, saved_layout def _on_tablet_connected(): if ws_server: snapshot, layout = _build_tablet_snapshot() ws_server.broadcast_preset(snapshot, layout) 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): 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: snapshot, layout = _build_tablet_snapshot() ws_server.broadcast_preset(snapshot, layout) 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;") 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 route_midi_message_input(event, data=None): global learning_strip 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 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 if status == 0xB0 and learning_strip: learning_strip.bind_hw_cc(cc_num, channel=channel) learning_strip = None return if bound_browser: bound_browser.on_hw_trigger(cc_val) 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) elif bound_fader: print(f"[hw] CC{cc_num} val={cc_val} → uid={bound_fader.uid} out_cc={bound_fader.cc_spin.value()}") midi_receiver.hw_cc_signal.emit(bound_fader.uid, cc_val) elif 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 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 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 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 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 _start_tablet_server(): global ws_server, _http_server import socket as _socket ws_server = WSServer(port=8765, parent=app_window) ws_server.log_signal.connect(log_tablet) ws_server.control_received.connect(lambda uid, val: midi_receiver.hw_cc_signal.emit(uid, val)) ws_server.layout_saved.connect(_on_tablet_layout_saved) ws_server.client_connected.connect(_on_tablet_connected) _http_server = start_http_server(port=8080) 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("Stop Tablet") label_tablet_status.setText(f"http://{ip}:8080") log_tablet(f"HTTP http://{ip}:8080") log_tablet(f"WS ws://{ip}:8765") def _stop_tablet_server(): global ws_server, _http_server if ws_server: ws_server.stop() ws_server = None if _http_server: stop_http_server(_http_server) _http_server = None button_tablet_server.setText("Start Tablet") label_tablet_status.setText("") log_tablet("server stopped") 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.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.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.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())