From 0aad488db533b140d072c3e4bb5dceb9d339b5bc Mon Sep 17 00:00:00 2001 From: Paul Lipscomb Date: Mon, 29 Jun 2026 01:49:39 -0400 Subject: [PATCH] preset browser widget / lock preset --- main.py | 148 +++++++++++++++---------- presets/presets.json | 49 +++++++- presetwidget.py | 258 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 392 insertions(+), 63 deletions(-) create mode 100644 presetwidget.py diff --git a/main.py b/main.py index ee76e5c..4ceb1ef 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,7 @@ import threading from togglewidget import ToggleWidget from faderwidget import FaderWidget from transportwidget import TransportWidget +from presetwidget import PresetWidget from palette import ( PALETTE, @@ -100,6 +101,7 @@ midi_in_port = None fader_show_hide_config = False transport_show_hide_config = False toggle_show_hide_config = False +preset_switch_locked = False uuid_presets_matched = [] uuid_presets_received_values = [] @@ -163,6 +165,14 @@ buttonImport = QPushButton("Import") buttonImport.setFixedWidth(55) buttonImport.clicked.connect(lambda: import_preset()) +checkbox_lock_preset = QCheckBox("Lock Preset") +checkbox_lock_preset.setStyleSheet("color: #aaa; font-size: 11px;") +checkbox_lock_preset.setChecked(False) +def on_lock_preset_toggled(checked): + global preset_switch_locked + preset_switch_locked = checked +checkbox_lock_preset.toggled.connect(on_lock_preset_toggled) + #save status label (should be used for other stuff as well) -> save_status_label labelStatus = QLabel("") labelStatus.setStyleSheet("color: #00e676; font-style: italic;") @@ -488,42 +498,16 @@ textedit_osc_out_log.setStyleSheet(""" """) # MARK: PRESET BROWSER ROW MODULE -# REFACTOR MAP: Old names → New names -# self.browser_back_spin → spinner_preset_browser_back -# self.browser_fwd_spin → spinner_preset_browser_fwd - -# Widget: label_preset_browser_title label_preset_browser_title = QLabel("Preset Browser") - -# Layout: hstack_preset_browser hstack_preset_browser = QHBoxLayout() hstack_preset_browser.setSpacing(8) -# Widget: button_preset_browser_back -button_preset_browser_back = QPushButton("◀") -button_preset_browser_back.setFixedSize(36, 28) +preset_browser_back = PresetWidget(label="Back", default_cc=22) +preset_browser_fwd = PresetWidget(label="Fwd", default_cc=23) +preset_browsers = [preset_browser_back, preset_browser_fwd] -# Widget: spinner_preset_browser_back -spinner_preset_browser_back = QSpinBox() -spinner_preset_browser_back.setMinimum(1) -spinner_preset_browser_back.setMaximum(127) -spinner_preset_browser_back.setValue(22) -spinner_preset_browser_back.setFixedWidth(55) - -# Widget: button_preset_browser_fwd -button_preset_browser_fwd = QPushButton("▶") -button_preset_browser_fwd.setFixedSize(36, 28) - -# Widget: spinner_preset_browser_fwd -spinner_preset_browser_fwd = QSpinBox() -spinner_preset_browser_fwd.setMinimum(1) -spinner_preset_browser_fwd.setMaximum(127) -spinner_preset_browser_fwd.setValue(23) -spinner_preset_browser_fwd.setFixedWidth(55) - -# Widget: spacer_preset_browser -spacer_preset_browser = QWidget() -spacer_preset_browser.setFixedWidth(12) +# show/hide state (mirrors fader/toggle pattern) +preset_browser_show_hide_config = False # MARK: TRANSPORT MODULE # REFACTOR MAP: Old names → New names @@ -701,6 +685,13 @@ button_transport_show_hide_config.setStyleSheet(_show_hide_btn_style) hstack_row_toggle_control_buttons = QHBoxLayout() hstack_row_toggle_control_buttons.setSpacing(8) +# Widget: button_preset_browser_show_hide_config +button_preset_browser_show_hide_config = QPushButton("Hide Parameters") +button_preset_browser_show_hide_config.setFixedWidth(120) +button_preset_browser_show_hide_config.setCheckable(True) +button_preset_browser_show_hide_config.setFlat(True) +button_preset_browser_show_hide_config.setStyleSheet(_show_hide_btn_style) + # Widget: button_toggle_show_hide_config button_toggle_show_hide_config = QPushButton("Hide Parameters") button_toggle_show_hide_config.setFixedWidth(120) @@ -756,6 +747,7 @@ class MainWindow(QMainWindow): hstack_preset_row.addWidget(buttonRemove) #remove button hstack_preset_row.addWidget(buttonExport) hstack_preset_row.addWidget(buttonImport) + hstack_preset_row.addWidget(checkbox_lock_preset) hstack_preset_row.addWidget(label_preset_uuid) hstack_preset_row.addWidget(button_preset_uuid_gen) hstack_preset_row.addWidget(button_preset_uuid_copy) @@ -1031,32 +1023,26 @@ class MainWindow(QMainWindow): global container_preset_browser global label_preset_browser_title global hstack_preset_browser - global button_preset_browser_back - global spinner_preset_browser_back - global button_preset_browser_fwd - global spinner_preset_browser_fwd - global spacer_preset_browser container_preset_browser = QWidget() vstack_preset_browser_container = QVBoxLayout(container_preset_browser) vstack_preset_browser_container.setContentsMargins(0, 0, 0, 0) - # assemble browser controls - hstack_preset_browser.addWidget(button_preset_browser_back) - hstack_preset_browser.addWidget(spinner_preset_browser_back) - hstack_preset_browser.addWidget(spacer_preset_browser) - hstack_preset_browser.addWidget(button_preset_browser_fwd) - hstack_preset_browser.addWidget(spinner_preset_browser_fwd) + hstack_preset_browser.addWidget(preset_browser_back) + hstack_preset_browser.addWidget(preset_browser_fwd) hstack_preset_browser.addStretch(1) + hstack_preset_browser_controls = QHBoxLayout() + hstack_preset_browser_controls.addWidget(button_preset_browser_show_hide_config) + hstack_preset_browser_controls.addStretch() + button_preset_browser_show_hide_config.clicked.connect(preset_browser_show_hide_config_toggle) + vstack_preset_browser_container.addWidget(make_separator()) vstack_preset_browser_container.addWidget(label_preset_browser_title) + vstack_preset_browser_container.addLayout(hstack_preset_browser_controls) vstack_preset_browser_container.addLayout(hstack_preset_browser) main_layout.addWidget(container_preset_browser) - # connect signals - button_preset_browser_back.clicked.connect(lambda: send_cc(spinner_preset_browser_back.value(), 127)) - button_preset_browser_fwd.clicked.connect(lambda: send_cc(spinner_preset_browser_fwd.value(), 127)) # MARK: CONTAINER ROWS AND WIDGETS (fader + toggle only) global container_rows_and_widgets @@ -1285,7 +1271,7 @@ def new_blank_preset(): if not ok or not name.strip(): return name = name.strip() - loaded_presets["presets"][name] = {"faders": [], "toggles": [], "preset_uuid": "", "browser": {}, "show_hide": {}} + loaded_presets["presets"][name] = {"faders": [], "toggles": [], "preset_uuid": "", "browser": {"back": preset_browser_back.get_state(), "fwd": preset_browser_fwd.get_state()}, "show_hide": {}} loaded_presets["__last_used__"] = name save_presets_file(loaded_presets) populate_preset_dropdown_menu() @@ -1449,7 +1435,8 @@ def _check_track_uuid_arrived(): _track_uuid_check_pending = False label_osc_status.setText("No preset linked to this track") label_osc_status.setStyleSheet("color: #f0c040; font-size: 11px; font-style: italic;") - ui_show_uuid_error_overlay("No preset linked to this track", "#f0c040") + if not preset_switch_locked: + ui_show_uuid_error_overlay("No preset linked to this track", "#f0c040") def update_ui_on_osc_track_received(name): global _track_uuid_check_pending @@ -1816,14 +1803,15 @@ def preset_build_snapshot(name): "faders": [f.get_state() for f in faders], "toggles": [t.get_state() for t in toggles], "browser": { - "back_cc": spinner_preset_browser_back.value(), - "fwd_cc": spinner_preset_browser_fwd.value() + "back": preset_browser_back.get_state(), + "fwd": preset_browser_fwd.get_state(), }, "preset_uuid": label_preset_uuid.text().strip(), "show_hide": { "fader": fader_show_hide_config, "toggle": toggle_show_hide_config, "transport": transport_show_hide_config, + "browser": preset_browser_show_hide_config, } } @@ -1916,8 +1904,14 @@ def preset_load(preset): hstack_toggle_zone_right.addWidget(t) browser = preset.get("browser", {}) - spinner_preset_browser_back.setValue(browser.get("back_cc", 22)) - spinner_preset_browser_fwd.setValue(browser.get("fwd_cc", 23)) + # migrate old format that only stored cc numbers + if "back" not in browser and "back_cc" in browser: + browser = { + "back": {"label": "Back", "cc": browser.get("back_cc", 22), "ch": 1}, + "fwd": {"label": "Fwd", "cc": browser.get("fwd_cc", 23), "ch": 1}, + } + preset_browser_back.set_state(browser.get("back", {"label": "Back", "cc": 22, "ch": 1})) + preset_browser_fwd.set_state(browser.get("fwd", {"label": "Fwd", "cc": 23, "ch": 1})) show_hide = preset.get("show_hide", {}) fader_show_hide_config_set(show_hide.get("fader", False)) @@ -1928,6 +1922,10 @@ def preset_load(preset): button_toggle_show_hide_config.setText("Show Parameters" if show_hide.get("toggle", False) else "Hide Parameters") button_toggle_show_hide_config.setChecked(show_hide.get("toggle", False)) + preset_browser_show_hide_config_set(show_hide.get("browser", False)) + button_preset_browser_show_hide_config.setText("Show Parameters" if show_hide.get("browser", False) else "Hide Parameters") + button_preset_browser_show_hide_config.setChecked(show_hide.get("browser", False)) + label_preset_uuid.setText(preset.get("preset_uuid", "")) check_and_index_strip_labels() @@ -2012,22 +2010,25 @@ def resolve_uuid_batch(): global uuid_presets_matched, uuid_presets_received_values uuid_presets_matched = list(dict.fromkeys(uuid_presets_matched)) if len(uuid_presets_matched) == 1: - menuPresets.setCurrentText(uuid_presets_matched[0]) track_name = label_osc_track.text() label_osc_preset.setText(uuid_presets_matched[0]) label_osc_status.setText(f"DAW Track Selected: {track_name}") label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;") - ui_show_rows_and_widgets() + if not preset_switch_locked: + menuPresets.setCurrentText(uuid_presets_matched[0]) + ui_show_rows_and_widgets() elif len(uuid_presets_matched) > 1: label_osc_status.setText("MULTIPLE PRESET IDs DETECTED") label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;") - ui_show_uuid_error_overlay("MULTIPLE PRESET IDs DETECTED", "#ff4444") - QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;"))) + if not preset_switch_locked: + ui_show_uuid_error_overlay("MULTIPLE PRESET IDs DETECTED", "#ff4444") + QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;"))) else: label_osc_status.setText("NO MATCHING ID IN PRESET LIST") label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;") - ui_show_uuid_error_overlay("NO MATCHING ID IN PRESET LIST", "#ff4444") - QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;"))) + if not preset_switch_locked: + ui_show_uuid_error_overlay("NO MATCHING ID IN PRESET LIST", "#ff4444") + QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;"))) uuid_presets_matched.clear() uuid_presets_received_values.clear() @@ -2086,6 +2087,7 @@ def route_midi_message_input(event, data=None): bound_fader = None bound_transport = None + bound_browser = None if status == 0xB0: for fader in faders: if fader.hw_cc == cc_num: @@ -2101,13 +2103,20 @@ def route_midi_message_input(event, data=None): if hasattr(t, 'hw_cc') and t.hw_cc == cc_num: bound_transport = t break + if not bound_fader and not bound_transport: + for w in preset_browsers: + if w.hw_cc == cc_num: + bound_browser = w + break if status == 0xB0 and learning_strip: learning_strip.bind_hw_cc(cc_num, channel=channel) learning_strip = None return - if bound_transport: + if bound_browser: + bound_browser.on_hw_trigger(cc_val) + elif bound_transport: print(f"[transport hw] CC{cc_num} val={cc_val} → uid={bound_transport.uid}") midi_receiver.hw_cc_signal.emit(bound_transport.uid, cc_val) elif bound_fader: @@ -2333,7 +2342,30 @@ def toggle_show_hide_config_toggle(): button_toggle_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters") button_toggle_show_hide_config.setChecked(enabled) +# MARK: PRESET BROWSER SHOW HIDE CONFIG +def preset_browser_show_hide_config_set(enabled): + global preset_browser_show_hide_config + preset_browser_show_hide_config = enabled + for w in preset_browsers: + w.grp_learn.setVisible(not enabled) + w.arrow_lbl.setVisible(not enabled) + w.grp_cc.setVisible(not enabled) + w.title_edit.setVisible(not enabled) + w.momentary_btn.setVisible(not enabled) + w.toggle_mode_btn.setVisible(not enabled) + w.btn.setFixedHeight(24 if enabled else 40) + w.container.setStyleSheet( + "QFrame#presetContainer { background: transparent; border: none; }" + if enabled else + "QFrame#presetContainer { background-color: #4a5568; border-radius: 6px; border: 2px solid rgba(0,0,0,0.3); }" + ) +def preset_browser_show_hide_config_toggle(): + global preset_browser_show_hide_config + enabled = not preset_browser_show_hide_config + preset_browser_show_hide_config_set(enabled) + button_preset_browser_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters") + button_preset_browser_show_hide_config.setChecked(enabled) # VS Code dark theme palette diff --git a/presets/presets.json b/presets/presets.json index 4e97fe6..0b3d4e7 100644 --- a/presets/presets.json +++ b/presets/presets.json @@ -1,5 +1,5 @@ { - "__last_used__": "preset1", + "__last_used__": "preset5", "presets": { "preset1": { "faders": [ @@ -231,14 +231,29 @@ ], "toggles": [], "browser": { - "back_cc": 22, - "fwd_cc": 23 + "back": { + "label": "Back", + "cc": 22, + "ch": 1, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "momentary" + }, + "fwd": { + "label": "Fwd", + "cc": 23, + "ch": 1, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "momentary" + } }, "preset_uuid": "e658b6bf-4bb7-4d68-bd8b-07775ee80c53", "show_hide": { "fader": false, "toggle": false, - "transport": false + "transport": true, + "browser": true } }, "whatever": { @@ -367,6 +382,30 @@ "toggle": false, "transport": false } + }, + "new preset browser shit": { + "faders": [], + "toggles": [], + "preset_uuid": "", + "browser": { + "back": { + "label": "Back", + "cc": 22, + "ch": 1, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "momentary" + }, + "fwd": { + "label": "Fwd", + "cc": 23, + "ch": 1, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "momentary" + } + }, + "show_hide": {} } }, "transport_preset": [ @@ -427,7 +466,7 @@ "trigger_mode": "momentary" } ], - "transport_show_hide": false, + "transport_show_hide": true, "io_config": { "midi_out": "Not Connected", "midi_in": "VMPK Output", diff --git a/presetwidget.py b/presetwidget.py new file mode 100644 index 0000000..1839e5b --- /dev/null +++ b/presetwidget.py @@ -0,0 +1,258 @@ +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): + if value == 0: + return # ignore releases — normalized by mom/tog logic + 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;")