Multi-iPad dest_id routing, activity ring, context menu hide, live label/visibility sync

Desktop:
- Replace Remote checkbox with T: dest_spin (0=Off, 1-9=tablet) on all widgets
- dest_id persisted in get_state/set_state, broadcast in tablet snapshot
- dest_spin.valueChanged triggers save + broadcast to iPad
- _on_widget_visibility_from_tablet updates dest_spin from iPad hide event
- _broadcast_preset_update shared helper; ws_server.has_clients guard
- label editingFinished triggers live label sync to iPad

iOS:
- VC-Arrange/VC-Logging/Widgets-Reusable-UI file reorganisation
- ArrangeObjects/ArrangeView rename, 7-bucket Arrange-Functions split
- myTabletId (UserDefaults) + tabletIdField next to port in toolbar
- Preset filter: destId == myTabletId instead of visible bool
- BaseWidget activity ring: long press gesture (touch) + final update() flash
- Context menu long press (arrange mode): hide widget, save layout, send dest_id:0
- Same-UUID preset check skips Preset Switched alert on visibility/label updates
- autoSwitchEnabled, toolbarStatusLabel, presetLabel, statusToken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-02 21:56:35 -04:00
parent 7a74cfa75e
commit d6201c59b4
32 changed files with 1042 additions and 501 deletions
+14 -1
View File
@@ -155,6 +155,15 @@ class FaderWidget(QWidget):
self.pickup_checkbox.stateChanged.connect(self.on_pickup_changed)
inner.addWidget(self.pickup_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
# Remote checkbox
self.dest_spin = QSpinBox()
self.dest_spin.setRange(0, 9)
self.dest_spin.setSpecialValueText("Off")
self.dest_spin.setPrefix("T:")
self.dest_spin.setValue(1)
self.dest_spin.setFixedWidth(52)
inner.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
# Scribble strip
self.title_edit = QLineEdit(label)
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
@@ -437,7 +446,8 @@ class FaderWidget(QWidget):
"hw_channel": self.hw_channel,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index
"zone_index": self.zone_index,
"dest_id": self.dest_spin.value(),
}
def set_state(self, state):
@@ -464,6 +474,9 @@ class FaderWidget(QWidget):
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)
self.dest_spin.blockSignals(True)
self.dest_spin.setValue(state.get("dest_id", 1))
self.dest_spin.blockSignals(False)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(self._cc_input_label())
+55 -5
View File
@@ -135,11 +135,21 @@ containerMain = QWidget()
#MARK: PRESET ROW
hstack_preset_row = QHBoxLayout()
#MARK: PRESENT
#MARK: PRESENT
menuPresets = QComboBox()
menuPresets.setMinimumWidth(160)
menuPresets.currentTextChanged.connect(lambda name: on_preset_selected(menuPresets, app_window, ready, loaded_presets, on_load=preset_load))
buttonPresetPrev = QPushButton("")
buttonPresetPrev.setFixedWidth(28)
buttonPresetPrev.setToolTip("Previous preset")
buttonPresetPrev.clicked.connect(lambda: menuPresets.setCurrentIndex(max(0, menuPresets.currentIndex() - 1)))
buttonPresetNext = QPushButton("")
buttonPresetNext.setFixedWidth(28)
buttonPresetNext.setToolTip("Next preset")
buttonPresetNext.clicked.connect(lambda: menuPresets.setCurrentIndex(min(menuPresets.count() - 1, menuPresets.currentIndex() + 1)))
#button new blank preset
buttonNew = QPushButton("New")
@@ -781,6 +791,8 @@ for _label, _cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]
_t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
_t.on_context_menu_callback = lambda strip, pos: strip_context_menu(strip, pos)
_t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
_t.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
_t.title_edit.editingFinished.connect(lambda: _broadcast_preset_update())
del _t, _label, _cc
# MARK: SELECTED CHANNEL ROW MODULE
@@ -964,6 +976,8 @@ class MainWindow(QMainWindow):
main_layout.addLayout(hstack_preset_row)
hstack_preset_row.addWidget(menuPresets) #dropdown
hstack_preset_row.addWidget(buttonPresetPrev)
hstack_preset_row.addWidget(buttonPresetNext)
hstack_preset_row.addWidget(buttonNew)
hstack_preset_row.addWidget(buttonSave) #save button
hstack_preset_row.addWidget(buttonSaveAs) #save as button
@@ -2181,6 +2195,8 @@ def toggle_add(insert_after=None):
toggle.on_context_menu_callback = strip_context_menu
toggle.on_zone_change_callback = handle_toggle_zone_change
toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle))
toggle.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
toggle.title_edit.editingFinished.connect(_broadcast_preset_update)
update_window_width()
check_and_index_strip_labels()
_update_toggle_placeholder()
@@ -2214,6 +2230,8 @@ def fader_add(insert_after=None):
fader.on_select_callback = check_and_update_selected_strip
fader.on_context_menu_callback = strip_context_menu
fader.on_zone_change_callback = handle_fader_zone_change
fader.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
fader.title_edit.editingFinished.connect(_broadcast_preset_update)
update_window_width()
check_and_index_strip_labels()
_update_fader_placeholder()
@@ -2238,6 +2256,8 @@ def transport_toggle_add(label, default_cc, insert_after=None):
t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t))
t.minus_btn.clicked.connect(lambda: transport_remove(t))
t.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
t.title_edit.editingFinished.connect(_broadcast_preset_update)
params_visible = not transport_show_hide_config
for attr in ['grp_learn', 'arrow_lbl', 'grp_cc', 'zone_btn', 'minus_btn',
'plus_btn', 'title_edit', 'id_sep', 'id_label', 'color_swatch_btn']:
@@ -2351,9 +2371,9 @@ def _build_tablet_snapshot():
"preset_uuid": label_preset_uuid.text().strip(),
"preset_name": name,
"big_title": big_title_edit.text() if big_title_edit else "",
"faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "visible": True, "color": PALETTE_HEX.get(f.color_name, "#00e676")} for f in faders],
"toggles": [{"uid": t.uid, "label": t.title_edit.text(), "state": t.toggle_state, "visible": True, "color": PALETTE_HEX.get(t.color_name, "#00e676")} for t in toggles],
"transports": [{"uid": t.uid, "label": t.title_edit.text(), "visible": True} for t in transports],
"faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "dest_id": f.dest_spin.value(), "color": PALETTE_HEX.get(f.color_name, "#00e676")} for f in faders],
"toggles": [{"uid": t.uid, "label": t.title_edit.text(), "state": t.toggle_state, "dest_id": t.dest_spin.value(), "color": PALETTE_HEX.get(t.color_name, "#00e676")} for t in toggles],
"transports": [{"uid": t.uid, "label": t.title_edit.text(), "dest_id": t.dest_spin.value()} for t in transports],
}
return snapshot, saved_layout
@@ -2365,6 +2385,35 @@ def _on_tablet_connected():
log_ws_outgoing(snapshot)
def _on_widget_visibility_from_tablet(uid, dest_id):
for w in faders + toggles + transports:
if w.uid == uid:
w.dest_spin.blockSignals(True)
w.dest_spin.setValue(dest_id)
w.dest_spin.blockSignals(False)
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
loaded_presets["presets"][name].update(preset_build_snapshot(name))
save_presets_file(loaded_presets)
show_save_status()
break
def _broadcast_preset_update():
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
loaded_presets["presets"][name].update(preset_build_snapshot(name))
save_presets_file(loaded_presets)
if ws_server and ws_server.has_clients:
snapshot, layout = _build_tablet_snapshot()
ws_server.broadcast_preset(snapshot, layout)
log_ws_outgoing(snapshot)
def _on_remote_checkbox_changed():
_broadcast_preset_update()
def _on_tablet_layout_saved(preset_uuid, layout):
name = menuPresets.currentText()
if name and name in loaded_presets.get("presets", {}):
@@ -2498,7 +2547,7 @@ def preset_load(preset):
check_and_index_strip_labels()
if ws_server:
if ws_server and ws_server.has_clients:
snapshot, layout = _build_tablet_snapshot()
ws_server.broadcast_preset(snapshot, layout)
log_ws_outgoing(snapshot)
@@ -2865,6 +2914,7 @@ def _start_tablet_server():
ws_server.raw_in_signal.connect(log_ws_incoming)
ws_server.control_received.connect(lambda uid, val: midi_receiver.hw_cc_signal.emit(uid, val))
ws_server.layout_saved.connect(_on_tablet_layout_saved)
ws_server.widget_visibility_received.connect(_on_widget_visibility_from_tablet)
ws_server.client_connected.connect(_on_tablet_connected)
try:
s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
+14 -1
View File
@@ -2,7 +2,7 @@ import uuid
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy, QCheckBox
)
from PyQt6.QtCore import Qt, QEvent, QTimer
@@ -180,6 +180,15 @@ class ToggleWidget(QWidget):
toggle_btn_row.addWidget(self.plus_btn)
inner.addLayout(toggle_btn_row)
# Remote checkbox
self.dest_spin = QSpinBox()
self.dest_spin.setRange(0, 9)
self.dest_spin.setSpecialValueText("Off")
self.dest_spin.setPrefix("T:")
self.dest_spin.setValue(1)
self.dest_spin.setFixedWidth(52)
inner.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
# Zone button
self.zone_btn = ZoneButton()
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
@@ -370,6 +379,7 @@ class ToggleWidget(QWidget):
"hw_cc": self.hw_cc,
"hw_channel": self.hw_channel,
"trigger_mode": self.trigger_mode,
"dest_id": self.dest_spin.value(),
}
def set_state(self, state):
@@ -394,6 +404,9 @@ class ToggleWidget(QWidget):
self.hw_cc = state.get("hw_cc", None)
self.hw_channel = state.get("hw_channel", None)
self.set_trigger_mode(state.get("trigger_mode", "toggle"))
self.dest_spin.blockSignals(True)
self.dest_spin.setValue(state.get("dest_id", 1))
self.dest_spin.blockSignals(False)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(self._cc_input_label())
+15 -2
View File
@@ -2,7 +2,7 @@ import uuid
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy, QCheckBox
)
from PyQt6.QtCore import Qt, QEvent, QTimer
@@ -195,6 +195,15 @@ class TransportWidget(QWidget):
btn_row.addWidget(self.plus_btn)
inner.addLayout(btn_row)
# Remote checkbox
self.dest_spin = QSpinBox()
self.dest_spin.setRange(0, 9)
self.dest_spin.setSpecialValueText("Off")
self.dest_spin.setPrefix("T:")
self.dest_spin.setValue(1)
self.dest_spin.setFixedWidth(52)
inner.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
# Zone button
self.zone_btn = ZoneButton()
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
@@ -378,6 +387,7 @@ class TransportWidget(QWidget):
"output_mode": self.output_mode,
"osc_addr": self.osc_addr_edit.text(),
"trigger_mode": self.trigger_mode,
"dest_id": self.dest_spin.value(),
}
def set_state(self, state):
@@ -403,4 +413,7 @@ class TransportWidget(QWidget):
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
self.osc_addr_edit.setText(state.get("osc_addr", "/play"))
self.set_output_mode(state.get("output_mode", "midi"))
self.set_trigger_mode(state.get("trigger_mode", "momentary"))
self.set_trigger_mode(state.get("trigger_mode", "momentary"))
self.dest_spin.blockSignals(True)
self.dest_spin.setValue(state.get("dest_id", 1))
self.dest_spin.blockSignals(False)