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:
@@ -0,0 +1,226 @@
|
||||
import uuid
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QLabel, QLineEdit, QFrame, QSizePolicy, QCheckBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QEvent
|
||||
|
||||
from palette import PALETTE_HEX, PALETTE_VIVID
|
||||
from styles import TITLE_STYLE
|
||||
from colorswatchpopup import ColorSwatchPopup
|
||||
|
||||
|
||||
class FocusFeedbackWidget(QWidget):
|
||||
"""Read-only OSC feedback display for Channel Focus — Learn an address, show whatever value arrives. No output, no interaction."""
|
||||
def __init__(self, label):
|
||||
super().__init__()
|
||||
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
||||
self.setFixedWidth(115)
|
||||
self.current_color = PALETTE_HEX.get("Gray", "#263238")
|
||||
self.uid = str(uuid.uuid4())
|
||||
self.is_learning = False
|
||||
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(0, 0, 0, 0)
|
||||
outer.setSpacing(2)
|
||||
|
||||
self.container = QFrame()
|
||||
self.container.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.apply_color(self.current_color)
|
||||
|
||||
inner = QVBoxLayout(self.container)
|
||||
inner.setContentsMargins(4, 6, 4, 6)
|
||||
inner.setSpacing(4)
|
||||
inner.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
_grp_style = "QFrame { border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }"
|
||||
|
||||
# Group: OSC input (feedback from the DAW)
|
||||
self.grp_input = QFrame()
|
||||
self.grp_input.setStyleSheet(_grp_style)
|
||||
_grp_input_layout = QVBoxLayout(self.grp_input)
|
||||
_grp_input_layout.setContentsMargins(4, 4, 4, 4)
|
||||
_grp_input_layout.setSpacing(3)
|
||||
|
||||
self.learn_btn = QPushButton("Learn")
|
||||
self.learn_btn.setFixedHeight(20)
|
||||
self.learn_btn.setStyleSheet("border: none;")
|
||||
_grp_input_layout.addWidget(self.learn_btn)
|
||||
|
||||
self.osc_addr_in_edit = QLineEdit("/3/trackname")
|
||||
self.osc_addr_in_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
self.osc_addr_in_edit.setPlaceholderText("/address")
|
||||
self.osc_addr_in_edit.setStyleSheet("border: none;")
|
||||
_grp_input_layout.addWidget(self.osc_addr_in_edit)
|
||||
|
||||
# Custom: shows a fixed, manually-typed string instead of live OSC
|
||||
# feedback — disables Learn and the OSC address input while checked.
|
||||
self.custom_checkbox = QCheckBox("Custom")
|
||||
self.custom_checkbox.setChecked(False)
|
||||
self.custom_checkbox.stateChanged.connect(self._on_custom_changed)
|
||||
_grp_input_layout.addWidget(self.custom_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
self.custom_text_edit = QLineEdit("")
|
||||
self.custom_text_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
self.custom_text_edit.setPlaceholderText("Custom text")
|
||||
self.custom_text_edit.setStyleSheet("border: none;")
|
||||
self.custom_text_edit.textChanged.connect(self._on_custom_text_changed)
|
||||
self.custom_text_edit.setVisible(False)
|
||||
_grp_input_layout.addWidget(self.custom_text_edit)
|
||||
|
||||
inner.addWidget(self.grp_input)
|
||||
|
||||
# Readout — the live feedback value
|
||||
self.readout_lbl = QLabel("—")
|
||||
self.readout_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.readout_lbl.setWordWrap(True)
|
||||
self.readout_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 14px; font-weight: bold; padding: 6px 2px; border-radius: 4px;")
|
||||
self.readout_lbl.setMinimumHeight(48)
|
||||
inner.addWidget(self.readout_lbl)
|
||||
|
||||
# Scribble strip (caption — what this feedback represents, e.g. "Track Name")
|
||||
self.title_edit = QLineEdit(label)
|
||||
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
self.title_edit.setStyleSheet(TITLE_STYLE)
|
||||
inner.addWidget(self.title_edit)
|
||||
|
||||
# Color swatch button
|
||||
self.color_name = "Gray"
|
||||
self.color_swatch_btn = QPushButton()
|
||||
self.color_swatch_btn.setFixedHeight(10)
|
||||
self.color_swatch_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID['Gray']}; border-radius: 2px; border: none;")
|
||||
self.color_swatch_btn.clicked.connect(self.show_color_popup)
|
||||
inner.addWidget(self.color_swatch_btn)
|
||||
|
||||
# + / - buttons
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setContentsMargins(0, 0, 0, 0)
|
||||
btn_row.setSpacing(2)
|
||||
self.minus_btn = QPushButton("-")
|
||||
self.minus_btn.setFixedSize(30, 20)
|
||||
self.plus_btn = QPushButton("+")
|
||||
self.plus_btn.setFixedSize(30, 20)
|
||||
btn_row.addWidget(self.minus_btn)
|
||||
btn_row.addStretch()
|
||||
btn_row.addWidget(self.plus_btn)
|
||||
inner.addLayout(btn_row)
|
||||
|
||||
outer.addWidget(self.container)
|
||||
self.on_select_callback = None
|
||||
self.on_context_menu_callback = None
|
||||
|
||||
for child in self.findChildren(QWidget):
|
||||
child.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QEvent.Type.MouseButtonPress:
|
||||
if event.button() != Qt.MouseButton.RightButton:
|
||||
if self.on_select_callback:
|
||||
self.on_select_callback(self)
|
||||
elif event.type() == QEvent.Type.ContextMenu:
|
||||
if self.on_context_menu_callback:
|
||||
self.on_context_menu_callback(self, event.globalPos())
|
||||
return True
|
||||
return False
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() != Qt.MouseButton.RightButton:
|
||||
if self.on_select_callback:
|
||||
self.on_select_callback(self)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
if self.on_context_menu_callback:
|
||||
self.on_context_menu_callback(self, event.globalPos())
|
||||
|
||||
def start_learn(self):
|
||||
self.is_learning = True
|
||||
self.learn_btn.setText("Listening...")
|
||||
self.learn_btn.setStyleSheet("color: orange; font-weight: bold;")
|
||||
|
||||
def stop_learn(self):
|
||||
self.is_learning = False
|
||||
self.learn_btn.setText("Learn")
|
||||
self.learn_btn.setStyleSheet("")
|
||||
|
||||
def bind_osc_addr(self, address):
|
||||
self.osc_addr_in_edit.setText(address)
|
||||
self.stop_learn()
|
||||
|
||||
def _update_custom_visibility(self):
|
||||
is_custom = self.custom_checkbox.isChecked()
|
||||
self.learn_btn.setVisible(not is_custom)
|
||||
self.osc_addr_in_edit.setVisible(not is_custom)
|
||||
self.custom_text_edit.setVisible(is_custom)
|
||||
|
||||
def _on_custom_changed(self, _state):
|
||||
self._update_custom_visibility()
|
||||
if self.custom_checkbox.isChecked():
|
||||
self.readout_lbl.setText(self.custom_text_edit.text())
|
||||
|
||||
def _on_custom_text_changed(self, text):
|
||||
if self.custom_checkbox.isChecked():
|
||||
self.readout_lbl.setText(text)
|
||||
|
||||
def receive_osc_value(self, value):
|
||||
"""Display whatever feedback arrives on the bound address, verbatim."""
|
||||
if self.custom_checkbox.isChecked():
|
||||
return
|
||||
self.readout_lbl.setText(value)
|
||||
|
||||
def apply_color(self, hex_color, selected=False):
|
||||
self.current_color = hex_color
|
||||
border = "#00ff88" if selected else "rgba(0,0,0,0.3)"
|
||||
border_width = "3px" if selected else "2px"
|
||||
self.container.setObjectName("stripContainer")
|
||||
self.container.setStyleSheet(f"""
|
||||
QFrame#stripContainer {{
|
||||
background-color: {hex_color};
|
||||
border-radius: 6px;
|
||||
border: {border_width} solid {border};
|
||||
}}
|
||||
""")
|
||||
|
||||
def set_selected(self, selected):
|
||||
self.apply_color(self.current_color, selected=selected)
|
||||
|
||||
def show_color_popup(self):
|
||||
popup = ColorSwatchPopup(self.on_color_change, self)
|
||||
btn_pos = self.color_swatch_btn.mapToGlobal(self.color_swatch_btn.rect().bottomLeft())
|
||||
popup.move(btn_pos)
|
||||
popup.show()
|
||||
|
||||
def on_color_change(self, name, hex_color=None):
|
||||
if hex_color is None:
|
||||
hex_color = PALETTE_HEX.get(name, self.current_color)
|
||||
self.color_name = name
|
||||
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(self.color_name, hex_color)}; border-radius: 2px; border: none;")
|
||||
self.apply_color(hex_color)
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
"uid": self.uid,
|
||||
"label": self.title_edit.text(),
|
||||
"text": self.readout_lbl.text(),
|
||||
"color": self.color_name,
|
||||
"osc_addr_in": self.osc_addr_in_edit.text(),
|
||||
"custom_enabled": self.custom_checkbox.isChecked(),
|
||||
"custom_text": self.custom_text_edit.text(),
|
||||
}
|
||||
|
||||
def set_state(self, state):
|
||||
if "uid" in state:
|
||||
self.uid = state["uid"]
|
||||
self.title_edit.setText(state.get("label", ""))
|
||||
self.readout_lbl.setText(state.get("text", "—"))
|
||||
color_name = state.get("color", "Gray")
|
||||
hex_color = PALETTE_HEX.get(color_name, "transparent")
|
||||
self.color_name = color_name
|
||||
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(self.color_name, hex_color)}; border-radius: 2px; border: none;")
|
||||
self.apply_color(hex_color)
|
||||
self.osc_addr_in_edit.setText(state.get("osc_addr_in", "/3/trackname"))
|
||||
self.custom_text_edit.setText(state.get("custom_text", ""))
|
||||
self.custom_checkbox.setChecked(state.get("custom_enabled", False))
|
||||
self._update_custom_visibility()
|
||||
Reference in New Issue
Block a user