Reorganize top-level directories with clearer naming convention

app -> app-desktop-macos, presets -> app-presets, server -> remote-server,
daw-config-reaper -> osc-config-daw, plugin-reaper-realearn -> plugin-reaper-relearn.
Updated run.py and presets.py path references accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-15 17:51:05 -04:00
parent e58f06d9fa
commit 7ecc718f5d
2256 changed files with 11 additions and 5 deletions
+491
View File
@@ -0,0 +1,491 @@
import uuid
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QLineEdit, QFrame, QSizePolicy, QCheckBox, QSpinBox
)
from PyQt6.QtCore import Qt, QEvent, QTimer
from palette import PALETTE_HEX, PALETTE_VIVID
from styles import TITLE_STYLE
from zonebutton import ZoneButton
from colorswatchpopup import ColorSwatchPopup
from osc_sender import send_osc_message
from midi_sender import send_cc
class FocusToggleWidget(QWidget):
"""Direct DAW-focus toggle — OSC-only output, shown on any iPad in Channel Focus mode, no hardware learn input."""
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.toggle_state = False
self.trigger_mode = "toggle"
self.uid = str(uuid.uuid4())
self.zone = None
self.center_index = None
self.zone_index = None
self.on_zone_change_callback = None
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("/track/mute")
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)
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; border: none;")
self.cc_input_lbl.setFixedHeight(16)
_grp_input_layout.addWidget(self.cc_input_lbl)
inner.addWidget(self.grp_input)
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: OSC output controls + readout (sent to the DAW on click)
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)
# Momentary / Toggle mode selector
trigger_row = QHBoxLayout()
trigger_row.setContentsMargins(0, 0, 0, 0)
trigger_row.setSpacing(2)
self.momentary_btn = QPushButton("Mom.")
self.momentary_btn.setFixedHeight(18)
self.momentary_btn.setCheckable(True)
self.momentary_btn.setChecked(False)
self.momentary_btn.setStyleSheet("border: none;")
self.toggle_mode_btn = QPushButton("Tog.")
self.toggle_mode_btn.setFixedHeight(18)
self.toggle_mode_btn.setCheckable(True)
self.toggle_mode_btn.setChecked(True)
self.toggle_mode_btn.setStyleSheet("font-weight: bold; color: #00e676;")
self.momentary_btn.clicked.connect(lambda: self.set_trigger_mode("momentary"))
self.toggle_mode_btn.clicked.connect(lambda: self.set_trigger_mode("toggle"))
trigger_row.addWidget(self.momentary_btn)
trigger_row.addWidget(self.toggle_mode_btn)
_grp2_layout.addLayout(trigger_row)
self.osc_addr_out_edit = QLineEdit("/track/mute")
self.osc_addr_out_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.osc_addr_out_edit.setPlaceholderText("/address")
self.osc_addr_out_edit.setStyleSheet("border: none;")
_grp2_layout.addWidget(self.osc_addr_out_edit)
self.cc_output_lbl = QLabel(self._osc_output_label("--"))
self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
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)
# Group: Hardware — optional physical MIDI CC control alongside the
# app/OSC control (e.g. a JL Cooper button), positioned like grp_remote
# on the regular widgets rather than up with the OSC learn section.
# "Hardware" binds a hw CC/note that can trigger this toggle; independently,
# "Feedback" sends a translated CC back out on every state change (from
# hardware, OSC feedback, or the app) so hardware LEDs/motors stay in
# sync — separate switches since you may want one without the other.
self.hw_cc = None
self.hw_channel = None
self.is_learning_hw = False
self.grp_hardware = QFrame()
self.grp_hardware.setStyleSheet(_grp_style)
_grp_hw_layout = QVBoxLayout(self.grp_hardware)
_grp_hw_layout.setContentsMargins(4, 4, 4, 4)
_grp_hw_layout.setSpacing(3)
self.hardware_checkbox = QCheckBox("Hardware")
self.hardware_checkbox.setChecked(False)
self.hardware_checkbox.stateChanged.connect(lambda _: self._update_hw_visibility())
_grp_hw_layout.addWidget(self.hardware_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
self.hw_learn_btn = QPushButton("Learn")
self.hw_learn_btn.setFixedHeight(20)
self.hw_learn_btn.setStyleSheet("border: none;")
_grp_hw_layout.addWidget(self.hw_learn_btn)
self.hw_cc_input_lbl = QLabel("")
self.hw_cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.hw_cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;")
self.hw_cc_input_lbl.setFixedHeight(16)
_grp_hw_layout.addWidget(self.hw_cc_input_lbl)
self.feedback_checkbox = QCheckBox("Feedback")
self.feedback_checkbox.setChecked(False)
self.feedback_checkbox.stateChanged.connect(lambda _: self._update_hw_visibility())
_grp_hw_layout.addWidget(self.feedback_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
self.hw_cc_spin = QSpinBox()
self.hw_cc_spin.setMinimum(0)
self.hw_cc_spin.setMaximum(127)
self.hw_cc_spin.setValue(7)
self.hw_cc_spin.setFixedWidth(55)
_hw_cc_lbl = QLabel("CC")
_hw_cc_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;")
self.hw_cc_spin_row = QWidget()
_hbox_hw_cc = QHBoxLayout(self.hw_cc_spin_row)
_hbox_hw_cc.setContentsMargins(0, 0, 0, 0)
_hbox_hw_cc.setSpacing(4)
_hbox_hw_cc.addWidget(_hw_cc_lbl)
_hbox_hw_cc.addWidget(self.hw_cc_spin)
_grp_hw_layout.addWidget(self.hw_cc_spin_row)
self.hw_ch_spin = QSpinBox()
self.hw_ch_spin.setMinimum(1)
self.hw_ch_spin.setMaximum(16)
self.hw_ch_spin.setValue(1)
self.hw_ch_spin.setFixedWidth(55)
_hw_ch_lbl = QLabel("CH")
_hw_ch_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;")
self.hw_ch_spin_row = QWidget()
_hbox_hw_ch = QHBoxLayout(self.hw_ch_spin_row)
_hbox_hw_ch.setContentsMargins(0, 0, 0, 0)
_hbox_hw_ch.setSpacing(4)
_hbox_hw_ch.addWidget(_hw_ch_lbl)
_hbox_hw_ch.addWidget(self.hw_ch_spin)
_grp_hw_layout.addWidget(self.hw_ch_spin_row)
self.hw_cc_output_lbl = QLabel(f"CC{self.hw_cc_spin.value()} CH{self.hw_ch_spin.value()} [--]")
self.hw_cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.hw_cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;")
self.hw_cc_output_lbl.setFixedHeight(16)
_grp_hw_layout.addWidget(self.hw_cc_output_lbl)
inner.addWidget(self.grp_hardware)
# Toggle button with LED dot overlay
btn_container = QWidget()
btn_container.setFixedSize(60, 60)
self.btn = QPushButton("", btn_container)
self.btn.setFixedSize(60, 60)
self.btn.clicked.connect(self.on_click)
self.led_dot = QLabel(btn_container)
self.led_dot.setFixedSize(6, 6)
self.led_dot.move(5, 49)
self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")
inner.addWidget(btn_container, alignment=Qt.AlignmentFlag.AlignHCenter)
# Scribble strip
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
toggle_btn_row = QHBoxLayout()
toggle_btn_row.setContentsMargins(0, 0, 0, 0)
toggle_btn_row.setSpacing(2)
self.minus_btn = QPushButton("-")
self.minus_btn.setFixedSize(30, 20)
self.plus_btn = QPushButton("+")
self.plus_btn.setFixedSize(30, 20)
toggle_btn_row.addWidget(self.minus_btn)
toggle_btn_row.addStretch()
toggle_btn_row.addWidget(self.plus_btn)
inner.addLayout(toggle_btn_row)
# Zone button
self.zone_btn = ZoneButton()
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
self.zone_btn.right_callback = lambda active: self.on_zone_checked("right", active)
inner.addWidget(self.zone_btn)
# Separator before ID label
id_sep = QWidget()
id_sep.setFixedHeight(1)
id_sep.setStyleSheet("background-color: #3a3a3a;")
inner.addWidget(id_sep)
# Channel ID label
self.id_label = QLabel("")
self.id_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.id_label.setStyleSheet("color: #ffffff; font-size: 13px; font-weight: bold; background: transparent;")
inner.addWidget(self.id_label)
outer.addWidget(self.container)
self.on_select_callback = None
self.on_context_menu_callback = None
self._update_hw_visibility()
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_hw_visibility(self):
hw_on = self.hardware_checkbox.isChecked()
self.hw_learn_btn.setVisible(hw_on)
self.hw_cc_input_lbl.setVisible(hw_on)
fb_on = self.feedback_checkbox.isChecked()
self.hw_cc_spin_row.setVisible(fb_on)
self.hw_ch_spin_row.setVisible(fb_on)
self.hw_cc_output_lbl.setVisible(fb_on)
def start_hw_learn(self):
self.is_learning_hw = True
self.hw_learn_btn.setText("Listening...")
self.hw_learn_btn.setStyleSheet("color: orange; font-weight: bold;")
def stop_hw_learn(self):
self.is_learning_hw = False
self.hw_learn_btn.setStyleSheet("")
if self.hw_cc is not None:
self.hw_learn_btn.setText(f"HW: CC{self.hw_cc}")
self.hw_cc_input_lbl.setText(self._hw_cc_input_label())
self.hw_cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.hw_learn_btn.setText("Learn")
self.hw_cc_input_lbl.setText("")
self.hw_cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
def bind_hw_cc(self, cc_number, channel=None):
self.hw_cc = cc_number
self.hw_channel = channel
self.stop_hw_learn()
def _hw_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 _send_hw_feedback(self, value):
"""Sends a translated MIDI CC out (e.g. so a hardware button's LED
tracks this toggle's state), independent of whether hardware CC input
is also enabled."""
if not self.feedback_checkbox.isChecked():
return
out_cc = self.hw_cc_spin.value()
out_ch = self.hw_ch_spin.value()
send_cc(out_cc, value, channel=out_ch)
self.hw_cc_output_lbl.setText(f"CC{out_cc} CH{out_ch} [{value}]")
def receive_osc_value(self, value):
"""Update the toggle to reflect feedback from the DAW, without echoing it back out."""
self.toggle_state = value >= 0.5
self.cc_input_lbl.setText(f"[{value:.2f}]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;")
self._update_btn_style()
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 update_id_label(self, zone, index):
if zone == "left":
self.id_label.setText(f"L{index + 1}")
elif zone == "right":
self.id_label.setText(f"R{index + 1}")
else:
self.id_label.setText(f"{index + 1}")
def on_zone_checked(self, zone, active):
if active:
self.zone = zone
else:
self.zone = None
if self.on_zone_change_callback:
self.on_zone_change_callback(self)
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)
self._update_btn_style()
def set_trigger_mode(self, mode):
self.trigger_mode = mode
if mode == "momentary":
self.momentary_btn.setChecked(True)
self.toggle_mode_btn.setChecked(False)
self.momentary_btn.setStyleSheet("font-weight: bold; color: #00e676;")
self.toggle_mode_btn.setStyleSheet("")
else:
self.toggle_mode_btn.setChecked(True)
self.momentary_btn.setChecked(False)
self.toggle_mode_btn.setStyleSheet("font-weight: bold; color: #00e676;")
self.momentary_btn.setStyleSheet("")
def on_click(self):
addr = self.osc_addr_out_edit.text().strip() or "/track/mute"
if self.trigger_mode == "momentary":
send_osc_message(addr, 1.0)
self.cc_output_lbl.setText(self._osc_output_label(1.0))
self.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
QTimer.singleShot(150, lambda: self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
self._send_hw_feedback(127)
else:
self.toggle_state = not self.toggle_state
value = 1.0 if self.toggle_state else 0.0
send_osc_message(addr, value)
self.cc_output_lbl.setText(self._osc_output_label(value))
self._update_btn_style()
self._send_hw_feedback(127 if self.toggle_state else 0)
def _update_btn_style(self):
if self.toggle_state:
self.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
else:
self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")
def _osc_output_label(self, value="--"):
addr = self.osc_addr_out_edit.text().strip() or "/track/mute"
if isinstance(value, (int, float)):
return f"{addr} [{value:.1f}]"
return f"{addr} [{value}]"
def get_state(self):
return {
"uid": self.uid,
"label": self.title_edit.text(),
"state": self.toggle_state,
"color": self.color_name,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index,
"trigger_mode": self.trigger_mode,
"osc_addr_in": self.osc_addr_in_edit.text(),
"osc_addr_out": self.osc_addr_out_edit.text(),
"hardware_enabled": self.hardware_checkbox.isChecked(),
"feedback_enabled": self.feedback_checkbox.isChecked(),
"hw_cc": self.hw_cc,
"hw_channel": self.hw_channel,
"hw_out_cc": self.hw_cc_spin.value(),
"hw_out_ch": self.hw_ch_spin.value(),
}
def set_state(self, state):
if "uid" in state:
self.uid = state["uid"]
self.title_edit.setText(state.get("label", ""))
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.toggle_state = state.get("state", False)
self._update_btn_style()
self.cc_output_lbl.setText(self._osc_output_label(1.0 if self.toggle_state else 0.0))
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.set_trigger_mode(state.get("trigger_mode", "toggle"))
self.osc_addr_in_edit.setText(state.get("osc_addr_in", "/track/mute"))
self.osc_addr_out_edit.setText(state.get("osc_addr_out", "/track/mute"))
self.hw_cc = state.get("hw_cc", None)
self.hw_channel = state.get("hw_channel", None)
self.hw_cc_spin.setValue(state.get("hw_out_cc", 7))
self.hw_ch_spin.setValue(state.get("hw_out_ch", 1))
self.stop_hw_learn()
self.hardware_checkbox.setChecked(state.get("hardware_enabled", False))
self.feedback_checkbox.setChecked(state.get("feedback_enabled", False))
self._update_hw_visibility()