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;")