diff --git a/faderwidget.py b/faderwidget.py
index 14f9e35..3abc882 100644
--- a/faderwidget.py
+++ b/faderwidget.py
@@ -22,10 +22,12 @@ from midi_sender import send_cc
class FaderWidget(QWidget):
def __init__(self, label, default_cc):
+
super().__init__()
- self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
- self.setFixedWidth(100)
- self.current_color = "transparent"
+
+ self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Maximum)
+ self.setFixedWidth(115)
+ self.current_color = PALETTE_HEX.get("Gray", "#263238")
self.uid = str(uuid.uuid4())
@@ -48,41 +50,96 @@ class FaderWidget(QWidget):
inner.setSpacing(4)
inner.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- # Learn button
+ # Learn / CC / CH group
self.hw_cc = None
+ self.hw_channel = None
self.is_learning = False
+
+ _grp_style = "QFrame { border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }"
+
+ # Group 1: Learn + input readout (border always visible; learn_btn hideable)
+ self.grp_learn = QFrame()
+ self.grp_learn.setStyleSheet(_grp_style)
+ _grp1_layout = QVBoxLayout(self.grp_learn)
+ _grp1_layout.setContentsMargins(4, 4, 4, 4)
+ _grp1_layout.setSpacing(3)
+
self.learn_btn = QPushButton("Learn")
self.learn_btn.setFixedHeight(20)
+ self.learn_btn.setStyleSheet("border: none;")
self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu)
- inner.addWidget(self.learn_btn)
+ _grp1_layout.addWidget(self.learn_btn)
- # CC In label
self.cc_input_lbl = QLabel("")
self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
- inner.addWidget(self.cc_input_lbl)
+ self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;")
+ self.cc_input_lbl.setFixedHeight(16)
+ _grp1_layout.addWidget(self.cc_input_lbl)
+
+ inner.addWidget(self.grp_learn)
+
+ _arrow_lbl = QLabel("▼")
+ _arrow_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
+ _arrow_lbl.setStyleSheet("color: rgba(255,255,255,0.3); font-size: 9px; background: transparent;")
+ _arrow_lbl.setFixedHeight(12)
+ inner.addWidget(_arrow_lbl)
+
+ # Group 2: CC spin + CH spin + output readout (border always visible; spins hideable)
+ self.grp_cc = QFrame()
+ self.grp_cc.setStyleSheet(_grp_style)
+ _grp2_layout = QVBoxLayout(self.grp_cc)
+ _grp2_layout.setContentsMargins(4, 4, 4, 4)
+ _grp2_layout.setSpacing(3)
- # CC spin
self.cc_spin = QSpinBox()
- self.cc_spin.setMinimum(1)
+ self.cc_spin.setMinimum(0)
self.cc_spin.setMaximum(127)
self.cc_spin.setValue(default_cc)
- self.cc_spin.setFixedWidth(75)
- inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
+ self.cc_spin.setFixedWidth(55)
+ _cc_lbl = QLabel("CC")
+ _cc_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;")
+ self.cc_spin_row = QWidget()
+ _hbox_cc = QHBoxLayout(self.cc_spin_row)
+ _hbox_cc.setContentsMargins(0, 0, 0, 0)
+ _hbox_cc.setSpacing(4)
+ _hbox_cc.addWidget(_cc_lbl)
+ _hbox_cc.addWidget(self.cc_spin)
+ _grp2_layout.addWidget(self.cc_spin_row)
- self.cc_output_lbl = QLabel("")
+ self.ch_spin = QSpinBox()
+ self.ch_spin.setMinimum(1)
+ self.ch_spin.setMaximum(16)
+ self.ch_spin.setValue(1)
+ self.ch_spin.setFixedWidth(55)
+ _ch_lbl = QLabel("CH")
+ _ch_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;")
+ self.ch_spin_row = QWidget()
+ _hbox_ch = QHBoxLayout(self.ch_spin_row)
+ _hbox_ch.setContentsMargins(0, 0, 0, 0)
+ _hbox_ch.setSpacing(4)
+ _hbox_ch.addWidget(_ch_lbl)
+ _hbox_ch.addWidget(self.ch_spin)
+ _grp2_layout.addWidget(self.ch_spin_row)
+
+ self.cc_output_lbl = QLabel(f"CC{default_cc} [--]")
self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
- inner.addWidget(self.cc_output_lbl)
+ self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;")
+ self.cc_output_lbl.setFixedHeight(16)
+ _grp2_layout.addWidget(self.cc_output_lbl)
+
+ inner.addWidget(self.grp_cc)
+
+
# Fader
self.fader = QSlider(Qt.Orientation.Vertical)
self.fader.setMinimum(0)
self.fader.setMaximum(127)
self.fader.setValue(64)
- self.fader.setFixedHeight(200)
+ self.fader.setMinimumHeight(200)
self.fader.setFixedWidth(40)
+ self.fader.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
self.fader.setStyleSheet(FADER_STYLE)
inner.addWidget(self.fader, alignment=Qt.AlignmentFlag.AlignHCenter)
@@ -214,7 +271,7 @@ class FaderWidget(QWidget):
def on_fader_moved(self, value):
cc_num = self.cc_spin.value()
- self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
+ self.cc_output_lbl.setText(self._cc_output_label(value))
if self.pickup_mode and self.hunting:
crossed = (
(self.prev_position < self.last_sent <= value) or
@@ -226,21 +283,21 @@ class FaderWidget(QWidget):
self._stop_blink()
self._set_readout_style("orange")
self.last_sent = value
- send_cc(cc_num, value)
+ send_cc(cc_num, value, channel=self.ch_spin.value())
else:
self.readout.setText(f"CC {cc_num} → {value}")
return
else:
self.last_sent = value
self.prev_position = value
- send_cc(cc_num, value)
+ send_cc(cc_num, value, channel=self.ch_spin.value())
self.readout.setText(f"CC {cc_num} → {value}")
def on_cc_changed(self):
cc_num = self.cc_spin.value()
value = self.fader.value()
self.readout.setText(f"CC {cc_num} → {value}")
- self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
+ self.cc_output_lbl.setText(self._cc_output_label(value))
if self.pickup_mode:
self.hunting = True
self._start_blink()
@@ -307,6 +364,7 @@ class FaderWidget(QWidget):
action = menu.exec(self.learn_btn.mapToGlobal(pos))
if action == clear_action:
self.hw_cc = None
+ self.hw_channel = None
self.is_learning = False
self.learn_btn.setText("Learn")
self.learn_btn.setStyleSheet("")
@@ -326,20 +384,32 @@ class FaderWidget(QWidget):
self.learn_btn.setText("Listening...")
self.learn_btn.setStyleSheet("color: orange; font-weight: bold;")
+ def _cc_input_label(self, vel="--"):
+ if self.hw_cc is None:
+ return ""
+ ch = f" CH{self.hw_channel}" if self.hw_channel is not None else ""
+ return f"CC{self.hw_cc}{ch} [{vel}]"
+
+ def _cc_output_label(self, vel="--"):
+ out_cc = self.cc_spin.value()
+ out_ch = self.ch_spin.value()
+ return f"CC{out_cc} CH{out_ch} [{vel}]"
+
def stop_learn(self):
self.is_learning = False
self.learn_btn.setStyleSheet("")
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
- self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
+ self.cc_input_lbl.setText(self._cc_input_label())
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
- def bind_hw_cc(self, cc_number):
+ def bind_hw_cc(self, cc_number, channel=None):
self.hw_cc = cc_number
+ self.hw_channel = channel
self.stop_learn()
def get_state(self):
@@ -347,11 +417,13 @@ class FaderWidget(QWidget):
"uid": self.uid,
"label": self.title_edit.text(),
"cc": self.cc_spin.value(),
+ "ch": self.ch_spin.value(),
"value": self.fader.value(),
"color": self.color_name,
"pickup": self.pickup_checkbox.isChecked(),
"last_sent": self.last_sent,
"hw_cc": self.hw_cc,
+ "hw_channel": self.hw_channel,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index
@@ -362,6 +434,7 @@ class FaderWidget(QWidget):
self.uid = state["uid"]
self.title_edit.setText(state.get("label", ""))
self.cc_spin.setValue(state.get("cc", 1))
+ self.ch_spin.setValue(state.get("ch", 1))
self.last_sent = state.get("last_sent", 64)
self.fader.setValue(state.get("value", 64))
color_name = state.get("color", "Gray")
@@ -379,9 +452,10 @@ class FaderWidget(QWidget):
self.zone_index = state.get("zone_index", None)
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
self.hw_cc = state.get("hw_cc", None)
+ self.hw_channel = state.get("hw_channel", None)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
- self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
+ self.cc_input_lbl.setText(self._cc_input_label())
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
diff --git a/main.py b/main.py
index 120b5fa..ee76e5c 100644
--- a/main.py
+++ b/main.py
@@ -2,6 +2,7 @@ import sys
import json
import os
import uuid
+import socket
import rtmidi
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
@@ -73,71 +74,11 @@ def set_window(w):
global _window
_window = w
-
_osc_server = None
+
_osc_thread = None
-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_tempo(addr, *args):
- emit_raw(addr, *args)
- if args: osc_receiver.tempo_signal.emit(float(args[0]))
- def on_beat(addr, *args):
- emit_raw(addr, *args)
- if args: osc_receiver.position_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)
-
- d.map("/play", on_play)
- d.map("/record", on_record)
- d.map("/tempo", on_tempo)
- d.map("/beat/str", on_beat)
- 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 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
@@ -151,6 +92,30 @@ 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
+
+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)
@@ -165,33 +130,62 @@ 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))
+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))
+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))
+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))
+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())
#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
@@ -207,20 +201,20 @@ 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;")
+# 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_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 Input")
-label_midi_in.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;")
+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;")
+# picker_midi_in.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;")
# REFACTOR MAP: Old names → New names
@@ -237,20 +231,20 @@ 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;")
+# 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_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 Output")
-label_midi_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;")
+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;")
+# 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
@@ -266,20 +260,20 @@ 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;")
+# 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_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("Transport Output")
-label_transport_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;")
+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;")
+# 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
@@ -343,6 +337,14 @@ 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
@@ -378,14 +380,14 @@ label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: b
label_osc_record_state = QLabel("● REC")
label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
-# Widget: label_osc_tempo
-label_osc_tempo = QLabel("BPM: —")
-label_osc_tempo.setStyleSheet("color: #888; font-size: 11px;")
-
# Widget: label_osc_position
-label_osc_position = QLabel("Pos: —")
+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)
@@ -416,7 +418,7 @@ spacer_osc_info = QWidget()
spacer_osc_info.setFixedWidth(16)
# Container: container_osc_in_log
-container_osc_in_log = QWidget(None, Qt.WindowType.Tool)
+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)
@@ -451,7 +453,7 @@ textedit_osc_in_log.setStyleSheet("""
""")
# Container: container_osc_out_log
-container_osc_out_log = QWidget(None, Qt.WindowType.Tool)
+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)
@@ -562,6 +564,18 @@ vsep_transport_right.setVisible(False)
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
@@ -570,8 +584,8 @@ hstack_transport_zone_right.setSpacing(0)
hstack_selected_channel_and_save_status = QHBoxLayout()
# Widget: label_selected_channel
-label_selected_channel = QLabel("Selected: None")
-label_selected_channel.setStyleSheet("color: #00ff88; font-weight: bold;")
+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
@@ -652,7 +666,52 @@ separator_vertical_toggle_zone_right.setVisible(False)
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_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
@@ -666,6 +725,8 @@ class MainWindow(QMainWindow):
global buttonSave
global buttonSaveAs
global buttonRemove
+ global buttonExport
+ global buttonImport
global labelStatus
global hstack_io_row
global available_ports_midi_in
@@ -689,13 +750,21 @@ class MainWindow(QMainWindow):
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(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(self._separator())
+ main_layout.addWidget(make_separator())
#MARK: MIDI IN COLUMN 1
#build the hstack with activity indicator and connection state label
@@ -713,6 +782,7 @@ class MainWindow(QMainWindow):
# 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:
@@ -720,7 +790,7 @@ class MainWindow(QMainWindow):
picker_midi_in.model().item(item_index).setEnabled(False)
picker_midi_in.model().item(item_index).setForeground(QColor('#888888'))
- picker_midi_in.currentTextChanged.connect(self.on_input_port_select)
+ picker_midi_in.currentTextChanged.connect(check_midi_port_avail_and_assign)
#MARK: MIDI OUT COLUMN 2
@@ -743,8 +813,9 @@ class MainWindow(QMainWindow):
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(self.on_port_select)
+ picker_midi_out.currentTextChanged.connect(set_port_midi_output)
# add the combined stack to the row
hstack_io_row.addLayout(vstack_midi_out)
@@ -769,8 +840,9 @@ class MainWindow(QMainWindow):
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(self.on_transport_output_select)
+ picker_transport_out.currentTextChanged.connect(set_port_transport_output)
# add the combined stack to the row
hstack_io_row.addLayout(vstack_transport_out)
@@ -820,10 +892,10 @@ class MainWindow(QMainWindow):
vstack_osc.addLayout(hstack_osc_buttons) # control buttons
# connect signals
- textbox_osc_daw_ip.textChanged.connect(self.on_osc_ip_changed)
- spinner_osc_send_port.valueChanged.connect(self.on_osc_send_port_changed)
- button_osc_toggle.clicked.connect(self.toggle_osc)
- button_osc_messages_log.clicked.connect(self.toggle_osc_log)
+ 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)
@@ -833,7 +905,7 @@ class MainWindow(QMainWindow):
hstack_io_row.addStretch()
main_layout.addLayout(hstack_io_row)
- main_layout.addWidget(self._separator())
+ main_layout.addWidget(make_separator())
# MARK: OSC STATUS ROW
@@ -842,8 +914,8 @@ class MainWindow(QMainWindow):
global hstack_osc_transport_state
global label_osc_play_state
global label_osc_record_state
- global label_osc_tempo
global label_osc_position
+ global label_osc_bar
global divider_osc_feedback
global hstack_osc_info
global label_track_header
@@ -869,8 +941,8 @@ class MainWindow(QMainWindow):
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_tempo)
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)
@@ -880,10 +952,15 @@ class MainWindow(QMainWindow):
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)
@@ -918,38 +995,19 @@ class MainWindow(QMainWindow):
container_osc_out_log.setVisible(False)
- #MARK: PRESET BROWSER
- # Globals for mutation safety
- global label_preset_browser_title
- global hstack_preset_browser
- global button_preset_browser_back
- global spinner_preset_browser_back
- global button_preset_browser_fwd
- global spinner_preset_browser_fwd
- global spacer_preset_browser
+ main_layout.addWidget(make_separator())
- # assemble browser controls
- hstack_preset_browser.addWidget(button_preset_browser_back)
- hstack_preset_browser.addWidget(spinner_preset_browser_back)
- hstack_preset_browser.addWidget(spacer_preset_browser)
- hstack_preset_browser.addWidget(button_preset_browser_fwd)
- hstack_preset_browser.addWidget(spinner_preset_browser_fwd)
- hstack_preset_browser.addStretch()
+ # MARK: TRANSPORT ROW CONTROLS
+ global hstack_row_transport_control_buttons
+ global button_transport_show_hide_config
- # add to main layout
- main_layout.addWidget(label_preset_browser_title)
- main_layout.addLayout(hstack_preset_browser)
-
- # connect signals
- button_preset_browser_back.clicked.connect(lambda: send_cc(spinner_preset_browser_back.value(), 127))
- button_preset_browser_fwd.clicked.connect(lambda: send_cc(spinner_preset_browser_fwd.value(), 127))
-
- main_layout.addWidget(self._separator())
+ 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
- # Instance state
- self.transports = []
- self.transport_strip_width = 100
+
self._capturing_key_for = None
# Globals for mutation safety
@@ -960,32 +1018,67 @@ class MainWindow(QMainWindow):
global vsep_transport_right
global hstack_transport_zone_right
- #MARK: TRANSPORT ROW
# assemble full transport row
- hstack_full_transport.addLayout(hstack_transport_zone_left) # left zone slot
- hstack_full_transport.addWidget(vsep_transport_left) # left separator
- hstack_full_transport.addLayout(hstack_transport_center) # center controls
- hstack_full_transport.addWidget(vsep_transport_right) # right separator
- hstack_full_transport.addLayout(hstack_transport_zone_right) # right zone slot
+ 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)
- # add to main layout
main_layout.addLayout(hstack_full_transport)
- # MARK: SELECTED CHANNEL ROW
- global hstack_selected_channel_and_save_status
- global label_selected_channel
- global labelStatus
+ #MARK: PRESET BROWSER CONTAINER
+ global container_preset_browser
+ global label_preset_browser_title
+ global hstack_preset_browser
+ global button_preset_browser_back
+ global spinner_preset_browser_back
+ global button_preset_browser_fwd
+ global spinner_preset_browser_fwd
+ global spacer_preset_browser
- #MARK: SELECTED CHANNEL AND SAVE STATUS ROW
- # assemble info row
- hstack_selected_channel_and_save_status.addStretch()
- hstack_selected_channel_and_save_status.addWidget(label_selected_channel)
- hstack_selected_channel_and_save_status.addStretch()
- labelStatus.setParent(None) # reparent status label
- hstack_selected_channel_and_save_status.addWidget(labelStatus)
+ container_preset_browser = QWidget()
+ vstack_preset_browser_container = QVBoxLayout(container_preset_browser)
+ vstack_preset_browser_container.setContentsMargins(0, 0, 0, 0)
- # add to main layout
- main_layout.addLayout(hstack_selected_channel_and_save_status)
+ # assemble browser controls
+ hstack_preset_browser.addWidget(button_preset_browser_back)
+ hstack_preset_browser.addWidget(spinner_preset_browser_back)
+ hstack_preset_browser.addWidget(spacer_preset_browser)
+ hstack_preset_browser.addWidget(button_preset_browser_fwd)
+ hstack_preset_browser.addWidget(spinner_preset_browser_fwd)
+ hstack_preset_browser.addStretch(1)
+
+ vstack_preset_browser_container.addWidget(make_separator())
+ vstack_preset_browser_container.addWidget(label_preset_browser_title)
+ vstack_preset_browser_container.addLayout(hstack_preset_browser)
+ main_layout.addWidget(container_preset_browser)
+
+ # connect signals
+ button_preset_browser_back.clicked.connect(lambda: send_cc(spinner_preset_browser_back.value(), 127))
+ button_preset_browser_fwd.clicked.connect(lambda: send_cc(spinner_preset_browser_fwd.value(), 127))
+
+ # 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
@@ -996,19 +1089,31 @@ class MainWindow(QMainWindow):
global hstack_fader_zone_right
# assemble full fader row
- hstack_full_fader.addLayout(hstack_fader_zone_left) # left zone slot
- hstack_full_fader.addWidget(separator_vertical_fader_zone_left) # left separator
- hstack_full_fader.addLayout(hstack_fader_center) # center faders
- hstack_full_fader.addWidget(separator_vertical_fader_zone_right) # right separator
- hstack_full_fader.addLayout(hstack_fader_zone_right) # right zone slot
+ 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)
- # add to main layout
- main_layout.addLayout(hstack_full_fader)
- main_layout.addWidget(self._separator())
+ controls_layout.addLayout(hstack_full_fader)
- #MARK: TOGGLE ROW
- self.toggles = []
- self.toggle_strip_width = 100
+ 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
@@ -1018,16 +1123,54 @@ class MainWindow(QMainWindow):
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) # left zone slot
- hstack_full_toggle.addWidget(separator_vertical_toggle_zone_left) # left separator
- hstack_full_toggle.addLayout(hstack_toggle_center) # center toggles
- hstack_full_toggle.addWidget(separator_vertical_toggle_zone_right) # right separator
- hstack_full_toggle.addLayout(hstack_toggle_zone_right) # right zone slot
+ 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)
- # add to main layout
- main_layout.addLayout(hstack_full_toggle)
+ 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
@@ -1035,820 +1178,1160 @@ class MainWindow(QMainWindow):
loaded_presets = ensure_preset_structure(loaded_presets)
- self.add_fader()
- self.add_fader()
- self.add_toggle()
- for name, cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]:
- self.add_transport(name, cc)
+ # 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
- self.refresh_preset_combo()
- self.restore_last_used()
- self.refresh_all_id_labels()
- self.refresh_port_availability()
+ 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: self.blink(activity_indicator_midi_in))
- midi_receiver.midi_out_signal.connect(lambda: self.blink(activity_indicator_midi_out))
- midi_receiver.transport_out_signal.connect(lambda: self.blink(activity_indicator_transport_out))
- midi_receiver.hw_cc_signal.connect(self.on_hw_cc)
+ midi_receiver.midi_in_signal.connect(lambda msg: make_activity_blink(activity_indicator_midi_in))
+ midi_receiver.midi_out_signal.connect(lambda: make_activity_blink(activity_indicator_midi_out))
+ 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(self.on_osc_track)
- osc_receiver.preset_name_signal.connect(self.on_osc_preset)
- osc_receiver.tempo_signal.connect(self.on_osc_tempo)
- osc_receiver.position_signal.connect(self.on_osc_position)
- osc_receiver.play_signal.connect(self.on_osc_play)
- osc_receiver.record_signal.connect(self.on_osc_record)
- osc_receiver.raw_message_signal.connect(self.on_osc_raw)
- osc_receiver.osc_out_signal.connect(self.on_osc_out_raw)
-
-
-
- def refresh_all_id_labels(self):
- # Center faders
-
- global faders
-
- center = [f for f in faders if not f.zone]
- for i, f in enumerate(center):
- f.update_id_label(None, i)
- # Left zone faders
- 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 zone faders
- 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)
- # Center toggles
- t_center = [t for t in self.toggles if not t.zone]
- for i, t in enumerate(t_center):
- t.update_id_label(None, i)
- # Left zone toggles
- t_left = sorted([t for t in self.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)
- # Right zone toggles
- t_right = sorted([t for t in self.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)
- # Transports
- tr_center = [t for t in self.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 self.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 self.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)
-
- def on_toggle_zone_change(self, toggle):
- if toggle.zone is not None and toggle.center_index is None:
- if toggle in self.toggles:
- toggle.center_index = self.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 self.toggles))
- separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in self.toggles))
- has_left = any(t.zone == "left" for t in self.toggles)
- has_right = any(t.zone == "right" for t in self.toggles)
-
- self.update_window_width()
- self.refresh_all_id_labels()
-
- def on_fader_zone_change(self, fader):
- # Save center index before removing from center row
-
- global faders
-
- if fader.zone is not None and fader.center_index is None:
- # Being zoned for first time — save current center position
- if fader in faders:
- fader.center_index = faders.index(fader)
-
- # Remove by UUID from whichever slot it's in
- 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 # subtract placeholder
- 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))
- has_left = any(f.zone == "left" for f in faders)
- has_right = any(f.zone == "right" for f in faders)
-
- self.update_window_width()
-
- self.refresh_all_id_labels()
-
- def select_strip(self, 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"Selected: {strip.title_edit.text()}")
-
- def add_toggle(self, insert_after=None):
- n = len(self.toggles) + 1
- toggle = ToggleWidget(f"Toggle {n}", default_cc=20)
- if insert_after is not None and insert_after in self.toggles:
- idx = self.toggles.index(insert_after)
- self.toggles.insert(idx + 1, toggle)
- hstack_toggle_center.insertWidget(idx + 1, toggle)
- else:
- self.toggles.append(toggle)
- hstack_toggle_center.addWidget(toggle)
- toggle.plus_btn.clicked.connect(lambda: self.add_toggle(insert_after=toggle))
- toggle.minus_btn.clicked.connect(lambda: self.remove_toggle(toggle))
- toggle.on_select_callback = self.select_strip
- toggle.on_zone_change_callback = self.on_toggle_zone_change
- toggle.learn_btn.clicked.connect(lambda: self.start_learn(toggle))
- self.update_window_width()
- self.refresh_all_id_labels()
-
- def remove_toggle(self, toggle):
- global selected_strip
- if len(self.toggles) <= 1:
- return
- if selected_strip == toggle:
- selected_strip = None
- label_selected_channel.setText("Selected: None")
- hstack_toggle_center.removeWidget(toggle)
- toggle.deleteLater()
- self.toggles.remove(toggle)
- self.update_window_width()
- self.refresh_all_id_labels()
-
- def add_fader(self, insert_after=None):
-
- global faders
-
- 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: self.add_fader(insert_after=fader))
- fader.minus_btn.clicked.connect(lambda: self.remove_fader(fader))
- fader.learn_btn.clicked.connect(lambda: self.start_learn(fader))
- fader.on_select_callback = self.select_strip
- fader.on_zone_change_callback = self.on_fader_zone_change
- self.update_window_width()
- self.refresh_all_id_labels()
-
- def remove_fader(self, fader):
-
- global selected_strip
- global faders
- global spacing
-
- if len(faders) <= 1:
- return
- if selected_strip == fader:
- selected_strip = None
- label_selected_channel.setText("Selected: None")
- hstack_fader_center.removeWidget(fader)
- fader.deleteLater()
- faders.remove(fader)
- self.update_window_width()
- self.refresh_all_id_labels()
-
- def update_window_width(self):
-
- global faders
- global strip_width
-
- nf = len(faders)
- nt = len(self.toggles)
- fader_w = (nf * strip_width) + ((nf - 1) * spacing) + padding
- toggle_w = (nt * self.toggle_strip_width) + ((nt - 1) * spacing) + padding
- w = max(fader_w, toggle_w, 300)
- self.resize(w, self.height())
-
- def _separator(self):
- sep = QWidget()
- sep.setFixedHeight(1)
- sep.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- sep.setStyleSheet("background-color: #3a3a3a;")
- return sep
-
- def refresh_preset_combo(self):
-
- global loaded_presets
- global menuPresets
-
- menuPresets.blockSignals(True)
- menuPresets.clear()
- menuPresets.addItems(sorted(loaded_presets["presets"].keys()))
- menuPresets.blockSignals(False)
-
- def restore_last_used(self):
-
- global loaded_presets
- global menuPresets
-
- last = loaded_presets.get("__last_used__")
- if last and last in loaded_presets["presets"]:
- menuPresets.blockSignals(True)
- menuPresets.setCurrentText(last)
- menuPresets.blockSignals(False)
- self.apply_preset(loaded_presets["presets"][last])
- print(f"Restored last used preset: {last}")
-
-
-
- def _show_save_status(self):
-
- 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 _build_preset_data(self, name):
-
- global faders
-
- return {
- "midi_port": picker_midi_out.currentText(),
- "midi_input": picker_midi_in.currentText(),
- "transport_port": picker_transport_out.currentText(),
- "faders": [f.get_state() for f in faders],
- "toggles": [t.get_state() for t in self.toggles],
- "transports": [t.get_state() for t in self.transports],
- "browser": {
- "back_cc": spinner_preset_browser_back.value(),
- "fwd_cc": spinner_preset_browser_fwd.value()
- },
- "osc": {
- "ip": textbox_osc_daw_ip.text(),
- "listen_port": spinner_osc_listen_port.value(),
- "send_port": spinner_osc_send_port.value(),
- }
- }
-
-
- def apply_preset(self, preset):
-
- global faders
- global available_ports_midi_in
-
- port_name = preset.get("midi_port", "")
- if port_name in available_ports:
- picker_midi_out.setCurrentText(port_name)
-
- transport_port = preset.get("transport_port", "")
- if transport_port in available_ports:
- picker_transport_out.setCurrentText(transport_port)
-
- midi_input = preset.get("midi_input", "")
- input_names = [entry[0] for entry in available_ports_midi_in]
- if midi_input in input_names:
- picker_midi_in.setCurrentText(midi_input)
- fader_states = preset.get("faders", [])
- while len(faders) < len(fader_states):
- self.add_fader()
- while len(faders) > len(fader_states):
- widget = faders.pop()
- hstack_fader_center.removeWidget(widget)
- widget.deleteLater()
- self.update_window_width()
- for i, widget in enumerate(faders):
- widget.set_state(fader_states[i])
-
- # First place all center faders in their saved positions
- 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)
-
- # Then place zoned faders in zone order
- 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)
-
- self.update_window_width()
- # Migration: old presets have 'toggle' (singular), new have 'toggles' (list)
- if "toggles" in preset:
- toggle_states = preset["toggles"]
- elif "toggle" in preset:
- toggle_states = [preset["toggle"]]
- else:
- toggle_states = []
-
- if not toggle_states:
- if len(self.toggles) == 0:
- self.add_toggle()
- else:
- while len(self.toggles) < len(toggle_states):
- self.add_toggle()
- while len(self.toggles) > len(toggle_states):
- widget = self.toggles.pop()
- hstack_toggle_center.removeWidget(widget)
- widget.deleteLater()
- self.update_window_width()
- for i, widget in enumerate(self.toggles):
- widget.set_state(toggle_states[i])
-
- # Place center toggles
- center_toggles = sorted(
- [t for t in self.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)
-
- # Place zoned toggles
- left_toggles = sorted(
- [t for t in self.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 self.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", {})
- spinner_preset_browser_back.setValue(browser.get("back_cc", 22))
- spinner_preset_browser_fwd.setValue(browser.get("fwd_cc", 23))
-
- transport_states = preset.get("transports", [])
- for i, t in enumerate(self.transports):
- if i < len(transport_states):
- t.set_state(transport_states[i])
-
- osc = preset.get("osc", {})
- if osc:
- textbox_osc_daw_ip.setText(osc.get("ip", "127.0.0.1"))
- spinner_osc_listen_port.setValue(osc.get("listen_port", 9000))
- spinner_osc_send_port.setValue(osc.get("send_port", 8000))
-
-
-
-
-
-
- def on_input_port_select(self, display_name):
-
- global available_ports_midi_in
-
- if not display_name:
- 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 hasattr(self, "midi_in"):
- close_midi_input_port(self.midi_in)
- del self.midi_in
- try:
-
- self.midi_in = open_midi_input_port(original_index, self.on_midi_in)
- label_connection_midi_in.setText("Connected")
- label_connection_midi_in.setStyleSheet("color: #27ae60;")
- print(f"Opened input port: {display_name}")
- 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 on_hw_cc(self, fader_uid, value):
-
- global faders
-
- for fader in faders:
- if fader.uid == fader_uid:
- out_cc = fader.cc_spin.value()
- print(f"[convert] HW CC{fader.hw_cc} ({value}) → CC{out_cc} ({value})")
- fader.fader.blockSignals(True)
- fader.fader.setValue(value)
- fader.fader.blockSignals(False)
- fader.cc_input_lbl.setText(f"In CC{fader.hw_cc} [{value}]")
- fader.cc_output_lbl.setText(f"Out CC{out_cc} [{value}]")
- fader.readout.setText(f"CC {out_cc} → {value}")
- fader.last_sent = value
- fader.prev_position = value
- send_cc(out_cc, value)
- return
- for t in self.transports:
- if t.uid == fader_uid:
- out_cc = t.cc_spin.value()
- 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_input_lbl.setText(f"In CC{t.hw_cc} [{value}]")
- t.cc_output_lbl.setText(f"Out CC{out_cc} [{out_val}]")
- t._update_btn_style()
- return
- should_be_on = value > 63
- if toggle.toggle_state != should_be_on:
- toggle.toggle_state = should_be_on
- out_cc = toggle.cc_spin.value()
- out_val = 127 if should_be_on else 0
- send_cc(out_cc, out_val)
- toggle.cc_input_lbl.setText(f"In CC{toggle.hw_cc} [{value}]")
- toggle.cc_output_lbl.setText(f"Out CC{out_cc} [{out_val}]")
- toggle._update_btn_style()
- return
-
- def start_learn(self, fader):
-
- global learning_strip
-
- if learning_strip and learning_strip != fader:
- learning_strip.stop_learn()
- learning_strip = fader
- fader.start_learn()
-
- def on_midi_in(self, event, data=None):
-
- global learning_strip
- global faders
-
- message, _ = event
- status = message[0] & 0xF0
- cc_num = message[1] if len(message) >= 2 else None
- cc_val = message[2] if len(message) >= 3 else None
-
- # Check if this CC is bound to a fader, toggle, or transport
- bound_fader = None
- bound_transport = 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 self.toggles:
- if hasattr(toggle, 'hw_cc') and toggle.hw_cc == cc_num:
- bound_fader = toggle
- break
- if not bound_fader:
- for t in self.transports:
- if hasattr(t, 'hw_cc') and t.hw_cc == cc_num:
- bound_transport = t
- break
-
- # Learning mode — bind CC to strip, don't forward
- if status == 0xB0 and learning_strip:
- learning_strip.bind_hw_cc(cc_num)
- learning_strip = None
- return
-
- if bound_transport:
- # Transport CC — only goes to transport_midi_out
- 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:
- # Regular fader/toggle — goes to midi_out
- 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:
- # Not bound — pass through normally
- 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 blink(self, led):
- led.setStyleSheet("color: #2ecc71; font-size: 14px;")
- QTimer.singleShot(80, lambda: led.setStyleSheet("color: #444; font-size: 14px;"))
-
- def refresh_port_availability(self):
- """Gray out ports already in use by another dropdown."""
- out_port = picker_midi_out.currentText()
- t_port = picker_transport_out.currentText()
-
- # Output combo — gray out transport port
- for i in range(picker_midi_out.model().rowCount()):
- item = picker_midi_out.model().item(i)
- # Strip any existing [in use] tag before comparing
- 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'))
-
- # Transport combo — gray out output port
- 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 on_port_select(self, port_name):
- if midi_out.is_port_open():
- midi_out.close_port()
- 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}")
- self.refresh_port_availability()
-
- def on_transport_output_select(self, port_name):
- if not port_name:
- return
- if transport_midi_out.is_port_open():
- transport_midi_out.close_port()
- 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}")
- self.refresh_port_availability()
-
- def add_transport(self, label, default_cc):
- t = TransportWidget(label, default_cc)
- t.on_select_callback = self.select_strip
- t.learn_btn.clicked.connect(lambda: self.start_learn(t))
- t.on_zone_change_callback = self.on_transport_zone_change
- self.transports.append(t)
- hstack_transport_center.addWidget(t)
- self.refresh_all_id_labels()
-
- def on_transport_zone_change(self, transport):
- if transport.zone is not None and transport.center_index is None:
- if transport in self.transports:
- transport.center_index = self.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 self.transports))
- vsep_transport_right.setVisible(any(t.zone == "right" for t in self.transports))
- self.refresh_all_id_labels()
-
- def toggle_osc_log(self, checked):
- container_osc_in_log.setVisible(checked)
- container_osc_out_log.setVisible(checked)
- self._position_osc_log()
-
- def _position_osc_log(self):
- geo = self.frameGeometry()
- h = geo.height()
- # In panel to the right
- container_osc_in_log.setFixedHeight(h)
- container_osc_in_log.move(geo.right() + 4, geo.top())
- # Out panel stacked below in panel
- container_osc_out_log.setFixedWidth(360)
- container_osc_out_log.setFixedHeight(h // 2)
- container_osc_out_log.move(geo.right() + 4, geo.top() + h // 2 + 4)
- # Adjust in panel height
- container_osc_in_log.setFixedHeight(h // 2 - 4)
+ 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():
- self._position_osc_log()
+ log_ui_window_calc()
def moveEvent(self, event):
super().moveEvent(event)
if container_osc_in_log.isVisible():
- self._position_osc_log()
+ log_ui_window_calc()
- def on_osc_raw(self, addr, val):
- from datetime import datetime
- ts = datetime.now().strftime("%H:%M:%S")
- line = f"{ts} {addr} {val}"
- textedit_osc_in_log.append(line)
- doc = textedit_osc_in_log.document()
- if doc.blockCount() > 200:
- cursor = textedit_osc_in_log.textCursor()
- cursor.movePosition(cursor.MoveOperation.Start)
- cursor.select(cursor.SelectionType.LineUnderCursor)
- cursor.removeSelectedText()
- cursor.deleteChar()
+def make_separator():
+ sep = QWidget()
+ sep.setFixedHeight(1)
+ sep.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ sep.setStyleSheet("background-color: #3a3a3a;")
+ return sep
- def on_osc_out_raw(self, addr, val):
- from datetime import datetime
- ts = datetime.now().strftime("%H:%M:%S")
- line = f"{ts} {addr} {val}"
- textedit_osc_out_log.append(line)
- doc = textedit_osc_out_log.document()
- if doc.blockCount() > 200:
- cursor = textedit_osc_out_log.textCursor()
- cursor.movePosition(cursor.MoveOperation.Start)
- cursor.select(cursor.SelectionType.LineUnderCursor)
- cursor.removeSelectedText()
- cursor.deleteChar()
+def make_activity_blink(led):
+ led.setStyleSheet("color: #2ecc71; font-size: 14px;")
+ QTimer.singleShot(80, lambda: led.setStyleSheet("color: #444; font-size: 14px;"))
- def on_osc_ip_changed(self, ip):
- set_osc_target(ip=ip.strip() or "127.0.0.1")
+def populate_preset_dropdown_menu():
- def on_osc_send_port_changed(self, port):
- set_osc_target(port=port)
+ global loaded_presets
- def toggle_osc(self):
- 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)
+ 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": {}, "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:
- port = spinner_osc_listen_port.value()
- 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;")
+ item.setText(base)
+ item.setEnabled(True)
+ item.setForeground(QColor('#ffffff'))
- def on_osc_track(self, name):
- label_osc_track.setText(name)
- self.blink(activity_indicator_osc)
-
- def on_osc_preset(self, name):
- label_osc_preset.setText(name)
- self.blink(activity_indicator_osc)
-
- def on_osc_tempo(self, bpm):
- label_osc_tempo.setText(f"BPM: {bpm:.1f}")
-
- def on_osc_position(self, pos):
- label_osc_position.setText(f"Pos: {pos}")
-
- def on_osc_play(self, state):
- if state:
- label_osc_play_state.setText("▶ PLAYING")
- label_osc_play_state.setStyleSheet("color: #00e676; font-size: 11px; font-weight: bold;")
+ 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:
- label_osc_play_state.setText("■ STOPPED")
- label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
+ item.setText(base)
+ item.setEnabled(True)
+ item.setForeground(QColor('#ffffff'))
- def on_osc_record(self, state):
- if state:
- label_osc_record_state.setText("● REC")
- label_osc_record_state.setStyleSheet("color: #e53935; font-size: 11px; font-weight: bold;")
+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 log_osc_incoming(addr, val):
+ from datetime import datetime
+ ts = datetime.now().strftime("%H:%M:%S")
+ line = f"{ts} {addr} {val}"
+ textedit_osc_in_log.append(line)
+ doc = textedit_osc_in_log.document()
+ if doc.blockCount() > 200:
+ cursor = textedit_osc_in_log.textCursor()
+ cursor.movePosition(cursor.MoveOperation.Start)
+ cursor.select(cursor.SelectionType.LineUnderCursor)
+ cursor.removeSelectedText()
+ cursor.deleteChar()
+
+def log_osc_outgoing(addr, val):
+
+ from datetime import datetime
+
+ ts = datetime.now().strftime("%H:%M:%S")
+
+ line = f"{ts} {addr} {val}"
+ textedit_osc_out_log.append(line)
+ doc = textedit_osc_out_log.document()
+ if doc.blockCount() > 200:
+ cursor = textedit_osc_out_log.textCursor()
+ cursor.movePosition(cursor.MoveOperation.Start)
+ cursor.select(cursor.SelectionType.LineUnderCursor)
+ cursor.removeSelectedText()
+ cursor.deleteChar()
+
+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_osc_record_state.setText("● REC")
- label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
+ label_connection_osc.setText("Failed")
+ label_connection_osc.setStyleSheet("color: #c0392b;")
- def fire_transport(self, name):
- for t in self.transports:
- if t._label == name:
- t.on_click()
+
+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;")
+ 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()
+
+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
- def capture_key(self, name):
- pass
+ 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)
- def keyPressEvent(self, event):
- super().keyPressEvent(event)
+ 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)
- def keyPressEvent(self, event):
- if self._capturing_key_for:
- name = self._capturing_key_for
- entry = self.transport_buttons[name]
- key = event.key()
- modifiers = event.modifiers()
- if modifiers == Qt.KeyboardModifier.ControlModifier:
- entry["modifier"] = Qt.KeyboardModifier.ControlModifier
- else:
- entry["modifier"] = Qt.KeyboardModifier.NoModifier
- entry["key"] = key
- self._capturing_key_for = None
+ 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 ['learn_btn', 'cc_input_lbl', 'cc_spin', 'cc_output_lbl', 'midi_btn', 'osc_btn',
+ 'momentary_btn', 'toggle_mode_btn', 'osc_addr_edit', '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_cc": spinner_preset_browser_back.value(),
+ "fwd_cc": spinner_preset_browser_fwd.value()
+ },
+ "preset_uuid": label_preset_uuid.text().strip(),
+ "show_hide": {
+ "fader": fader_show_hide_config,
+ "toggle": toggle_show_hide_config,
+ "transport": transport_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", {})
+ spinner_preset_browser_back.setValue(browser.get("back_cc", 22))
+ spinner_preset_browser_fwd.setValue(browser.get("fwd_cc", 23))
+
+ 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))
+
+ 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:
+ menuPresets.setCurrentText(uuid_presets_matched[0])
+ 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;")
+ 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;")
+ 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;")
+ 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
+ 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 status == 0xB0 and learning_strip:
+ learning_strip.bind_hw_cc(cc_num, channel=channel)
+ learning_strip = None
+ return
+
+ if 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:
+ out_cc = t.cc_spin.value()
+ 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_input_lbl.setText(f"In CC{t.hw_cc} [{value}]")
+ t.cc_output_lbl.setText(f"Out 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)
+ log_ui_window_calc()
+
+
+def log_ui_window_calc():
+ geo = app_window.frameGeometry()
+ h = geo.height()
+ container_osc_in_log.setFixedHeight(h)
+ container_osc_in_log.move(geo.right() + 4, geo.top())
+ container_osc_out_log.setFixedWidth(360)
+ container_osc_out_log.setFixedHeight(h // 2)
+ container_osc_out_log.move(geo.right() + 4, geo.top() + h // 2 + 4)
+ container_osc_in_log.setFixedHeight(h // 2 - 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:
- key = event.key()
- modifiers = event.modifiers()
- for name, entry in self.transport_buttons.items():
- if entry["key"] is None:
- continue
- required_modifier = entry.get("modifier", Qt.KeyboardModifier.NoModifier)
- if key == entry["key"] and modifiers == required_modifier:
- self.fire_transport(name)
- return
- super().keyPressEvent(event)
+ 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.learn_btn.setVisible(not enabled)
+ t.cc_input_lbl.setVisible(not enabled)
+ t.cc_spin.setVisible(not enabled)
+ t.cc_output_lbl.setVisible(not enabled)
+ t.midi_btn.setVisible(not enabled)
+ t.osc_btn.setVisible(not enabled)
+ t.momentary_btn.setVisible(not enabled)
+ t.toggle_mode_btn.setVisible(not enabled)
+ t.osc_addr_edit.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)
diff --git a/midi_sender.py b/midi_sender.py
index 85177d2..8dfd305 100644
--- a/midi_sender.py
+++ b/midi_sender.py
@@ -12,11 +12,11 @@ available_ports = midi_out.get_ports()
print("Available MIDI ports:", available_ports)
-def send_cc(cc_number, value):
+def send_cc(cc_number, value, channel=None):
+ ch = (channel - 1) if channel is not None else MIDI_CH
if midi_out.is_port_open():
- midi_out.send_message([0xB0 | MIDI_CH, cc_number, value])
+ midi_out.send_message([0xB0 | ch, cc_number, value])
midi_receiver.midi_out_signal.emit()
- #print([0xB0 | MIDI_CH, cc_number, value])
def send_transport_cc(cc_number, value):
diff --git a/osc_receiver.py b/osc_receiver.py
index 8c1386d..31fa12f 100644
--- a/osc_receiver.py
+++ b/osc_receiver.py
@@ -9,6 +9,8 @@ class OSCReceiver(QObject):
record_signal = pyqtSignal(int)
raw_message_signal = pyqtSignal(str, str)
osc_out_signal = pyqtSignal(str, str)
+ uuid_preset = pyqtSignal(str)
+ bar_signal = pyqtSignal(str)
osc_receiver = OSCReceiver()
\ No newline at end of file
diff --git a/osc_sender.py b/osc_sender.py
index 29a45d5..da2417a 100644
--- a/osc_sender.py
+++ b/osc_sender.py
@@ -1,5 +1,4 @@
from pythonosc import udp_client
-from midi_receiver import midi_receiver
from osc_receiver import osc_receiver
@@ -32,7 +31,6 @@ def set_osc_target(ip=None, port=None):
def send_osc_message(address, value=1.0):
try:
get_osc_client().send_message(address, float(value))
- midi_receiver.transport_out_signal.emit()
osc_receiver.osc_out_signal.emit(address, str(value))
print(f"[OSC] → {address} {value}")
except Exception as e:
diff --git a/presets.py b/presets.py
index 90f5518..2034ea8 100644
--- a/presets.py
+++ b/presets.py
@@ -15,6 +15,7 @@ PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
os.makedirs(PRESETS_DIR, exist_ok=True)
+
def load_presets_file():
if os.path.exists(PRESETS_FILE):
with open(PRESETS_FILE, "r") as f:
@@ -27,32 +28,42 @@ def save_presets_file(data):
def ensure_preset_structure(presets):
if "presets" not in presets:
- return {"__last_used__": None, "presets": presets}
+ return {"__last_used__": None, "presets": presets, "transport_preset": [], "transport_show_hide": False, "io_config": {}}
+ if "transport_preset" not in presets:
+ presets["transport_preset"] = []
+ if "transport_show_hide" not in presets:
+ presets["transport_show_hide"] = False
+ if "io_config" not in presets:
+ presets["io_config"] = {}
return presets
-def on_preset_selected(menu_presets: QComboBox, app_window: QMainWindow, ready: bool, loaded_presets: dict):
+def on_preset_selected(menu_presets: QComboBox, app_window: QMainWindow, ready: bool, loaded_presets: dict, on_load=None):
selectedOption = menu_presets.currentText()
if ready and selectedOption and selectedOption in loaded_presets["presets"]:
loaded_presets["__last_used__"] = selectedOption
save_presets_file(loaded_presets)
- app_window.apply_preset(loaded_presets["presets"][selectedOption])
+ if on_load:
+ on_load(loaded_presets["presets"][selectedOption])
-def save_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict):
+
+def save_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict, on_save=None, on_build=None, on_save_as=None):
selectedOption = menu_presets.currentText()
if not selectedOption or selectedOption not in loaded_presets["presets"]:
- app_window.save_preset_as() # not self
+ if on_save_as:
+ on_save_as()
return
- loaded_presets["presets"][selectedOption] = app_window._build_preset_data(selectedOption) # not self
+ loaded_presets["presets"][selectedOption] = on_build(selectedOption) if on_build else {}
loaded_presets["__last_used__"] = selectedOption
save_presets_file(loaded_presets)
- app_window._show_save_status() # not self
+ if on_save:
+ on_save()
print(f"Saved preset: {selectedOption}")
-def save_preset_as(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict):
+def save_preset_as(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict, on_save=None, on_refresh=None, on_build=None):
print(f"save as button tapped")
@@ -62,24 +73,25 @@ def save_preset_as(app_window: QMainWindow, menu_presets: QComboBox, loaded_pres
if not ok or not selectedOption.strip():
return
selectedOption = selectedOption.strip()
- loaded_presets["presets"][selectedOption] = app_window._build_preset_data(selectedOption)
+ loaded_presets["presets"][selectedOption] = on_build(selectedOption) if on_build else {}
loaded_presets["__last_used__"] = selectedOption
save_presets_file(loaded_presets)
- app_window.refresh_preset_combo()
+ if on_refresh:
+ on_refresh()
menu_presets.setCurrentText(selectedOption)
- app_window._show_save_status()
+ if on_save:
+ on_save()
print(f"Saved preset as: {selectedOption}")
-
-def delete_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict):
- selectedOption = menu_presets.currentText()
+
+def delete_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict, on_refresh=None):
selectedOption = menu_presets.currentText()
if not selectedOption or selectedOption not in loaded_presets["presets"]:
return
-
+
msg = QMessageBox(app_window)
msg.setWindowTitle("Remove Preset")
msg.setText(f'Are you sure you want to remove "{selectedOption}"?')
@@ -89,12 +101,13 @@ def delete_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_prese
if reply != QMessageBox.StandardButton.Yes:
return
-
+
del loaded_presets["presets"][selectedOption]
if loaded_presets["__last_used__"] == selectedOption:
loaded_presets["__last_used__"] = None
-
+
save_presets_file(loaded_presets)
- app_window.refresh_preset_combo()
+ if on_refresh:
+ on_refresh()
print(f"Deleted preset: {selectedOption}")
\ No newline at end of file
diff --git a/presets/presets.json b/presets/presets.json
index 30d9a07..4e97fe6 100644
--- a/presets/presets.json
+++ b/presets/presets.json
@@ -1,1086 +1,439 @@
{
- "__last_used__": "gay3",
+ "__last_used__": "preset1",
"presets": {
- "1": {
- "midi_port": "IAC Driver Bus 1",
- "midi_input": "IAC Driver Bus 1",
- "transport_port": "IAC Driver Bus 1",
+ "preset1": {
"faders": [
{
- "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5",
+ "uid": "c98d2009-d186-489a-8883-ccbd612cb4af",
"label": "Fader 1",
"cc": 1,
+ "ch": 1,
+ "value": 38,
+ "color": "Red",
+ "pickup": false,
+ "last_sent": 38,
+ "hw_cc": null,
+ "hw_channel": null,
+ "zone": null,
+ "center_index": null,
+ "zone_index": null
+ },
+ {
+ "uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095",
+ "label": "Fader 2",
+ "cc": 1,
+ "ch": 1,
+ "value": 64,
+ "color": "Gray",
+ "pickup": false,
+ "last_sent": 64,
+ "hw_cc": null,
+ "hw_channel": null,
+ "zone": "right",
+ "center_index": 1,
+ "zone_index": -1
+ },
+ {
+ "uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
+ "label": "Fader 3",
+ "cc": 1,
+ "ch": 1,
"value": 64,
"color": "Orange",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71",
- "label": "Fader 2",
- "cc": 1,
- "value": 64,
- "color": "Yellow",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678",
- "label": "Fader 3",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc",
- "label": "Fader 4",
- "cc": 1,
- "value": 97,
- "color": "Gray",
- "pickup": false,
- "last_sent": 97,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
+ "hw_channel": null,
+ "zone": "left",
+ "center_index": 2,
+ "zone_index": -1
}
],
"toggles": [
{
- "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e",
+ "uid": "b4159deb-758b-4711-838c-a6bfe6d59872",
"label": "Toggle 1",
"cc": 20,
+ "ch": 1,
"state": false,
"color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null
- }
- ],
- "transports": [
- {
- "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
- "label": "Play",
- "cc": 117,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
+ "zone": "left",
+ "center_index": 0,
+ "zone_index": -1,
"hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
+ "hw_channel": null
},
{
- "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
- "label": "Stop",
- "cc": 116,
+ "uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583",
+ "label": "Toggle 2",
+ "cc": 20,
+ "ch": 1,
"state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
+ "color": "Blue",
+ "zone": "left",
+ "center_index": 1,
+ "zone_index": 0,
"hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
+ "hw_channel": null
},
{
- "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
- "label": "Record",
- "cc": 118,
+ "uid": "b3cf419a-5183-4272-bd6f-44f858afbec8",
+ "label": "Toggle 3",
+ "cc": 20,
+ "ch": 1,
"state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
+ "color": "Yellow",
+ "zone": "left",
+ "center_index": 2,
+ "zone_index": 1,
"hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
+ "hw_channel": null
},
{
- "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
- "label": "Arm",
- "cc": 119,
+ "uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03",
+ "label": "Toggle 4",
+ "cc": 20,
+ "ch": 1,
+ "state": false,
+ "color": "Orange",
+ "zone": null,
+ "center_index": null,
+ "zone_index": null,
+ "hw_cc": null,
+ "hw_channel": null
+ },
+ {
+ "uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7",
+ "label": "Toggle 6",
+ "cc": 20,
+ "ch": 1,
"state": false,
"color": "Gray",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
+ "hw_channel": null
+ },
+ {
+ "uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a",
+ "label": "Toggle 7",
+ "cc": 20,
+ "ch": 1,
+ "state": false,
+ "color": "Gray",
+ "zone": null,
+ "center_index": null,
+ "zone_index": null,
+ "hw_cc": null,
+ "hw_channel": null
+ },
+ {
+ "uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d",
+ "label": "Toggle 5",
+ "cc": 20,
+ "ch": 1,
+ "state": false,
+ "color": "Pink",
+ "zone": "right",
+ "center_index": 4,
+ "zone_index": -1,
+ "hw_cc": null,
+ "hw_channel": null
}
],
"browser": {
"back_cc": 22,
"fwd_cc": 23
},
- "osc": {
- "ip": "127.0.0.1",
- "listen_port": 9000,
- "send_port": 8000
+ "preset_uuid": "f230a4fd-edc3-4c4b-8585-f73c82168fee",
+ "show_hide": {
+ "fader": false,
+ "toggle": false,
+ "transport": false
}
},
- "2": {
- "midi_port": "IAC Driver Bus 1",
- "midi_input": "IAC Driver Bus 1",
- "transport_port": "IAC Driver Bus 1",
- "faders": [
- {
- "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5",
- "label": "Fader 1",
- "cc": 1,
- "value": 64,
- "color": "Orange",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71",
- "label": "Fader 2",
- "cc": 1,
- "value": 64,
- "color": "Yellow",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678",
- "label": "Fader 3",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc",
- "label": "Fader 4",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8",
- "label": "Fader 5",
- "cc": 1,
- "value": 64,
- "color": "Blue",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5",
- "label": "Fader 6",
- "cc": 1,
- "value": 64,
- "color": "Blue",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- }
- ],
+ "preset2": {
+ "faders": [],
"toggles": [
{
- "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e",
+ "uid": "6e8e21a9-0d01-482f-b5cc-8551bfd9c717",
"label": "Toggle 1",
"cc": 20,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null
- }
- ],
- "transports": [
- {
- "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
- "label": "Play",
- "cc": 117,
+ "ch": 1,
"state": false,
"color": "Gray",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
- "label": "Stop",
- "cc": 116,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
- "label": "Record",
- "cc": 118,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
- "label": "Arm",
- "cc": 119,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
+ "hw_channel": null
}
],
"browser": {
"back_cc": 22,
"fwd_cc": 23
},
- "osc": {
- "ip": "127.0.0.1",
- "listen_port": 9000,
- "send_port": 8000
+ "preset_uuid": "30ff84d1-9240-41f6-b499-1d18ef7ab2eb",
+ "show_hide": {
+ "fader": false,
+ "toggle": false,
+ "transport": false
}
},
- "gay": {
- "midi_port": "IAC Driver Bus 1",
- "midi_input": "IAC Driver Bus 1",
- "transport_port": "IAC Driver Bus 1",
+ "preset3": {
"faders": [
{
- "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5",
+ "uid": "c98d2009-d186-489a-8883-ccbd612cb4af",
"label": "Fader 1",
"cc": 1,
+ "ch": 1,
"value": 64,
- "color": "Orange",
+ "color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
+ "hw_channel": null,
+ "zone": "left",
+ "center_index": 0,
+ "zone_index": -1
},
{
- "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71",
+ "uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095",
"label": "Fader 2",
"cc": 1,
+ "ch": 1,
"value": 64,
- "color": "Yellow",
+ "color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678",
+ "uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
"label": "Fader 3",
"cc": 1,
+ "ch": 1,
"value": 64,
"color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc",
- "label": "Fader 4",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8",
- "label": "Fader 5",
- "cc": 1,
- "value": 64,
- "color": "Blue",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5",
- "label": "Fader 6",
- "cc": 1,
- "value": 64,
- "color": "Blue",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- }
- ],
- "toggles": [
- {
- "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e",
- "label": "Toggle 1",
- "cc": 20,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null
- }
- ],
- "transports": [
- {
- "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
- "label": "Play",
- "cc": 117,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
- "label": "Stop",
- "cc": 116,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
- "label": "Record",
- "cc": 118,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
- "label": "Arm",
- "cc": 119,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
+ "hw_channel": null,
+ "zone": "right",
+ "center_index": 2,
+ "zone_index": -1
}
],
+ "toggles": [],
"browser": {
"back_cc": 22,
"fwd_cc": 23
},
- "osc": {
- "ip": "127.0.0.1",
- "listen_port": 9000,
- "send_port": 8000
+ "preset_uuid": "e658b6bf-4bb7-4d68-bd8b-07775ee80c53",
+ "show_hide": {
+ "fader": false,
+ "toggle": false,
+ "transport": false
}
},
- "gay2": {
- "midi_port": "IAC Driver Bus 1",
- "midi_input": "IAC Driver Bus 1",
- "transport_port": "IAC Driver Bus 1",
+ "whatever": {
+ "faders": [],
+ "toggles": [],
+ "preset_uuid": "",
+ "browser": {},
+ "show_hide": {}
+ },
+ "preset5": {
"faders": [
{
- "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5",
+ "uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e",
"label": "Fader 1",
"cc": 1,
+ "ch": 1,
"value": 64,
- "color": "Orange",
+ "color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71",
+ "uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69",
"label": "Fader 2",
"cc": 1,
+ "ch": 1,
"value": 64,
- "color": "Yellow",
+ "color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678",
+ "uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb",
"label": "Fader 3",
"cc": 1,
+ "ch": 1,
"value": 64,
"color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc",
+ "uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e",
"label": "Fader 4",
"cc": 1,
+ "ch": 1,
"value": 64,
"color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8",
+ "uid": "fee53fde-2f59-4800-8873-3d75ca619366",
"label": "Fader 5",
"cc": 1,
+ "ch": 1,
"value": 64,
- "color": "Blue",
+ "color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5",
+ "uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421",
"label": "Fader 6",
"cc": 1,
- "value": 109,
- "color": "Blue",
+ "ch": 1,
+ "value": 64,
+ "color": "Gray",
"pickup": false,
- "last_sent": 109,
+ "last_sent": 64,
"hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
- "uid": "da37cac1-d0d9-4a18-bb94-8b7345bb8ee5",
+ "uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0",
"label": "Fader 7",
"cc": 1,
+ "ch": 1,
"value": 64,
"color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a77ac937-8661-4639-8091-bba5275d971f",
- "label": "Fader 8",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "8e852e9a-a579-4c45-b260-a1311b144b12",
- "label": "Fader 9",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
+ "hw_channel": null,
"zone": null,
"center_index": null,
"zone_index": null
}
],
- "toggles": [
- {
- "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e",
- "label": "Toggle 1",
- "cc": 20,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null
- }
- ],
- "transports": [
- {
- "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
- "label": "Play",
- "cc": 117,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
- "label": "Stop",
- "cc": 116,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
- "label": "Record",
- "cc": 118,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
- "label": "Arm",
- "cc": 119,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- }
- ],
+ "toggles": [],
"browser": {
"back_cc": 22,
"fwd_cc": 23
},
- "osc": {
- "ip": "127.0.0.1",
- "listen_port": 9000,
- "send_port": 8000
- }
- },
- "gay3": {
- "midi_port": "IAC Driver Bus 1",
- "midi_input": "IAC Driver Bus 1",
- "transport_port": "IAC Driver Bus 1",
- "faders": [
- {
- "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5",
- "label": "Fader 1",
- "cc": 1,
- "value": 64,
- "color": "Orange",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71",
- "label": "Fader 2",
- "cc": 1,
- "value": 64,
- "color": "Yellow",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678",
- "label": "Fader 3",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc",
- "label": "Fader 4",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8",
- "label": "Fader 5",
- "cc": 1,
- "value": 64,
- "color": "Blue",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5",
- "label": "Fader 6",
- "cc": 1,
- "value": 109,
- "color": "Blue",
- "pickup": false,
- "last_sent": 109,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "da37cac1-d0d9-4a18-bb94-8b7345bb8ee5",
- "label": "Fader 7",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a77ac937-8661-4639-8091-bba5275d971f",
- "label": "Fader 8",
- "cc": 1,
- "value": 94,
- "color": "Yellow",
- "pickup": false,
- "last_sent": 94,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "8e852e9a-a579-4c45-b260-a1311b144b12",
- "label": "Fader 9",
- "cc": 1,
- "value": 102,
- "color": "Teal",
- "pickup": false,
- "last_sent": 102,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- }
- ],
- "toggles": [
- {
- "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e",
- "label": "Toggle 1",
- "cc": 20,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null
- }
- ],
- "transports": [
- {
- "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
- "label": "Play",
- "cc": 117,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
- "label": "Stop",
- "cc": 116,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
- "label": "Record",
- "cc": 118,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- },
- {
- "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
- "label": "Arm",
- "cc": 119,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "midi",
- "osc_addr": "/play",
- "trigger_mode": "momentary"
- }
- ],
- "browser": {
- "back_cc": 22,
- "fwd_cc": 23
- },
- "osc": {
- "ip": "127.0.0.1",
- "listen_port": 9000,
- "send_port": 8000
- }
- },
- "gay5": {
- "midi_port": "IAC Driver Bus 1",
- "midi_input": "IAC Driver Bus 1",
- "transport_port": "IAC Driver Bus 1",
- "faders": [
- {
- "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5",
- "label": "Fader 1",
- "cc": 1,
- "value": 64,
- "color": "Orange",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71",
- "label": "Fader 2",
- "cc": 1,
- "value": 64,
- "color": "Yellow",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678",
- "label": "Fader 3",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc",
- "label": "Fader 4",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8",
- "label": "Fader 5",
- "cc": 1,
- "value": 64,
- "color": "Blue",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5",
- "label": "Fader 6",
- "cc": 1,
- "value": 109,
- "color": "Blue",
- "pickup": false,
- "last_sent": 109,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "da37cac1-d0d9-4a18-bb94-8b7345bb8ee5",
- "label": "Fader 7",
- "cc": 1,
- "value": 64,
- "color": "Gray",
- "pickup": false,
- "last_sent": 64,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "a77ac937-8661-4639-8091-bba5275d971f",
- "label": "Fader 8",
- "cc": 1,
- "value": 94,
- "color": "Yellow",
- "pickup": false,
- "last_sent": 94,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- },
- {
- "uid": "8e852e9a-a579-4c45-b260-a1311b144b12",
- "label": "Fader 9",
- "cc": 1,
- "value": 102,
- "color": "White",
- "pickup": false,
- "last_sent": 102,
- "hw_cc": null,
- "zone": null,
- "center_index": null,
- "zone_index": null
- }
- ],
- "toggles": [
- {
- "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e",
- "label": "Toggle 1",
- "cc": 20,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null
- }
- ],
- "transports": [
- {
- "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
- "label": "Play",
- "cc": 117,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "osc",
- "osc_addr": "/play",
- "trigger_mode": "toggle"
- },
- {
- "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
- "label": "Stop",
- "cc": 116,
- "state": false,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "osc",
- "osc_addr": "/play",
- "trigger_mode": "toggle"
- },
- {
- "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
- "label": "Record",
- "cc": 118,
- "state": true,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "osc",
- "osc_addr": "/play",
- "trigger_mode": "toggle"
- },
- {
- "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
- "label": "Arm",
- "cc": 119,
- "state": true,
- "color": "Gray",
- "zone": null,
- "center_index": null,
- "zone_index": null,
- "hw_cc": null,
- "output_mode": "osc",
- "osc_addr": "/play",
- "trigger_mode": "toggle"
- }
- ],
- "browser": {
- "back_cc": 22,
- "fwd_cc": 23
- },
- "osc": {
- "ip": "127.0.0.1",
- "listen_port": 9000,
- "send_port": 8000
+ "preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959",
+ "show_hide": {
+ "fader": false,
+ "toggle": false,
+ "transport": false
}
}
+ },
+ "transport_preset": [
+ {
+ "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c",
+ "label": "Play",
+ "cc": 117,
+ "state": false,
+ "color": "Green",
+ "zone": null,
+ "center_index": null,
+ "zone_index": null,
+ "hw_cc": null,
+ "output_mode": "osc",
+ "osc_addr": "t/1/play",
+ "trigger_mode": "momentary"
+ },
+ {
+ "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
+ "label": "Stop",
+ "cc": 116,
+ "state": false,
+ "color": "Gray",
+ "zone": null,
+ "center_index": null,
+ "zone_index": null,
+ "hw_cc": null,
+ "output_mode": "osc",
+ "osc_addr": "t/1/stop",
+ "trigger_mode": "momentary"
+ },
+ {
+ "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
+ "label": "Record",
+ "cc": 118,
+ "state": false,
+ "color": "Red",
+ "zone": null,
+ "center_index": null,
+ "zone_index": null,
+ "hw_cc": null,
+ "output_mode": "osc",
+ "osc_addr": "t/1/record",
+ "trigger_mode": "momentary"
+ },
+ {
+ "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
+ "label": "Arm",
+ "cc": 119,
+ "state": false,
+ "color": "Red",
+ "zone": null,
+ "center_index": null,
+ "zone_index": null,
+ "hw_cc": null,
+ "output_mode": "osc",
+ "osc_addr": "t/1/recenable/1/@",
+ "trigger_mode": "momentary"
+ }
+ ],
+ "transport_show_hide": false,
+ "io_config": {
+ "midi_out": "Not Connected",
+ "midi_in": "VMPK Output",
+ "transport_out": "Not Connected",
+ "osc_ip": "127.0.0.1",
+ "osc_listen_port": 9000,
+ "osc_send_port": 8000
}
}
\ No newline at end of file
diff --git a/togglewidget.py b/togglewidget.py
index 46c8937..ad3310a 100644
--- a/togglewidget.py
+++ b/togglewidget.py
@@ -16,8 +16,8 @@ class ToggleWidget(QWidget):
def __init__(self, label, default_cc):
super().__init__()
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
- self.setFixedWidth(100)
- self.current_color = "transparent"
+ self.setFixedWidth(115)
+ self.current_color = PALETTE_HEX.get("Gray", "#263238")
self.toggle_state = False
self.uid = str(uuid.uuid4())
self.zone = None
@@ -38,33 +38,83 @@ class ToggleWidget(QWidget):
inner.setSpacing(4)
inner.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- # Learn button
+ _grp_style = "QFrame { border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }"
+
+ # Group 1: Learn + input readout
self.hw_cc = None
+ self.hw_channel = None
self.is_learning = False
+ self.grp_learn = QFrame()
+ self.grp_learn.setStyleSheet(_grp_style)
+ _grp1_layout = QVBoxLayout(self.grp_learn)
+ _grp1_layout.setContentsMargins(4, 4, 4, 4)
+ _grp1_layout.setSpacing(3)
+
self.learn_btn = QPushButton("Learn")
self.learn_btn.setFixedHeight(20)
+ self.learn_btn.setStyleSheet("border: none;")
self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu)
- inner.addWidget(self.learn_btn)
+ _grp1_layout.addWidget(self.learn_btn)
- # CC In label
self.cc_input_lbl = QLabel("")
self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
- inner.addWidget(self.cc_input_lbl)
+ self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;")
+ self.cc_input_lbl.setFixedHeight(16)
+ _grp1_layout.addWidget(self.cc_input_lbl)
+
+ inner.addWidget(self.grp_learn)
+
+ self.arrow_lbl = QLabel("▼")
+ self.arrow_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
+ self.arrow_lbl.setStyleSheet("color: rgba(255,255,255,0.3); font-size: 9px; background: transparent;")
+ self.arrow_lbl.setFixedHeight(12)
+ inner.addWidget(self.arrow_lbl)
+
+ # Group 2: CC spin + CH spin + output readout
+ self.grp_cc = QFrame()
+ self.grp_cc.setStyleSheet(_grp_style)
+ _grp2_layout = QVBoxLayout(self.grp_cc)
+ _grp2_layout.setContentsMargins(4, 4, 4, 4)
+ _grp2_layout.setSpacing(3)
- # CC spin
self.cc_spin = QSpinBox()
- self.cc_spin.setMinimum(1)
+ self.cc_spin.setMinimum(0)
self.cc_spin.setMaximum(127)
self.cc_spin.setValue(default_cc)
- self.cc_spin.setFixedWidth(75)
- inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
+ self.cc_spin.setFixedWidth(55)
+ _cc_lbl = QLabel("CC")
+ _cc_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;")
+ self.cc_spin_row = QWidget()
+ _hbox_cc = QHBoxLayout(self.cc_spin_row)
+ _hbox_cc.setContentsMargins(0, 0, 0, 0)
+ _hbox_cc.setSpacing(4)
+ _hbox_cc.addWidget(_cc_lbl)
+ _hbox_cc.addWidget(self.cc_spin)
+ _grp2_layout.addWidget(self.cc_spin_row)
- self.cc_output_lbl = QLabel(f"Out CC{default_cc} [--]")
+ self.ch_spin = QSpinBox()
+ self.ch_spin.setMinimum(1)
+ self.ch_spin.setMaximum(16)
+ self.ch_spin.setValue(1)
+ self.ch_spin.setFixedWidth(55)
+ _ch_lbl = QLabel("CH")
+ _ch_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;")
+ self.ch_spin_row = QWidget()
+ _hbox_ch = QHBoxLayout(self.ch_spin_row)
+ _hbox_ch.setContentsMargins(0, 0, 0, 0)
+ _hbox_ch.setSpacing(4)
+ _hbox_ch.addWidget(_ch_lbl)
+ _hbox_ch.addWidget(self.ch_spin)
+ _grp2_layout.addWidget(self.ch_spin_row)
+
+ self.cc_output_lbl = QLabel(f"CC{default_cc} [--]")
self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
- inner.addWidget(self.cc_output_lbl)
+ self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;")
+ self.cc_output_lbl.setFixedHeight(16)
+ _grp2_layout.addWidget(self.cc_output_lbl)
+
+ inner.addWidget(self.grp_cc)
# Toggle button with LED dot overlay
btn_container = QWidget()
@@ -161,6 +211,17 @@ class ToggleWidget(QWidget):
def set_selected(self, selected):
self.apply_color(self.current_color, selected=selected)
+ def _cc_input_label(self, vel="--"):
+ if self.hw_cc is None:
+ return ""
+ ch = f" CH{self.hw_channel}" if self.hw_channel is not None else ""
+ return f"CC{self.hw_cc}{ch} [{vel}]"
+
+ def _cc_output_label(self, vel="--"):
+ out_cc = self.cc_spin.value()
+ out_ch = self.ch_spin.value()
+ return f"CC{out_cc} CH{out_ch} [{vel}]"
+
def start_learn(self):
self.is_learning = True
self.learn_btn.setText("Listening...")
@@ -168,18 +229,19 @@ class ToggleWidget(QWidget):
def stop_learn(self):
self.is_learning = False
- self.learn_btn.setStyleSheet("")
+ self.learn_btn.setStyleSheet("border: none;")
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
- self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
- self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
+ self.cc_input_lbl.setText(self._cc_input_label())
+ self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
- self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
+ self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;")
- def bind_hw_cc(self, cc_number):
+ def bind_hw_cc(self, cc_number, channel=None):
self.hw_cc = cc_number
+ self.hw_channel = channel
self.stop_learn()
def on_learn_context_menu(self, pos):
@@ -191,9 +253,10 @@ class ToggleWidget(QWidget):
action = menu.exec(self.learn_btn.mapToGlobal(pos))
if action == clear_action:
self.hw_cc = None
+ self.hw_channel = None
self.is_learning = False
self.learn_btn.setText("Learn")
- self.learn_btn.setStyleSheet("")
+ self.learn_btn.setStyleSheet("border: none;")
self.cc_input_lbl.setText("")
def update_id_label(self, zone, index):
@@ -229,9 +292,11 @@ class ToggleWidget(QWidget):
def on_click(self):
self.toggle_state = not self.toggle_state
cc_num = self.cc_spin.value()
+ out_ch = self.ch_spin.value()
value = 127 if self.toggle_state else 0
- send_cc(cc_num, value)
- self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
+ print(f"[toggle] CC{cc_num} CH{out_ch} ({value})")
+ send_cc(cc_num, value, channel=out_ch)
+ self.cc_output_lbl.setText(self._cc_output_label(value))
self._update_btn_style()
def _update_btn_style(self):
@@ -245,12 +310,14 @@ class ToggleWidget(QWidget):
"uid": self.uid,
"label": self.title_edit.text(),
"cc": self.cc_spin.value(),
+ "ch": self.ch_spin.value(),
"state": self.toggle_state,
"color": self.color_name,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index,
- "hw_cc": self.hw_cc
+ "hw_cc": self.hw_cc,
+ "hw_channel": self.hw_channel,
}
def set_state(self, state):
@@ -258,6 +325,7 @@ class ToggleWidget(QWidget):
self.uid = state["uid"]
self.title_edit.setText(state.get("label", ""))
self.cc_spin.setValue(state.get("cc", 1))
+ self.ch_spin.setValue(state.get("ch", 1))
color_name = state.get("color", "Gray")
hex_color = PALETTE_HEX.get(color_name, "transparent")
self.color_name = color_name
@@ -265,18 +333,18 @@ class ToggleWidget(QWidget):
self.apply_color(hex_color)
self.toggle_state = state.get("state", False)
self._update_btn_style()
- cc_num = self.cc_spin.value()
value = 127 if self.toggle_state else 0
- self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
+ self.cc_output_lbl.setText(self._cc_output_label(value))
self.zone = state.get("zone", None)
self.center_index = state.get("center_index", None)
self.zone_index = state.get("zone_index", None)
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
self.hw_cc = state.get("hw_cc", None)
+ self.hw_channel = state.get("hw_channel", None)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
- self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
- self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
+ self.cc_input_lbl.setText(self._cc_input_label())
+ self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
\ No newline at end of file
diff --git a/transportwidget.py b/transportwidget.py
index 2999adf..d59527c 100644
--- a/transportwidget.py
+++ b/transportwidget.py
@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
)
-from PyQt6.QtCore import Qt, QEvent
+from PyQt6.QtCore import Qt, QEvent, QTimer
from palette import PALETTE_HEX, PALETTE_VIVID
from styles import TITLE_STYLE
@@ -24,13 +24,13 @@ class TransportWidget(QWidget):
self.toggle_state = False
self.hw_cc = None
self.is_learning = False
- self.current_color = "transparent"
+ self.current_color = PALETTE_HEX.get("Gray", "#263238")
self.color_name = "Gray"
self.on_select_callback = None
self.on_zone_change_callback = None
self._label = label
- self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
+ self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Maximum)
self.setFixedWidth(100)
outer = QVBoxLayout(self)
@@ -57,6 +57,7 @@ class TransportWidget(QWidget):
self.cc_input_lbl = QLabel("")
self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
+ self.cc_input_lbl.setFixedHeight(16)
inner.addWidget(self.cc_input_lbl)
# CC spin
@@ -71,6 +72,7 @@ class TransportWidget(QWidget):
self.cc_output_lbl = QLabel(f"Out CC{default_cc} [--]")
self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
+ self.cc_output_lbl.setFixedHeight(16)
inner.addWidget(self.cc_output_lbl)
# MIDI / OSC mode selector
@@ -114,10 +116,9 @@ class TransportWidget(QWidget):
# OSC address field (hidden by default)
self.osc_addr_edit = QLineEdit("/play")
self.osc_addr_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
- self.osc_addr_edit.setFixedWidth(75)
self.osc_addr_edit.setPlaceholderText("/address")
self.osc_addr_edit.setVisible(False)
- inner.addWidget(self.osc_addr_edit, alignment=Qt.AlignmentFlag.AlignHCenter)
+ inner.addWidget(self.osc_addr_edit)
self.output_mode = "midi" # "midi" or "osc"
@@ -140,6 +141,7 @@ class TransportWidget(QWidget):
self.title_edit = QLineEdit(label)
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.title_edit.setStyleSheet(TITLE_STYLE)
+ self.title_edit.textChanged.connect(self.btn.setText)
inner.addWidget(self.title_edit)
# Color swatch
@@ -170,10 +172,10 @@ class TransportWidget(QWidget):
inner.addWidget(self.zone_btn)
# Separator + ID label
- id_sep = QWidget()
- id_sep.setFixedHeight(1)
- id_sep.setStyleSheet("background-color: #3a3a3a;")
- inner.addWidget(id_sep)
+ self.id_sep = QWidget()
+ self.id_sep.setFixedHeight(1)
+ self.id_sep.setStyleSheet("background-color: #3a3a3a;")
+ inner.addWidget(self.id_sep)
self.id_label = QLabel("")
self.id_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
diff --git a/zonebutton.py b/zonebutton.py
index a43f88f..a942fa9 100644
--- a/zonebutton.py
+++ b/zonebutton.py
@@ -38,7 +38,7 @@ class ZoneButton(QPushButton):
self._update_style()
def _update_style(self):
- l_color = "#00c853" if self.left_active else "#2a2a2a"
+ l_color = "#ff8c00" if self.left_active else "#2a2a2a"
r_color = "#00c853" if self.right_active else "#2a2a2a"
self.setStyleSheet(f"""