Files
virtual-controller/app/main.py
T
Paul Lipscomb 7a74cfa75e iOS remote client: native UIKit surface, WS logging, client-side layout persistence
- Add remote-client-ios: full native UIKit app with Objects/View/UILayout/Functions pattern
- Widget hierarchy: BaseWidget → Fader, Toggle, Transport, Title, Feedback, LogWidget
- LoggingVC: child VC embedded in LogWidget for live WS message inspection
- Codable model: ModelPresetLoadToRemoteDevice with snake_case decoder strategy
- Layout persistence: UserDefaults keyed by preset_uuid, fully client-side
- WS logging: dedicated WS In/Out floating panels on desktop app
- Remove HTTP server and JS client (replaced by native iOS app)
- Add Xcode noise to .gitignore (xcuserstate, xcuserdata)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 00:02:10 -04:00

3086 lines
122 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
def set_window(w):
global _window
_window = w
_osc_server = None
_osc_thread = None
ws_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: button_ws_log
button_ws_log = QPushButton("WS")
button_ws_log.setFixedWidth(50)
button_ws_log.setCheckable(True)
# Widget: label_tablet_status
label_tablet_status = QLabel("")
label_tablet_status.setStyleSheet("color: #8e24aa; font-size: 11px; font-family: monospace;")
# 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_ws_in_log
container_ws_in_log = QWidget(None, Qt.WindowType.Window)
container_ws_in_log.setWindowTitle("WS In — Incoming from Tablet")
container_ws_in_log.setStyleSheet("background-color: #1a1a1a;")
container_ws_in_log.setFixedWidth(420)
vstack_ws_in_log = QVBoxLayout(container_ws_in_log)
vstack_ws_in_log.setContentsMargins(6, 6, 6, 6)
vstack_ws_in_log.setSpacing(4)
hstack_ws_in_log_header = QHBoxLayout()
label_ws_in_log_title = QLabel("WS In — Incoming from Tablet")
label_ws_in_log_title.setStyleSheet("color: #00acc1; font-weight: bold; font-size: 11px;")
button_ws_in_clear = QPushButton("Clear")
button_ws_in_clear.setFixedSize(45, 18)
textedit_ws_in_log = QTextEdit()
textedit_ws_in_log.setReadOnly(True)
textedit_ws_in_log.setStyleSheet("""
QTextEdit {
background-color: #111111;
color: #00e5ff;
font-family: monospace;
font-size: 10px;
border: none;
}
""")
# Container: container_ws_out_log
container_ws_out_log = QWidget(None, Qt.WindowType.Window)
container_ws_out_log.setWindowTitle("WS Out — Outgoing to Tablet")
container_ws_out_log.setStyleSheet("background-color: #1a1a1a;")
container_ws_out_log.setFixedWidth(420)
vstack_ws_out_log = QVBoxLayout(container_ws_out_log)
vstack_ws_out_log.setContentsMargins(6, 6, 6, 6)
vstack_ws_out_log.setSpacing(4)
hstack_ws_out_log_header = QHBoxLayout()
label_ws_out_log_title = QLabel("WS Out — Outgoing to Tablet")
label_ws_out_log_title.setStyleSheet("color: #ff6f00; font-weight: bold; font-size: 11px;")
button_ws_out_clear = QPushButton("Clear")
button_ws_out_clear.setFixedSize(45, 18)
textedit_ws_out_log = QTextEdit()
textedit_ws_out_log.setReadOnly(True)
textedit_ws_out_log.setStyleSheet("""
QTextEdit {
background-color: #111111;
color: #ffb300;
font-family: monospace;
font-size: 10px;
border: none;
}
""")
# Container: container_tablet_log
container_tablet_log = QWidget(None, Qt.WindowType.Window)
container_tablet_log.setWindowTitle("Tablet Server")
container_tablet_log.setStyleSheet("background-color: #1a1a1a;")
container_tablet_log.setFixedWidth(360)
vstack_tablet_log = QVBoxLayout(container_tablet_log)
vstack_tablet_log.setContentsMargins(6, 6, 6, 6)
vstack_tablet_log.setSpacing(4)
hstack_tablet_log_header = QHBoxLayout()
label_tablet_log_title = QLabel("Tablet Server")
label_tablet_log_title.setStyleSheet("color: #8e24aa; font-weight: bold; font-size: 11px;")
button_tablet_log_clear = QPushButton("Clear")
button_tablet_log_clear.setFixedSize(45, 18)
textedit_tablet_log = QTextEdit()
textedit_tablet_log.setReadOnly(True)
textedit_tablet_log.setStyleSheet("""
QTextEdit {
background-color: #111111;
color: #8e24aa;
font-family: monospace;
font-size: 10px;
border: none;
}
""")
# 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(button_ws_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: WS IN LOG CONTAINER
hstack_ws_in_log_header.addWidget(label_ws_in_log_title)
hstack_ws_in_log_header.addStretch()
hstack_ws_in_log_header.addWidget(button_ws_in_clear)
vstack_ws_in_log.addLayout(hstack_ws_in_log_header)
vstack_ws_in_log.addWidget(textedit_ws_in_log)
button_ws_in_clear.clicked.connect(lambda: textedit_ws_in_log.clear())
container_ws_in_log.setVisible(False)
#MARK: WS OUT LOG CONTAINER
hstack_ws_out_log_header.addWidget(label_ws_out_log_title)
hstack_ws_out_log_header.addStretch()
hstack_ws_out_log_header.addWidget(button_ws_out_clear)
vstack_ws_out_log.addLayout(hstack_ws_out_log_header)
vstack_ws_out_log.addWidget(textedit_ws_out_log)
button_ws_out_clear.clicked.connect(lambda: textedit_ws_out_log.clear())
container_ws_out_log.setVisible(False)
#MARK: TABLET LOG CONTAINER
hstack_tablet_log_header.addWidget(label_tablet_log_title)
hstack_tablet_log_header.addStretch()
hstack_tablet_log_header.addWidget(button_tablet_log_clear)
vstack_tablet_log.addLayout(hstack_tablet_log_header)
vstack_tablet_log.addWidget(textedit_tablet_log)
button_tablet_log_clear.clicked.connect(lambda: textedit_tablet_log.clear())
container_tablet_log.setVisible(False)
button_tablet_server.clicked.connect(toggle_tablet_server)
button_tablet_log.clicked.connect(tablet_log_show_hide)
button_ws_log.clicked.connect(ws_log_show_hide)
#MARK: 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"<span style='color:#555'>{ts}</span> <span style='color:#ff8c00'>{_midi_msg_str(msg)}</span>"
_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"<span style='color:#555'>{ts}</span> <span style='color:#00bcd4'>{_midi_msg_str(msg)}</span>"
_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"<span style='color:#555'>{ts}</span> <span style='color:#00e676'>{addr}</span> <span style='color:#d4d4d4'>{val}</span>"
_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"<span style='color:#555'>{ts}</span> <span style='color:#1e88e5'>{addr}</span> <span style='color:#d4d4d4'>{val}</span>"
_append_log(textedit_osc_out_log, line)
def log_ws_incoming(message):
import json
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
try:
obj = json.loads(message)
pretty = json.dumps(obj, indent=2)
except Exception:
pretty = message
header = f"<span style='color:#555'>{ts}</span> <span style='color:#00acc1'>◀ WS In</span>"
body = f"<pre style='color:#00e5ff; margin:0; font-size:10px'>{pretty}</pre>"
textedit_ws_in_log.append(header)
textedit_ws_in_log.append(body)
textedit_ws_in_log.append("<span style='color:#333'>───────────────────</span>")
def log_ws_outgoing(data):
import json
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
pretty = json.dumps(data, indent=2)
header = f"<span style='color:#555'>{ts}</span> <span style='color:#ff6f00'>▶ WS Out</span>"
body = f"<pre style='color:#ffb300; margin:0; font-size:10px'>{pretty}</pre>"
textedit_ws_out_log.append(header)
textedit_ws_out_log.append(body)
textedit_ws_out_log.append("<span style='color:#333'>───────────────────</span>")
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 (116):", 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,
"big_title": big_title_edit.text() if big_title_edit else "",
"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)
log_ws_outgoing(snapshot)
def _on_tablet_layout_saved(preset_uuid, layout):
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
if loaded_presets["presets"][name].get("preset_uuid") == preset_uuid:
loaded_presets["presets"][name]["tablet_layout"] = layout
save_presets_file(loaded_presets)
def preset_load(preset):
global selected_strips
selected_strips = []
fader_states = preset.get("faders", [])
while len(faders) < len(fader_states):
fader_add()
while len(faders) > len(fader_states):
widget = faders.pop()
hstack_fader_center.removeWidget(widget)
widget.deleteLater()
update_window_width()
_update_fader_placeholder()
for i, widget in enumerate(faders):
widget.set_state(fader_states[i])
center_faders = sorted(
[f for f in faders if not f.zone],
key=lambda f: f.center_index if f.center_index is not None else 999
)
for f in center_faders:
hstack_fader_center.removeWidget(f)
f.setParent(None)
for f in center_faders:
hstack_fader_center.addWidget(f)
left_faders = sorted(
[f for f in faders if f.zone == "left"],
key=lambda f: f.zone_index if f.zone_index is not None else 999
)
right_faders = sorted(
[f for f in faders if f.zone == "right"],
key=lambda f: f.zone_index if f.zone_index is not None else 999
)
for f in left_faders:
hstack_fader_center.removeWidget(f)
f.setParent(None)
hstack_fader_zone_left.addWidget(f)
for f in right_faders:
hstack_fader_center.removeWidget(f)
f.setParent(None)
hstack_fader_zone_right.addWidget(f)
update_window_width()
if "toggles" in preset:
toggle_states = preset["toggles"]
elif "toggle" in preset:
toggle_states = [preset["toggle"]]
else:
toggle_states = []
while len(toggles) < len(toggle_states):
toggle_add()
while len(toggles) > len(toggle_states):
widget = toggles.pop()
hstack_toggle_center.removeWidget(widget)
widget.deleteLater()
update_window_width()
_update_toggle_placeholder()
for i, widget in enumerate(toggles):
widget.set_state(toggle_states[i])
center_toggles = sorted(
[t for t in toggles if not t.zone],
key=lambda t: t.center_index if t.center_index is not None else 999
)
for t in center_toggles:
hstack_toggle_center.removeWidget(t)
t.setParent(None)
for t in center_toggles:
hstack_toggle_center.addWidget(t)
left_toggles = sorted(
[t for t in toggles if t.zone == "left"],
key=lambda t: t.zone_index if t.zone_index is not None else 999
)
right_toggles = sorted(
[t for t in toggles if t.zone == "right"],
key=lambda t: t.zone_index if t.zone_index is not None else 999
)
for t in left_toggles:
hstack_toggle_center.removeWidget(t)
t.setParent(None)
hstack_toggle_zone_left.addWidget(t)
for t in right_toggles:
hstack_toggle_center.removeWidget(t)
t.setParent(None)
hstack_toggle_zone_right.addWidget(t)
browser = preset.get("browser", {})
# migrate old format that only stored cc numbers
if "back" not in browser and "back_cc" in browser:
browser = {
"back": {"label": "Back", "cc": browser.get("back_cc", 22), "ch": 1},
"fwd": {"label": "Fwd", "cc": browser.get("fwd_cc", 23), "ch": 1},
}
preset_browser_back.set_state(browser.get("back", {"label": "Back", "cc": 22, "ch": 1}))
preset_browser_fwd.set_state(browser.get("fwd", {"label": "Fwd", "cc": 23, "ch": 1}))
show_hide = preset.get("show_hide", {})
fader_show_hide_config_set(show_hide.get("fader", False))
button_fader_show_hide_config.setText("Show Parameters" if show_hide.get("fader", False) else "Hide Parameters")
button_fader_show_hide_config.setChecked(show_hide.get("fader", False))
toggle_show_hide_config_set(show_hide.get("toggle", False))
button_toggle_show_hide_config.setText("Show Parameters" if show_hide.get("toggle", False) else "Hide Parameters")
button_toggle_show_hide_config.setChecked(show_hide.get("toggle", False))
preset_browser_show_hide_config_set(show_hide.get("browser", False))
button_preset_browser_show_hide_config.setText("Show Parameters" if show_hide.get("browser", False) else "Hide Parameters")
button_preset_browser_show_hide_config.setChecked(show_hide.get("browser", False))
big_title_edit.setText(preset.get("big_title", ""))
label_preset_uuid.setText(preset.get("preset_uuid", ""))
sections = preset.get("sections", {})
chk_section_preset_browser.setChecked(sections.get("preset_browser", True))
chk_section_big_title.setChecked(sections.get("big_title", True))
chk_section_fader_row.setChecked(sections.get("fader_row", True))
chk_section_toggle_row.setChecked(sections.get("toggle_row", True))
chk_section_transport_row.setChecked(sections.get("transport_row", True))
check_and_index_strip_labels()
if ws_server:
snapshot, layout = _build_tablet_snapshot()
ws_server.broadcast_preset(snapshot, layout)
log_ws_outgoing(snapshot)
def export_preset():
import json as _json
from PyQt6.QtWidgets import QFileDialog, QMessageBox
msg = QMessageBox(app_window)
msg.setWindowTitle("Export")
msg.setText("Export current preset or all presets?")
msg.setIcon(QMessageBox.Icon.NoIcon)
btn_current = msg.addButton("Current", QMessageBox.ButtonRole.AcceptRole)
btn_all = msg.addButton("All", QMessageBox.ButtonRole.AcceptRole)
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
msg.exec()
clicked = msg.clickedButton()
if clicked is btn_current:
name = menuPresets.currentText()
if not name or name not in loaded_presets["presets"]:
return
default_path = os.path.join(os.path.expanduser("~"), f"{name}.json")
path, _ = QFileDialog.getSaveFileName(app_window, "Export Preset", default_path, "JSON Files (*.json)")
if not path:
return
with open(path, "w") as f:
_json.dump(loaded_presets["presets"][name], f, indent=2)
show_save_status()
elif clicked is btn_all:
default_path = os.path.join(os.path.expanduser("~"), "presets.json")
path, _ = QFileDialog.getSaveFileName(app_window, "Export All Presets", default_path, "JSON Files (*.json)")
if not path:
return
with open(path, "w") as f:
_json.dump(loaded_presets["presets"], f, indent=2)
show_save_status()
def import_preset():
import json as _json
from PyQt6.QtWidgets import QFileDialog, QInputDialog
path, _ = QFileDialog.getOpenFileName(app_window, "Import Preset", "", "JSON Files (*.json)")
if not path:
return
with open(path, "r") as f:
data = _json.load(f)
default_name = os.path.splitext(os.path.basename(path))[0]
name, ok = QInputDialog.getText(app_window, "Import Preset", "Preset name:", text=default_name)
if not ok or not name.strip():
return
name = name.strip()
loaded_presets["presets"][name] = data
loaded_presets["__last_used__"] = name
save_presets_file(loaded_presets)
populate_preset_dropdown_menu()
menuPresets.setCurrentText(name)
preset_load(data)
show_save_status()
def generate_preset_uuid():
label_preset_uuid.setText(str(uuid.uuid4()))
save_preset(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_build=preset_build_snapshot, on_save_as=lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot))
def copy_preset_uuid():
text = label_preset_uuid.text().strip()
if text:
QApplication.clipboard().setText(text)
def switch_preset_by_uuid(uuid_str):
global uuid_presets_matched, uuid_presets_received_values, _track_uuid_check_pending
if not uuid_str.strip():
return
_track_uuid_check_pending = False
if not uuid_presets_received_values:
labelStatus.setText("")
labelStatus.setStyleSheet("color: #00e676; font-style: italic;")
uuid_presets_received_values.append(uuid_str.strip())
for name, preset in loaded_presets["presets"].items():
if preset.get("preset_uuid", "") == uuid_str.strip():
uuid_presets_matched.append(name)
break
uuid_presets_batch_timer.start(200)
def resolve_uuid_batch():
global uuid_presets_matched, uuid_presets_received_values
uuid_presets_matched = list(dict.fromkeys(uuid_presets_matched))
if len(uuid_presets_matched) == 1:
track_name = label_osc_track.text()
label_osc_preset.setText(uuid_presets_matched[0])
label_osc_status.setText(f"DAW Track Selected: {track_name}")
label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
menuPresets.setCurrentText(uuid_presets_matched[0])
ui_show_rows_and_widgets()
elif len(uuid_presets_matched) > 1:
label_osc_status.setText("MULTIPLE PRESET IDs DETECTED")
label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
ui_show_uuid_error_overlay("MULTIPLE PRESET IDs DETECTED", "#ff4444")
QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;")))
else:
label_osc_status.setText("NO MATCHING ID IN PRESET LIST")
label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;")
if not preset_switch_locked:
ui_show_uuid_error_overlay("NO MATCHING ID IN PRESET LIST", "#ff4444")
QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;")))
uuid_presets_matched.clear()
uuid_presets_received_values.clear()
def check_preset_previous_and_assign():
last = loaded_presets.get("__last_used__")
if last and last in loaded_presets["presets"]:
menuPresets.blockSignals(True)
menuPresets.setCurrentText(last)
menuPresets.blockSignals(False)
preset_load(loaded_presets["presets"][last])
print(f"Restored last used preset: {last}")
def check_midi_port_avail_and_assign(display_name):
global available_ports_midi_in
global midi_in_port
if not display_name or display_name == "Not Connected":
if midi_in_port is not None:
close_midi_input_port(midi_in_port)
midi_in_port = None
label_connection_midi_in.setText("Not Connected")
label_connection_midi_in.setStyleSheet("color: #888;")
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 ws_log_show_hide(checked):
container_ws_in_log.setVisible(checked)
container_ws_out_log.setVisible(checked)
if checked:
ws_log_window_calc()
def ws_log_window_calc():
geo = app_window.frameGeometry()
half = geo.height() // 2 - 2
container_ws_in_log.setFixedWidth(360)
container_ws_in_log.setFixedHeight(half)
container_ws_in_log.move(geo.right() + 4, geo.top())
container_ws_out_log.setFixedWidth(360)
container_ws_out_log.setFixedHeight(half)
container_ws_out_log.move(geo.right() + 4, geo.top() + half + 4)
def log_tablet(line):
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
full = f"<span style='color:#555'>{ts}</span> <span style='color:#8e24aa'>{line}</span>"
_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
import socket as _socket
ws_server = WSServer(port=8765, parent=app_window)
ws_server.log_signal.connect(log_tablet)
ws_server.raw_in_signal.connect(log_ws_incoming)
ws_server.control_received.connect(lambda uid, val: midi_receiver.hw_cc_signal.emit(uid, val))
ws_server.layout_saved.connect(_on_tablet_layout_saved)
ws_server.client_connected.connect(_on_tablet_connected)
try:
s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
except Exception:
ip = "localhost"
button_tablet_server.setText("Stop Tablet")
label_tablet_status.setText(f"ws://{ip}:8765")
log_tablet(f"WS ws://{ip}:8765")
def _stop_tablet_server():
global ws_server
if ws_server:
ws_server.stop()
ws_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())