2500 lines
99 KiB
Python
2500 lines
99 KiB
Python
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,
|
|
)
|
|
|
|
def set_window(w):
|
|
global _window
|
|
_window = w
|
|
|
|
_osc_server = None
|
|
|
|
_osc_thread = None
|
|
|
|
|
|
|
|
|
|
|
|
#MARK: GLOBALS
|
|
ready = False
|
|
selected_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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#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())
|
|
|
|
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_osc_messages_log
|
|
button_osc_messages_log = QPushButton("Messages")
|
|
button_osc_messages_log.setFixedWidth(80)
|
|
button_osc_messages_log.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;
|
|
}
|
|
""")
|
|
|
|
# MARK: PRESET BROWSER ROW MODULE
|
|
label_preset_browser_title = QLabel("Preset 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_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(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.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)
|
|
|
|
# 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)
|
|
|
|
main_layout.addWidget(make_separator())
|
|
|
|
# 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)
|
|
main_layout.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)
|
|
|
|
main_layout.addLayout(hstack_full_transport)
|
|
|
|
#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.addWidget(label_preset_browser_title)
|
|
vstack_preset_browser_container.addLayout(hstack_preset_browser_controls)
|
|
vstack_preset_browser_container.addLayout(hstack_preset_browser)
|
|
main_layout.addWidget(container_preset_browser)
|
|
|
|
|
|
# MARK: CONTAINER ROWS AND WIDGETS (fader + toggle only)
|
|
global container_rows_and_widgets
|
|
global view_uuid_error_overlay
|
|
global label_overlay_message
|
|
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)
|
|
|
|
controls_layout.addWidget(make_separator())
|
|
|
|
# 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)
|
|
controls_layout.addLayout(hstack_row_fader_control_buttons)
|
|
|
|
#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)
|
|
|
|
controls_layout.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)
|
|
controls_layout.addWidget(placeholder_fader_empty)
|
|
|
|
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
|
|
|
|
# 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)
|
|
controls_layout.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)
|
|
|
|
controls_layout.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)
|
|
controls_layout.addWidget(placeholder_toggle_empty)
|
|
|
|
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()
|
|
|
|
def moveEvent(self, event):
|
|
super().moveEvent(event)
|
|
if container_osc_in_log.isVisible():
|
|
log_ui_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": {}}
|
|
loaded_presets["__last_used__"] = name
|
|
save_presets_file(loaded_presets)
|
|
populate_preset_dropdown_menu()
|
|
menuPresets.setCurrentText(name)
|
|
clear_all_strips()
|
|
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()
|
|
|
|
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]}]"
|
|
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 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)
|
|
|
|
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}")
|
|
|
|
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;")
|
|
|
|
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;")
|
|
|
|
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 check_and_update_selected_strip(strip):
|
|
global selected_strip
|
|
if selected_strip and selected_strip != strip:
|
|
selected_strip.set_selected(False)
|
|
selected_strip = strip
|
|
strip.set_selected(True)
|
|
label_selected_channel.setText(f"Software item selected: {strip.title_edit.text()}")
|
|
|
|
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_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):
|
|
global selected_strip
|
|
if len(toggles) <= 0:
|
|
return
|
|
if selected_strip == toggle:
|
|
selected_strip = None
|
|
label_selected_channel.setText("Software item selected: None")
|
|
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_zone_change_callback = handle_fader_zone_change
|
|
update_window_width()
|
|
check_and_index_strip_labels()
|
|
_update_fader_placeholder()
|
|
|
|
def fader_remove(fader):
|
|
global selected_strip
|
|
if len(faders) <= 0:
|
|
return
|
|
if selected_strip == fader:
|
|
selected_strip = None
|
|
label_selected_channel.setText("Software item selected: None")
|
|
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.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):
|
|
global selected_strip
|
|
if len(transports) <= 1:
|
|
return
|
|
if selected_strip == t:
|
|
selected_strip = None
|
|
label_selected_channel.setText("Software item selected: None")
|
|
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(),
|
|
},
|
|
"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,
|
|
}
|
|
}
|
|
|
|
|
|
def preset_load(preset):
|
|
fader_states = preset.get("faders", [])
|
|
while len(faders) < len(fader_states):
|
|
fader_add()
|
|
while len(faders) > len(fader_states):
|
|
widget = faders.pop()
|
|
hstack_fader_center.removeWidget(widget)
|
|
widget.deleteLater()
|
|
update_window_width()
|
|
_update_fader_placeholder()
|
|
for i, widget in enumerate(faders):
|
|
widget.set_state(fader_states[i])
|
|
|
|
center_faders = sorted(
|
|
[f for f in faders if not f.zone],
|
|
key=lambda f: f.center_index if f.center_index is not None else 999
|
|
)
|
|
for f in center_faders:
|
|
hstack_fader_center.removeWidget(f)
|
|
f.setParent(None)
|
|
for f in center_faders:
|
|
hstack_fader_center.addWidget(f)
|
|
|
|
left_faders = sorted(
|
|
[f for f in faders if f.zone == "left"],
|
|
key=lambda f: f.zone_index if f.zone_index is not None else 999
|
|
)
|
|
right_faders = sorted(
|
|
[f for f in faders if f.zone == "right"],
|
|
key=lambda f: f.zone_index if f.zone_index is not None else 999
|
|
)
|
|
for f in left_faders:
|
|
hstack_fader_center.removeWidget(f)
|
|
f.setParent(None)
|
|
hstack_fader_zone_left.addWidget(f)
|
|
for f in right_faders:
|
|
hstack_fader_center.removeWidget(f)
|
|
f.setParent(None)
|
|
hstack_fader_zone_right.addWidget(f)
|
|
|
|
update_window_width()
|
|
|
|
if "toggles" in preset:
|
|
toggle_states = preset["toggles"]
|
|
elif "toggle" in preset:
|
|
toggle_states = [preset["toggle"]]
|
|
else:
|
|
toggle_states = []
|
|
|
|
while len(toggles) < len(toggle_states):
|
|
toggle_add()
|
|
while len(toggles) > len(toggle_states):
|
|
widget = toggles.pop()
|
|
hstack_toggle_center.removeWidget(widget)
|
|
widget.deleteLater()
|
|
update_window_width()
|
|
_update_toggle_placeholder()
|
|
for i, widget in enumerate(toggles):
|
|
widget.set_state(toggle_states[i])
|
|
|
|
center_toggles = sorted(
|
|
[t for t in toggles if not t.zone],
|
|
key=lambda t: t.center_index if t.center_index is not None else 999
|
|
)
|
|
for t in center_toggles:
|
|
hstack_toggle_center.removeWidget(t)
|
|
t.setParent(None)
|
|
for t in center_toggles:
|
|
hstack_toggle_center.addWidget(t)
|
|
|
|
left_toggles = sorted(
|
|
[t for t in toggles if t.zone == "left"],
|
|
key=lambda t: t.zone_index if t.zone_index is not None else 999
|
|
)
|
|
right_toggles = sorted(
|
|
[t for t in toggles if t.zone == "right"],
|
|
key=lambda t: t.zone_index if t.zone_index is not None else 999
|
|
)
|
|
for t in left_toggles:
|
|
hstack_toggle_center.removeWidget(t)
|
|
t.setParent(None)
|
|
hstack_toggle_zone_left.addWidget(t)
|
|
for t in right_toggles:
|
|
hstack_toggle_center.removeWidget(t)
|
|
t.setParent(None)
|
|
hstack_toggle_zone_right.addWidget(t)
|
|
|
|
browser = preset.get("browser", {})
|
|
# migrate old format that only stored cc numbers
|
|
if "back" not in browser and "back_cc" in browser:
|
|
browser = {
|
|
"back": {"label": "Back", "cc": browser.get("back_cc", 22), "ch": 1},
|
|
"fwd": {"label": "Fwd", "cc": browser.get("fwd_cc", 23), "ch": 1},
|
|
}
|
|
preset_browser_back.set_state(browser.get("back", {"label": "Back", "cc": 22, "ch": 1}))
|
|
preset_browser_fwd.set_state(browser.get("fwd", {"label": "Fwd", "cc": 23, "ch": 1}))
|
|
|
|
show_hide = preset.get("show_hide", {})
|
|
fader_show_hide_config_set(show_hide.get("fader", False))
|
|
button_fader_show_hide_config.setText("Show Parameters" if show_hide.get("fader", False) else "Hide Parameters")
|
|
button_fader_show_hide_config.setChecked(show_hide.get("fader", False))
|
|
|
|
toggle_show_hide_config_set(show_hide.get("toggle", False))
|
|
button_toggle_show_hide_config.setText("Show Parameters" if show_hide.get("toggle", False) else "Hide Parameters")
|
|
button_toggle_show_hide_config.setChecked(show_hide.get("toggle", False))
|
|
|
|
preset_browser_show_hide_config_set(show_hide.get("browser", False))
|
|
button_preset_browser_show_hide_config.setText("Show Parameters" if show_hide.get("browser", False) else "Hide Parameters")
|
|
button_preset_browser_show_hide_config.setChecked(show_hide.get("browser", False))
|
|
|
|
label_preset_uuid.setText(preset.get("preset_uuid", ""))
|
|
check_and_index_strip_labels()
|
|
|
|
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)
|
|
else:
|
|
print(f"[passthrough] CC{cc_num} val={cc_val}")
|
|
if midi_out.is_port_open():
|
|
midi_out.send_message(message)
|
|
|
|
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)
|
|
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} ({value})")
|
|
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_input_lbl.setText(toggle._cc_input_label(value))
|
|
toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val))
|
|
toggle._update_btn_style()
|
|
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":
|
|
if value > 0:
|
|
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 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()
|
|
sys.exit(app.exec()) |