From d8f4cbcdd4680bef52731fdb30c511af6b21b58c Mon Sep 17 00:00:00 2001 From: Paul Lipscomb Date: Mon, 29 Jun 2026 18:49:50 -0400 Subject: [PATCH] add Mom/Tog to ToggleWidget, Big Title, Layout panel scaffold - ToggleWidget: add Momentary/Toggle mode selector (mirrors TransportWidget minus OSC); HW routing respects trigger_mode; fix invisible button on init by removing explicit empty stylesheet; saves/loads trigger_mode per preset - Big Title: full-width 28px bold QLineEdit above fader row, saves per-preset as big_title; green underline on focus; single bottom border, no separator - Layout panel: floating 220px window to the left of main window, toggled by new Layout button next to Messages; shows Sections checkboxes (Preset Browser, Big Title, Fader Row, Toggle Row, Transport Row); tracks window move/resize; checkboxes not yet wired to functionality Co-Authored-By: Claude Sonnet 4.6 --- main.py | 118 +++++++++++++++++++++++++++++++++++++++---- presets/presets.json | 27 ++++++++-- togglewidget.py | 58 ++++++++++++++++++--- 3 files changed, 179 insertions(+), 24 deletions(-) diff --git a/main.py b/main.py index bbdf54e..c44a35e 100644 --- a/main.py +++ b/main.py @@ -113,6 +113,7 @@ view_uuid_error_overlay = None label_overlay_message = None placeholder_fader_empty = None placeholder_toggle_empty = None +big_title_edit = None @@ -346,6 +347,11 @@ button_osc_messages_log = QPushButton("Messages") button_osc_messages_log.setFixedWidth(80) button_osc_messages_log.setCheckable(True) +# Widget: button_layout_panel +button_layout_panel = QPushButton("Layout") +button_layout_panel.setFixedWidth(65) +button_layout_panel.setCheckable(True) + # Layout: hstack_osc_status hstack_osc_status = QHBoxLayout() @@ -553,6 +559,54 @@ textedit_midi_out_log.setStyleSheet(""" } """) +# MARK: LAYOUT PANEL +container_layout_panel = QWidget(None, Qt.WindowType.Window) +container_layout_panel.setWindowTitle("Layout") +container_layout_panel.setStyleSheet("background-color: #1a1a1a;") +container_layout_panel.setFixedWidth(220) + +vstack_layout_panel = QVBoxLayout(container_layout_panel) +vstack_layout_panel.setContentsMargins(10, 10, 10, 10) +vstack_layout_panel.setSpacing(6) + +label_layout_panel_title = QLabel("Sections") +label_layout_panel_title.setStyleSheet("color: #ffffff; font-weight: bold; font-size: 12px;") +vstack_layout_panel.addWidget(label_layout_panel_title) + +_section_sep = QFrame() +_section_sep.setFrameShape(QFrame.Shape.HLine) +_section_sep.setStyleSheet("color: #3a3a3a;") +vstack_layout_panel.addWidget(_section_sep) + +# Section rows (layout only — not wired yet) +_section_rows = [ + ("Preset Browser", "chk_section_preset_browser"), + ("Big Title", "chk_section_big_title"), + ("Fader Row", "chk_section_fader_row"), + ("Toggle Row", "chk_section_toggle_row"), + ("Transport Row", "chk_section_transport_row"), +] +from PyQt6.QtWidgets import QCheckBox +for _lbl, _attr in _section_rows: + _row = QWidget() + _row.setStyleSheet("background: transparent;") + _hbox = QHBoxLayout(_row) + _hbox.setContentsMargins(4, 2, 4, 2) + _hbox.setSpacing(8) + _chk = QCheckBox() + _chk.setChecked(True) + _chk.setFixedSize(16, 16) + _name_lbl = QLabel(_lbl) + _name_lbl.setStyleSheet("color: #d4d4d4; font-size: 12px; background: transparent;") + _hbox.addWidget(_chk) + _hbox.addWidget(_name_lbl) + _hbox.addStretch() + globals()[_attr] = _chk + vstack_layout_panel.addWidget(_row) +del _lbl, _attr, _row, _hbox, _chk, _name_lbl, _section_rows + +vstack_layout_panel.addStretch() + # MARK: PRESET BROWSER ROW MODULE label_preset_browser_title = QLabel("Preset Browser") hstack_preset_browser = QHBoxLayout() @@ -931,6 +985,7 @@ class MainWindow(QMainWindow): # build hstack for control buttons hstack_osc_buttons.addWidget(button_osc_toggle) hstack_osc_buttons.addWidget(button_osc_messages_log) + hstack_osc_buttons.addWidget(button_layout_panel) hstack_osc_buttons.addStretch() # add all rows to vstack @@ -944,6 +999,7 @@ class MainWindow(QMainWindow): spinner_osc_send_port.valueChanged.connect(update_osc_server_port) button_osc_toggle.clicked.connect(start_stop_osc_server) button_osc_messages_log.clicked.connect(log_ui_show_hide) + button_layout_panel.clicked.connect(layout_panel_show_hide) # add the combined stack to the row hstack_io_row.addLayout(vstack_osc) @@ -1061,6 +1117,9 @@ class MainWindow(QMainWindow): button_midi_out_clear.clicked.connect(lambda: textedit_midi_out_log.clear()) container_midi_out_log.setVisible(False) + #MARK: LAYOUT PANEL + container_layout_panel.setVisible(False) + main_layout.addWidget(make_separator()) # MARK: TRANSPORT ROW CONTROLS @@ -1122,12 +1181,22 @@ class MainWindow(QMainWindow): global container_rows_and_widgets global view_uuid_error_overlay global label_overlay_message + global big_title_edit container_rows_and_widgets = QWidget() controls_layout = QVBoxLayout(container_rows_and_widgets) controls_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(container_rows_and_widgets) - controls_layout.addWidget(make_separator()) + # MARK: BIG TITLE + big_title_edit = QLineEdit("") + big_title_edit.setPlaceholderText("Title") + big_title_edit.setAlignment(Qt.AlignmentFlag.AlignCenter) + big_title_edit.setStyleSheet( + "QLineEdit { background: transparent; border: none; border-bottom: 1px solid #3a3a3a;" + " color: #ffffff; font-size: 28px; font-weight: bold; padding: 4px 0; }" + "QLineEdit:focus { border-bottom: 1px solid #00e676; }" + ) + controls_layout.addWidget(big_title_edit) # MARK: FADER ROW CONTROLS global hstack_row_fader_control_buttons @@ -1288,11 +1357,15 @@ class MainWindow(QMainWindow): super().resizeEvent(event) if container_osc_in_log.isVisible(): log_ui_window_calc() + if container_layout_panel.isVisible(): + layout_panel_window_calc() def moveEvent(self, event): super().moveEvent(event) if container_osc_in_log.isVisible(): log_ui_window_calc() + if container_layout_panel.isVisible(): + layout_panel_window_calc() def make_separator(): sep = QWidget() @@ -1347,12 +1420,13 @@ def new_blank_preset(): if not ok or not name.strip(): return name = name.strip() - 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["presets"][name] = {"faders": [], "toggles": [], "preset_uuid": "", "browser": {"back": preset_browser_back.get_state(), "fwd": preset_browser_fwd.get_state()}, "show_hide": {}, "big_title": ""} loaded_presets["__last_used__"] = name save_presets_file(loaded_presets) populate_preset_dropdown_menu() menuPresets.setCurrentText(name) clear_all_strips() + big_title_edit.setText("") show_save_status() def show_save_status(): @@ -1904,6 +1978,7 @@ def preset_build_snapshot(name): "back": preset_browser_back.get_state(), "fwd": preset_browser_fwd.get_state(), }, + "big_title": big_title_edit.text(), "preset_uuid": label_preset_uuid.text().strip(), "show_hide": { "fader": fader_show_hide_config, @@ -2024,6 +2099,7 @@ def preset_load(preset): 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)) + big_title_edit.setText(preset.get("big_title", "")) label_preset_uuid.setText(preset.get("preset_uuid", "")) check_and_index_strip_labels() @@ -2249,15 +2325,21 @@ def midi_message_route_hw_to_output_cc(fader_uid, value): if toggle.uid == fader_uid: out_cc = toggle.cc_spin.value() out_ch = toggle.ch_spin.value() - print(f"[toggle convert] HW CC{toggle.hw_cc} CH{toggle.hw_channel} ({value}) → CC{out_cc} CH{out_ch} ({value})") - should_be_on = value > 63 - if toggle.toggle_state != should_be_on: - toggle.toggle_state = should_be_on - out_val = 127 if should_be_on else 0 - send_cc(out_cc, out_val, channel=out_ch) - toggle.cc_input_lbl.setText(toggle._cc_input_label(value)) - toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val)) - toggle._update_btn_style() + print(f"[toggle convert] HW CC{toggle.hw_cc} CH{toggle.hw_channel} ({value}) → CC{out_cc} CH{out_ch}") + toggle.cc_input_lbl.setText(toggle._cc_input_label(value)) + if toggle.trigger_mode == "momentary": + send_cc(out_cc, 127, channel=out_ch) + toggle.cc_output_lbl.setText(toggle._cc_output_label(127)) + toggle.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;") + QTimer.singleShot(150, lambda: toggle.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")) + else: + should_be_on = value > 63 + if toggle.toggle_state != should_be_on: + toggle.toggle_state = should_be_on + out_val = 127 if should_be_on else 0 + send_cc(out_cc, out_val, channel=out_ch) + toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val)) + toggle._update_btn_style() return for t in transports: if t.uid == fader_uid: @@ -2312,6 +2394,20 @@ def log_ui_window_calc(): container_midi_out_log.setFixedHeight(half) container_midi_out_log.move(geo.right() + 368, geo.top() + half + 4) + +def layout_panel_show_hide(checked): + container_layout_panel.setVisible(checked) + if checked: + layout_panel_window_calc() + + +def layout_panel_window_calc(): + geo = app_window.frameGeometry() + container_layout_panel.setFixedWidth(220) + container_layout_panel.setFixedHeight(geo.height()) + container_layout_panel.move(geo.left() - 224, geo.top()) + + def start_osc_server(port=9000): global _osc_server, _osc_thread stop_osc_server() diff --git a/presets/presets.json b/presets/presets.json index 68f2af5..a7e97e4 100644 --- a/presets/presets.json +++ b/presets/presets.json @@ -1,5 +1,5 @@ { - "__last_used__": "preset3", + "__last_used__": "preset5", "presets": { "preset1": { "faders": [ @@ -244,7 +244,22 @@ "zone_index": -1 } ], - "toggles": [], + "toggles": [ + { + "uid": "e291394c-cc09-4d3c-9ac3-ed5fe2803a46", + "label": "Toggle 1", + "cc": 20, + "ch": 1, + "state": false, + "color": "Gray", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "toggle" + } + ], "browser": { "back": { "label": "Back", @@ -398,7 +413,8 @@ "center_index": null, "zone_index": null, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" } ], "browser": { @@ -419,6 +435,7 @@ "trigger_mode": "momentary" } }, + "big_title": "STRING SHORTS", "preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959", "show_hide": { "fader": false, @@ -510,10 +527,10 @@ "trigger_mode": "momentary" } ], - "transport_show_hide": false, + "transport_show_hide": true, "io_config": { "midi_out": "IAC Driver Bus 5", - "midi_in": "VMPK Output", + "midi_in": "Not Connected", "transport_out": "Not Connected", "osc_ip": "127.0.0.1", "osc_listen_port": 9000, diff --git a/togglewidget.py b/togglewidget.py index ad3310a..971c3cb 100644 --- a/togglewidget.py +++ b/togglewidget.py @@ -4,7 +4,7 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy ) -from PyQt6.QtCore import Qt, QEvent +from PyQt6.QtCore import Qt, QEvent, QTimer from palette import PALETTE_HEX, PALETTE_VIVID from styles import TITLE_STYLE @@ -19,6 +19,7 @@ class ToggleWidget(QWidget): 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 @@ -108,6 +109,26 @@ class ToggleWidget(QWidget): _hbox_ch.addWidget(self.ch_spin) _grp2_layout.addWidget(self.ch_spin_row) + # 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.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;") @@ -122,7 +143,6 @@ class ToggleWidget(QWidget): self.btn = QPushButton("", btn_container) self.btn.setFixedSize(60, 60) - self.btn.setStyleSheet("") self.btn.clicked.connect(self.on_click) self.led_dot = QLabel(btn_container) @@ -289,15 +309,35 @@ class ToggleWidget(QWidget): 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): - self.toggle_state = not self.toggle_state cc_num = self.cc_spin.value() out_ch = self.ch_spin.value() - value = 127 if self.toggle_state else 0 - print(f"[toggle] CC{cc_num} CH{out_ch} ({value})") - send_cc(cc_num, value, channel=out_ch) - self.cc_output_lbl.setText(self._cc_output_label(value)) - self._update_btn_style() + if self.trigger_mode == "momentary": + print(f"[toggle momentary] CC{cc_num} CH{out_ch} (127)") + send_cc(cc_num, 127, channel=out_ch) + self.cc_output_lbl.setText(self._cc_output_label(127)) + self.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;") + QTimer.singleShot(150, lambda: self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")) + else: + self.toggle_state = not self.toggle_state + value = 127 if self.toggle_state else 0 + print(f"[toggle] CC{cc_num} CH{out_ch} ({value})") + send_cc(cc_num, value, channel=out_ch) + self.cc_output_lbl.setText(self._cc_output_label(value)) + self._update_btn_style() def _update_btn_style(self): if self.toggle_state: @@ -318,6 +358,7 @@ class ToggleWidget(QWidget): "zone_index": self.zone_index, "hw_cc": self.hw_cc, "hw_channel": self.hw_channel, + "trigger_mode": self.trigger_mode, } def set_state(self, state): @@ -341,6 +382,7 @@ class ToggleWidget(QWidget): self.zone_btn.set_state(self.zone == "left", self.zone == "right") self.hw_cc = state.get("hw_cc", None) self.hw_channel = state.get("hw_channel", None) + self.set_trigger_mode(state.get("trigger_mode", "toggle")) 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())