Files
virtual-controller/app-desktop-macos/presetwidget.py
T
Paul Lipscomb 7ecc718f5d 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>
2026-07-15 17:51:05 -04:00

257 lines
10 KiB
Python

from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt
from styles import TITLE_STYLE
from midi_sender import send_cc
class PresetWidget(QWidget):
def __init__(self, label="Preset", default_cc=1):
super().__init__()
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
self.setFixedWidth(115)
self.hw_cc = None
self.hw_channel = 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.container.setObjectName("presetContainer")
self.container.setStyleSheet("""
QFrame#presetContainer {
background-color: #4a5568;
border-radius: 6px;
border: 2px solid rgba(0,0,0,0.3);
}
""")
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 1: Learn + input readout
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)
self.learn_btn.clicked.connect(self.start_learn)
_grp1_layout.addWidget(self.learn_btn)
self.trigger_mode = "momentary"
self.toggle_state = False
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)
_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: Mom./Tog. + 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)
# Momentary / Toggle 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(True)
self.toggle_mode_btn = QPushButton("Tog.")
self.toggle_mode_btn.setFixedHeight(18)
self.toggle_mode_btn.setCheckable(True)
self.toggle_mode_btn.setChecked(False)
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.cc_spin = QSpinBox()
self.cc_spin.setMinimum(0)
self.cc_spin.setMaximum(127)
self.cc_spin.setValue(default_cc)
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.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; border: none;")
self.cc_output_lbl.setFixedHeight(16)
_grp2_layout.addWidget(self.cc_output_lbl)
inner.addWidget(self.grp_cc)
# Trigger button
self.btn = QPushButton(label)
self.btn.setFixedHeight(40)
self.btn.clicked.connect(self.on_click)
inner.addWidget(self.btn)
# Scribble strip
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)
outer.addWidget(self.container)
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="--"):
return f"CC{self.cc_spin.value()} CH{self.ch_spin.value()} [{vel}]"
def start_learn(self):
self.is_learning = True
self.learn_btn.setText("Listening...")
self.learn_btn.setStyleSheet("color: orange; font-weight: bold; border: none;")
def stop_learn(self):
self.is_learning = False
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(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; border: none;")
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):
from PyQt6.QtWidgets import QMenu
if self.hw_cc is None:
return
menu = QMenu(self)
clear_action = menu.addAction("Clear Input Assignment")
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("border: none;")
self.cc_input_lbl.setText("")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;")
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 _fire(self):
out_cc = self.cc_spin.value()
out_ch = self.ch_spin.value()
if self.trigger_mode == "momentary":
out_val = 127
else:
self.toggle_state = not self.toggle_state
out_val = 127 if self.toggle_state else 0
print(f"[preset] CC{out_cc} CH{out_ch} ({out_val})")
send_cc(out_cc, out_val, channel=out_ch)
self.cc_output_lbl.setText(self._cc_output_label(out_val))
def on_click(self):
self._fire()
def on_hw_trigger(self, value):
self.cc_input_lbl.setText(self._cc_input_label(value))
self._fire()
def get_state(self):
return {
"label": self.title_edit.text(),
"cc": self.cc_spin.value(),
"ch": self.ch_spin.value(),
"hw_cc": self.hw_cc,
"hw_channel": self.hw_channel,
"trigger_mode": self.trigger_mode,
}
def set_state(self, state):
self.title_edit.setText(state.get("label", self.title_edit.text()))
self.cc_spin.setValue(state.get("cc", 1))
self.ch_spin.setValue(state.get("ch", 1))
self.hw_cc = state.get("hw_cc", None)
self.hw_channel = state.get("hw_channel", None)
self.set_trigger_mode(state.get("trigger_mode", "momentary"))
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())
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; border: none;")