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 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-06-29 18:49:50 -04:00
parent b68e5e3ac2
commit d8f4cbcdd4
3 changed files with 179 additions and 24 deletions
+100 -4
View File
@@ -113,6 +113,7 @@ view_uuid_error_overlay = None
label_overlay_message = None label_overlay_message = None
placeholder_fader_empty = None placeholder_fader_empty = None
placeholder_toggle_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.setFixedWidth(80)
button_osc_messages_log.setCheckable(True) 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 # Layout: hstack_osc_status
hstack_osc_status = QHBoxLayout() 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 # MARK: PRESET BROWSER ROW MODULE
label_preset_browser_title = QLabel("Preset Browser") label_preset_browser_title = QLabel("Preset Browser")
hstack_preset_browser = QHBoxLayout() hstack_preset_browser = QHBoxLayout()
@@ -931,6 +985,7 @@ class MainWindow(QMainWindow):
# build hstack for control buttons # build hstack for control buttons
hstack_osc_buttons.addWidget(button_osc_toggle) hstack_osc_buttons.addWidget(button_osc_toggle)
hstack_osc_buttons.addWidget(button_osc_messages_log) hstack_osc_buttons.addWidget(button_osc_messages_log)
hstack_osc_buttons.addWidget(button_layout_panel)
hstack_osc_buttons.addStretch() hstack_osc_buttons.addStretch()
# add all rows to vstack # add all rows to vstack
@@ -944,6 +999,7 @@ class MainWindow(QMainWindow):
spinner_osc_send_port.valueChanged.connect(update_osc_server_port) spinner_osc_send_port.valueChanged.connect(update_osc_server_port)
button_osc_toggle.clicked.connect(start_stop_osc_server) button_osc_toggle.clicked.connect(start_stop_osc_server)
button_osc_messages_log.clicked.connect(log_ui_show_hide) 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 # add the combined stack to the row
hstack_io_row.addLayout(vstack_osc) 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()) button_midi_out_clear.clicked.connect(lambda: textedit_midi_out_log.clear())
container_midi_out_log.setVisible(False) container_midi_out_log.setVisible(False)
#MARK: LAYOUT PANEL
container_layout_panel.setVisible(False)
main_layout.addWidget(make_separator()) main_layout.addWidget(make_separator())
# MARK: TRANSPORT ROW CONTROLS # MARK: TRANSPORT ROW CONTROLS
@@ -1122,12 +1181,22 @@ class MainWindow(QMainWindow):
global container_rows_and_widgets global container_rows_and_widgets
global view_uuid_error_overlay global view_uuid_error_overlay
global label_overlay_message global label_overlay_message
global big_title_edit
container_rows_and_widgets = QWidget() container_rows_and_widgets = QWidget()
controls_layout = QVBoxLayout(container_rows_and_widgets) controls_layout = QVBoxLayout(container_rows_and_widgets)
controls_layout.setContentsMargins(0, 0, 0, 0) controls_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(container_rows_and_widgets) 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 # MARK: FADER ROW CONTROLS
global hstack_row_fader_control_buttons global hstack_row_fader_control_buttons
@@ -1288,11 +1357,15 @@ class MainWindow(QMainWindow):
super().resizeEvent(event) super().resizeEvent(event)
if container_osc_in_log.isVisible(): if container_osc_in_log.isVisible():
log_ui_window_calc() log_ui_window_calc()
if container_layout_panel.isVisible():
layout_panel_window_calc()
def moveEvent(self, event): def moveEvent(self, event):
super().moveEvent(event) super().moveEvent(event)
if container_osc_in_log.isVisible(): if container_osc_in_log.isVisible():
log_ui_window_calc() log_ui_window_calc()
if container_layout_panel.isVisible():
layout_panel_window_calc()
def make_separator(): def make_separator():
sep = QWidget() sep = QWidget()
@@ -1347,12 +1420,13 @@ 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": {"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 loaded_presets["__last_used__"] = name
save_presets_file(loaded_presets) save_presets_file(loaded_presets)
populate_preset_dropdown_menu() populate_preset_dropdown_menu()
menuPresets.setCurrentText(name) menuPresets.setCurrentText(name)
clear_all_strips() clear_all_strips()
big_title_edit.setText("")
show_save_status() show_save_status()
def show_save_status(): def show_save_status():
@@ -1904,6 +1978,7 @@ def preset_build_snapshot(name):
"back": preset_browser_back.get_state(), "back": preset_browser_back.get_state(),
"fwd": preset_browser_fwd.get_state(), "fwd": preset_browser_fwd.get_state(),
}, },
"big_title": big_title_edit.text(),
"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,
@@ -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.setText("Show Parameters" if show_hide.get("browser", False) else "Hide Parameters")
button_preset_browser_show_hide_config.setChecked(show_hide.get("browser", False)) 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", "")) label_preset_uuid.setText(preset.get("preset_uuid", ""))
check_and_index_strip_labels() check_and_index_strip_labels()
@@ -2249,13 +2325,19 @@ def midi_message_route_hw_to_output_cc(fader_uid, value):
if toggle.uid == fader_uid: if toggle.uid == fader_uid:
out_cc = toggle.cc_spin.value() out_cc = toggle.cc_spin.value()
out_ch = toggle.ch_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})") 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 should_be_on = value > 63
if toggle.toggle_state != should_be_on: if toggle.toggle_state != should_be_on:
toggle.toggle_state = should_be_on toggle.toggle_state = should_be_on
out_val = 127 if should_be_on else 0 out_val = 127 if should_be_on else 0
send_cc(out_cc, out_val, channel=out_ch) 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.cc_output_lbl.setText(toggle._cc_output_label(out_val))
toggle._update_btn_style() toggle._update_btn_style()
return return
@@ -2312,6 +2394,20 @@ def log_ui_window_calc():
container_midi_out_log.setFixedHeight(half) container_midi_out_log.setFixedHeight(half)
container_midi_out_log.move(geo.right() + 368, geo.top() + half + 4) 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): def start_osc_server(port=9000):
global _osc_server, _osc_thread global _osc_server, _osc_thread
stop_osc_server() stop_osc_server()
+22 -5
View File
@@ -1,5 +1,5 @@
{ {
"__last_used__": "preset3", "__last_used__": "preset5",
"presets": { "presets": {
"preset1": { "preset1": {
"faders": [ "faders": [
@@ -244,7 +244,22 @@
"zone_index": -1 "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": { "browser": {
"back": { "back": {
"label": "Back", "label": "Back",
@@ -398,7 +413,8 @@
"center_index": null, "center_index": null,
"zone_index": null, "zone_index": null,
"hw_cc": null, "hw_cc": null,
"hw_channel": null "hw_channel": null,
"trigger_mode": "toggle"
} }
], ],
"browser": { "browser": {
@@ -419,6 +435,7 @@
"trigger_mode": "momentary" "trigger_mode": "momentary"
} }
}, },
"big_title": "STRING SHORTS",
"preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959", "preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959",
"show_hide": { "show_hide": {
"fader": false, "fader": false,
@@ -510,10 +527,10 @@
"trigger_mode": "momentary" "trigger_mode": "momentary"
} }
], ],
"transport_show_hide": false, "transport_show_hide": true,
"io_config": { "io_config": {
"midi_out": "IAC Driver Bus 5", "midi_out": "IAC Driver Bus 5",
"midi_in": "VMPK Output", "midi_in": "Not Connected",
"transport_out": "Not Connected", "transport_out": "Not Connected",
"osc_ip": "127.0.0.1", "osc_ip": "127.0.0.1",
"osc_listen_port": 9000, "osc_listen_port": 9000,
+45 -3
View File
@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy 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 palette import PALETTE_HEX, PALETTE_VIVID
from styles import TITLE_STYLE from styles import TITLE_STYLE
@@ -19,6 +19,7 @@ class ToggleWidget(QWidget):
self.setFixedWidth(115) self.setFixedWidth(115)
self.current_color = PALETTE_HEX.get("Gray", "#263238") self.current_color = PALETTE_HEX.get("Gray", "#263238")
self.toggle_state = False self.toggle_state = False
self.trigger_mode = "toggle"
self.uid = str(uuid.uuid4()) self.uid = str(uuid.uuid4())
self.zone = None self.zone = None
self.center_index = None self.center_index = None
@@ -108,6 +109,26 @@ class ToggleWidget(QWidget):
_hbox_ch.addWidget(self.ch_spin) _hbox_ch.addWidget(self.ch_spin)
_grp2_layout.addWidget(self.ch_spin_row) _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 = QLabel(f"CC{default_cc} [--]")
self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) 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.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 = QPushButton("", btn_container)
self.btn.setFixedSize(60, 60) self.btn.setFixedSize(60, 60)
self.btn.setStyleSheet("")
self.btn.clicked.connect(self.on_click) self.btn.clicked.connect(self.on_click)
self.led_dot = QLabel(btn_container) self.led_dot = QLabel(btn_container)
@@ -289,10 +309,30 @@ class ToggleWidget(QWidget):
self.apply_color(hex_color) self.apply_color(hex_color)
self._update_btn_style() 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): def on_click(self):
self.toggle_state = not self.toggle_state
cc_num = self.cc_spin.value() cc_num = self.cc_spin.value()
out_ch = self.ch_spin.value() out_ch = self.ch_spin.value()
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 value = 127 if self.toggle_state else 0
print(f"[toggle] CC{cc_num} CH{out_ch} ({value})") print(f"[toggle] CC{cc_num} CH{out_ch} ({value})")
send_cc(cc_num, value, channel=out_ch) send_cc(cc_num, value, channel=out_ch)
@@ -318,6 +358,7 @@ class ToggleWidget(QWidget):
"zone_index": self.zone_index, "zone_index": self.zone_index,
"hw_cc": self.hw_cc, "hw_cc": self.hw_cc,
"hw_channel": self.hw_channel, "hw_channel": self.hw_channel,
"trigger_mode": self.trigger_mode,
} }
def set_state(self, state): def set_state(self, state):
@@ -341,6 +382,7 @@ class ToggleWidget(QWidget):
self.zone_btn.set_state(self.zone == "left", self.zone == "right") self.zone_btn.set_state(self.zone == "left", self.zone == "right")
self.hw_cc = state.get("hw_cc", None) self.hw_cc = state.get("hw_cc", None)
self.hw_channel = state.get("hw_channel", 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: if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}") self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(self._cc_input_label()) self.cc_input_lbl.setText(self._cc_input_label())