preset browser widget / lock preset

This commit is contained in:
Paul Lipscomb
2026-06-29 01:49:39 -04:00
parent 0a5cfa88f1
commit 0aad488db5
3 changed files with 392 additions and 63 deletions
+90 -58
View File
@@ -19,6 +19,7 @@ import threading
from togglewidget import ToggleWidget from togglewidget import ToggleWidget
from faderwidget import FaderWidget from faderwidget import FaderWidget
from transportwidget import TransportWidget from transportwidget import TransportWidget
from presetwidget import PresetWidget
from palette import ( from palette import (
PALETTE, PALETTE,
@@ -100,6 +101,7 @@ midi_in_port = None
fader_show_hide_config = False fader_show_hide_config = False
transport_show_hide_config = False transport_show_hide_config = False
toggle_show_hide_config = False toggle_show_hide_config = False
preset_switch_locked = False
uuid_presets_matched = [] uuid_presets_matched = []
uuid_presets_received_values = [] uuid_presets_received_values = []
@@ -163,6 +165,14 @@ buttonImport = QPushButton("Import")
buttonImport.setFixedWidth(55) buttonImport.setFixedWidth(55)
buttonImport.clicked.connect(lambda: import_preset()) 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 #save status label (should be used for other stuff as well) -> save_status_label
labelStatus = QLabel("") labelStatus = QLabel("")
labelStatus.setStyleSheet("color: #00e676; font-style: italic;") labelStatus.setStyleSheet("color: #00e676; font-style: italic;")
@@ -488,42 +498,16 @@ textedit_osc_out_log.setStyleSheet("""
""") """)
# MARK: PRESET BROWSER ROW MODULE # 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") label_preset_browser_title = QLabel("Preset Browser")
# Layout: hstack_preset_browser
hstack_preset_browser = QHBoxLayout() hstack_preset_browser = QHBoxLayout()
hstack_preset_browser.setSpacing(8) hstack_preset_browser.setSpacing(8)
# Widget: button_preset_browser_back preset_browser_back = PresetWidget(label="Back", default_cc=22)
button_preset_browser_back = QPushButton("") preset_browser_fwd = PresetWidget(label="Fwd", default_cc=23)
button_preset_browser_back.setFixedSize(36, 28) preset_browsers = [preset_browser_back, preset_browser_fwd]
# Widget: spinner_preset_browser_back # show/hide state (mirrors fader/toggle pattern)
spinner_preset_browser_back = QSpinBox() preset_browser_show_hide_config = False
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)
# MARK: TRANSPORT MODULE # MARK: TRANSPORT MODULE
# REFACTOR MAP: Old names → New names # 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 = QHBoxLayout()
hstack_row_toggle_control_buttons.setSpacing(8) 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 # Widget: button_toggle_show_hide_config
button_toggle_show_hide_config = QPushButton("Hide Parameters") button_toggle_show_hide_config = QPushButton("Hide Parameters")
button_toggle_show_hide_config.setFixedWidth(120) 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(buttonRemove) #remove button
hstack_preset_row.addWidget(buttonExport) hstack_preset_row.addWidget(buttonExport)
hstack_preset_row.addWidget(buttonImport) hstack_preset_row.addWidget(buttonImport)
hstack_preset_row.addWidget(checkbox_lock_preset)
hstack_preset_row.addWidget(label_preset_uuid) hstack_preset_row.addWidget(label_preset_uuid)
hstack_preset_row.addWidget(button_preset_uuid_gen) hstack_preset_row.addWidget(button_preset_uuid_gen)
hstack_preset_row.addWidget(button_preset_uuid_copy) hstack_preset_row.addWidget(button_preset_uuid_copy)
@@ -1031,32 +1023,26 @@ class MainWindow(QMainWindow):
global container_preset_browser global container_preset_browser
global label_preset_browser_title global label_preset_browser_title
global hstack_preset_browser 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() container_preset_browser = QWidget()
vstack_preset_browser_container = QVBoxLayout(container_preset_browser) vstack_preset_browser_container = QVBoxLayout(container_preset_browser)
vstack_preset_browser_container.setContentsMargins(0, 0, 0, 0) vstack_preset_browser_container.setContentsMargins(0, 0, 0, 0)
# assemble browser controls hstack_preset_browser.addWidget(preset_browser_back)
hstack_preset_browser.addWidget(button_preset_browser_back) hstack_preset_browser.addWidget(preset_browser_fwd)
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.addStretch(1) 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(make_separator())
vstack_preset_browser_container.addWidget(label_preset_browser_title) 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) vstack_preset_browser_container.addLayout(hstack_preset_browser)
main_layout.addWidget(container_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) # MARK: CONTAINER ROWS AND WIDGETS (fader + toggle only)
global container_rows_and_widgets global container_rows_and_widgets
@@ -1285,7 +1271,7 @@ def new_blank_preset():
if not ok or not name.strip(): if not ok or not name.strip():
return return
name = name.strip() 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 loaded_presets["__last_used__"] = name
save_presets_file(loaded_presets) save_presets_file(loaded_presets)
populate_preset_dropdown_menu() populate_preset_dropdown_menu()
@@ -1449,7 +1435,8 @@ def _check_track_uuid_arrived():
_track_uuid_check_pending = False _track_uuid_check_pending = False
label_osc_status.setText("No preset linked to this track") label_osc_status.setText("No preset linked to this track")
label_osc_status.setStyleSheet("color: #f0c040; font-size: 11px; font-style: italic;") 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): def update_ui_on_osc_track_received(name):
global _track_uuid_check_pending global _track_uuid_check_pending
@@ -1816,14 +1803,15 @@ def preset_build_snapshot(name):
"faders": [f.get_state() for f in faders], "faders": [f.get_state() for f in faders],
"toggles": [t.get_state() for t in toggles], "toggles": [t.get_state() for t in toggles],
"browser": { "browser": {
"back_cc": spinner_preset_browser_back.value(), "back": preset_browser_back.get_state(),
"fwd_cc": spinner_preset_browser_fwd.value() "fwd": preset_browser_fwd.get_state(),
}, },
"preset_uuid": label_preset_uuid.text().strip(), "preset_uuid": label_preset_uuid.text().strip(),
"show_hide": { "show_hide": {
"fader": fader_show_hide_config, "fader": fader_show_hide_config,
"toggle": toggle_show_hide_config, "toggle": toggle_show_hide_config,
"transport": transport_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) hstack_toggle_zone_right.addWidget(t)
browser = preset.get("browser", {}) browser = preset.get("browser", {})
spinner_preset_browser_back.setValue(browser.get("back_cc", 22)) # migrate old format that only stored cc numbers
spinner_preset_browser_fwd.setValue(browser.get("fwd_cc", 23)) 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", {}) show_hide = preset.get("show_hide", {})
fader_show_hide_config_set(show_hide.get("fader", False)) 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.setText("Show Parameters" if show_hide.get("toggle", False) else "Hide Parameters")
button_toggle_show_hide_config.setChecked(show_hide.get("toggle", False)) 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", "")) label_preset_uuid.setText(preset.get("preset_uuid", ""))
check_and_index_strip_labels() check_and_index_strip_labels()
@@ -2012,22 +2010,25 @@ def resolve_uuid_batch():
global uuid_presets_matched, uuid_presets_received_values global uuid_presets_matched, uuid_presets_received_values
uuid_presets_matched = list(dict.fromkeys(uuid_presets_matched)) uuid_presets_matched = list(dict.fromkeys(uuid_presets_matched))
if len(uuid_presets_matched) == 1: if len(uuid_presets_matched) == 1:
menuPresets.setCurrentText(uuid_presets_matched[0])
track_name = label_osc_track.text() track_name = label_osc_track.text()
label_osc_preset.setText(uuid_presets_matched[0]) label_osc_preset.setText(uuid_presets_matched[0])
label_osc_status.setText(f"DAW Track Selected: {track_name}") label_osc_status.setText(f"DAW Track Selected: {track_name}")
label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;") 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: elif len(uuid_presets_matched) > 1:
label_osc_status.setText("MULTIPLE PRESET IDs DETECTED") label_osc_status.setText("MULTIPLE PRESET IDs DETECTED")
label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;") label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;")
ui_show_uuid_error_overlay("MULTIPLE PRESET IDs DETECTED", "#ff4444") if not preset_switch_locked:
QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; 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;")))
else: else:
label_osc_status.setText("NO MATCHING ID IN PRESET LIST") label_osc_status.setText("NO MATCHING ID IN PRESET LIST")
label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;") label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;")
ui_show_uuid_error_overlay("NO MATCHING ID IN PRESET LIST", "#ff4444") if not preset_switch_locked:
QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; 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;")))
uuid_presets_matched.clear() uuid_presets_matched.clear()
uuid_presets_received_values.clear() uuid_presets_received_values.clear()
@@ -2086,6 +2087,7 @@ def route_midi_message_input(event, data=None):
bound_fader = None bound_fader = None
bound_transport = None bound_transport = None
bound_browser = None
if status == 0xB0: if status == 0xB0:
for fader in faders: for fader in faders:
if fader.hw_cc == cc_num: 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: if hasattr(t, 'hw_cc') and t.hw_cc == cc_num:
bound_transport = t bound_transport = t
break 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: if status == 0xB0 and learning_strip:
learning_strip.bind_hw_cc(cc_num, channel=channel) learning_strip.bind_hw_cc(cc_num, channel=channel)
learning_strip = None learning_strip = None
return 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}") 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) midi_receiver.hw_cc_signal.emit(bound_transport.uid, cc_val)
elif bound_fader: 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.setText("Show Parameters" if enabled else "Hide Parameters")
button_toggle_show_hide_config.setChecked(enabled) 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 # VS Code dark theme palette
+44 -5
View File
@@ -1,5 +1,5 @@
{ {
"__last_used__": "preset1", "__last_used__": "preset5",
"presets": { "presets": {
"preset1": { "preset1": {
"faders": [ "faders": [
@@ -231,14 +231,29 @@
], ],
"toggles": [], "toggles": [],
"browser": { "browser": {
"back_cc": 22, "back": {
"fwd_cc": 23 "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", "preset_uuid": "e658b6bf-4bb7-4d68-bd8b-07775ee80c53",
"show_hide": { "show_hide": {
"fader": false, "fader": false,
"toggle": false, "toggle": false,
"transport": false "transport": true,
"browser": true
} }
}, },
"whatever": { "whatever": {
@@ -367,6 +382,30 @@
"toggle": false, "toggle": false,
"transport": 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": [ "transport_preset": [
@@ -427,7 +466,7 @@
"trigger_mode": "momentary" "trigger_mode": "momentary"
} }
], ],
"transport_show_hide": false, "transport_show_hide": true,
"io_config": { "io_config": {
"midi_out": "Not Connected", "midi_out": "Not Connected",
"midi_in": "VMPK Output", "midi_in": "VMPK Output",
+258
View File
@@ -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;")