JL Cooper MIDI Controller I/O, Focus widget hardware binding, iOS Channel Focus polish

Desktop:
- New dedicated MIDI Controller In port (separate from MIDI In), same CC learn/bind pipeline but never passes notes through; positioned first in the IO column order
- Fixed MIDI In "Not Connected" not persisting across restart (missing save_io_config call)
- Fixed MIDI Out/Transport Out mutual-exclusion incorrectly flagging "Not Connected" as an in-use port
- Renamed "Remote" checkbox to "App" on Fader/Toggle/Transport widgets
- FocusFaderWidget/FocusToggleWidget: new Hardware section (Learn + CC/CH bind) so a physical controller can drive a Channel Focus widget alongside the app/OSC, plus an independent Feedback checkbox that sends translated CC out (for motorized fader / LED sync)
- JL Cooper touch-sense profile ("No Profile" / "MIDI JL Cooper CC Mode") on FaderWidget/FocusFaderWidget: touch channel (value channel - 1, same CC) is filtered out of the value and gates incoming OSC/DAW feedback while touching
- FocusFeedbackWidget: "Custom" checkbox to show a manually-typed static string instead of live OSC feedback, disabling Learn/OSC input and suppressing the iPad broadcast while active
- New daw-config-reaper/ folder with reaper-osc-config-paul-custom.ReaperOSC

iOS:
- New FocusToggleWidget (dedicated Channel Focus toggle, split out of shared ToggleWidget) with colorName-aware darken/border and isMomentary handling — keeps Channel-Focus-only behavior off the regular ToggleWidget
- Arrange mode long-press menu: text alignment (left/center/right) and a 10-color named-palette Text Color picker for TitleWidget/FocusFeedbackWidget, persisted in the saved layout
- FocusFeedbackWidget: Reset Size action, monospaced-digit font fix for timestamp/counter jitter
- New "Cancel Changes" button in Arrange mode — reverts layout/hides/aligns to the state at Arrange-mode entry and restores desktop-side Remote/dest assignments
- Grid line brightness increased for visibility
- Bonjour discovery for desktop auto-connect

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-06 22:17:37 -04:00
parent d6201c59b4
commit 0c7b6b4442
34 changed files with 4815 additions and 208 deletions
+27 -10
View File
@@ -152,6 +152,26 @@ class TransportWidget(QWidget):
self.output_mode = "midi"
# Group 3: Remote routing
self.grp_remote = QFrame()
self.grp_remote.setStyleSheet(_grp_style)
_grp_remote_layout = QVBoxLayout(self.grp_remote)
_grp_remote_layout.setContentsMargins(4, 4, 4, 4)
_grp_remote_layout.setSpacing(3)
self.remote_checkbox = QCheckBox("App")
self.remote_checkbox.setChecked(True)
self.remote_checkbox.stateChanged.connect(lambda _: self.dest_spin.setEnabled(self.remote_checkbox.isChecked()))
_grp_remote_layout.addWidget(self.remote_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
self.dest_spin = QSpinBox()
self.dest_spin.setRange(1, 9)
self.dest_spin.setValue(1)
self.dest_spin.setFixedWidth(52)
_grp_remote_layout.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
inner.addWidget(self.grp_remote)
# Transport button with LED dot
btn_container = QWidget()
btn_container.setFixedSize(60, 60)
@@ -195,15 +215,6 @@ 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)
@@ -387,6 +398,7 @@ class TransportWidget(QWidget):
"output_mode": self.output_mode,
"osc_addr": self.osc_addr_edit.text(),
"trigger_mode": self.trigger_mode,
"remote": self.remote_checkbox.isChecked(),
"dest_id": self.dest_spin.value(),
}
@@ -414,6 +426,11 @@ class TransportWidget(QWidget):
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"))
remote = state.get("remote", True)
self.remote_checkbox.blockSignals(True)
self.remote_checkbox.setChecked(remote)
self.remote_checkbox.blockSignals(False)
self.dest_spin.blockSignals(True)
self.dest_spin.setValue(state.get("dest_id", 1))
self.dest_spin.blockSignals(False)
self.dest_spin.blockSignals(False)
self.dest_spin.setEnabled(remote)