Multi-select, context menu, section strikethrough, rename/duplicate presets
- Multi-select strips with click, Ctrl+click (toggle), Shift+click (range, same zone) - Right-click selected strip: context menu with Cascade CC, Assign Channel, Deselect All - Right-click unselected strip: clears all selections - Selection status label shows count and type (faders/toggles/transport toggles) - Rename preset button added to preset row - Duplicate preset button auto-generates new UUID - Layout panel section buttons show strikethrough when hidden, normal when visible - Renamed Preset Browser to Instrument Browser in UI - Transport strips wired for multi-select and context menu Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-4
@@ -206,6 +206,7 @@ class FaderWidget(QWidget):
|
|||||||
self.fader.valueChanged.connect(self.on_fader_moved)
|
self.fader.valueChanged.connect(self.on_fader_moved)
|
||||||
self.cc_spin.valueChanged.connect(self.on_cc_changed)
|
self.cc_spin.valueChanged.connect(self.on_cc_changed)
|
||||||
self.on_select_callback = None
|
self.on_select_callback = None
|
||||||
|
self.on_context_menu_callback = None
|
||||||
self.hw_cc = None # bound hardware CC number
|
self.hw_cc = None # bound hardware CC number
|
||||||
self.is_learning = False
|
self.is_learning = False
|
||||||
self.zone = None # None, "left", or "right"
|
self.zone = None # None, "left", or "right"
|
||||||
@@ -219,15 +220,25 @@ class FaderWidget(QWidget):
|
|||||||
|
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
if event.type() == QEvent.Type.MouseButtonPress:
|
if event.type() == QEvent.Type.MouseButtonPress:
|
||||||
if self.on_select_callback:
|
if event.button() != Qt.MouseButton.RightButton:
|
||||||
self.on_select_callback(self)
|
if self.on_select_callback:
|
||||||
|
self.on_select_callback(self)
|
||||||
|
elif event.type() == QEvent.Type.ContextMenu:
|
||||||
|
if self.on_context_menu_callback:
|
||||||
|
self.on_context_menu_callback(self, event.globalPos())
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if self.on_select_callback:
|
if event.button() != Qt.MouseButton.RightButton:
|
||||||
self.on_select_callback(self)
|
if self.on_select_callback:
|
||||||
|
self.on_select_callback(self)
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event):
|
||||||
|
if self.on_context_menu_callback:
|
||||||
|
self.on_context_menu_callback(self, event.globalPos())
|
||||||
|
|
||||||
def on_pickup_changed(self, state):
|
def on_pickup_changed(self, state):
|
||||||
self.pickup_mode = bool(state)
|
self.pickup_mode = bool(state)
|
||||||
if self.pickup_mode:
|
if self.pickup_mode:
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ _osc_thread = None
|
|||||||
|
|
||||||
#MARK: GLOBALS
|
#MARK: GLOBALS
|
||||||
ready = False
|
ready = False
|
||||||
selected_strip = None
|
selected_strips = []
|
||||||
|
last_clicked_strip = None
|
||||||
learning_strip = None
|
learning_strip = None
|
||||||
faders = []
|
faders = []
|
||||||
strip_width = 100
|
strip_width = 100
|
||||||
@@ -588,8 +589,8 @@ vstack_layout_panel.addWidget(_section_sep)
|
|||||||
|
|
||||||
_section_btn_style = (
|
_section_btn_style = (
|
||||||
"QPushButton { background: transparent; border: none; color: #ffffff;"
|
"QPushButton { background: transparent; border: none; color: #ffffff;"
|
||||||
" font-size: 12px; font-weight: bold; text-align: left; padding: 4px 6px; }"
|
" font-size: 12px; font-weight: bold; text-align: left; padding: 4px 6px; text-decoration: none; }"
|
||||||
"QPushButton:!checked { color: #444444; font-weight: normal; }"
|
"QPushButton:!checked { color: #444444; font-weight: normal; text-decoration: line-through; }"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_section_row(label_text):
|
def _make_section_row(label_text):
|
||||||
@@ -600,7 +601,7 @@ def _make_section_row(label_text):
|
|||||||
btn.setStyleSheet(_section_btn_style)
|
btn.setStyleSheet(_section_btn_style)
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
chk_section_preset_browser = _make_section_row("Preset Browser")
|
chk_section_preset_browser = _make_section_row("Instrument Browser")
|
||||||
chk_section_big_title = _make_section_row("Big Title")
|
chk_section_big_title = _make_section_row("Big Title")
|
||||||
chk_section_fader_row = _make_section_row("Fader Row")
|
chk_section_fader_row = _make_section_row("Fader Row")
|
||||||
chk_section_toggle_row = _make_section_row("Toggle Row")
|
chk_section_toggle_row = _make_section_row("Toggle Row")
|
||||||
@@ -613,7 +614,7 @@ del _btn
|
|||||||
vstack_layout_panel.addStretch()
|
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("Instrument Browser")
|
||||||
hstack_preset_browser = QHBoxLayout()
|
hstack_preset_browser = QHBoxLayout()
|
||||||
hstack_preset_browser.setSpacing(8)
|
hstack_preset_browser.setSpacing(8)
|
||||||
|
|
||||||
@@ -672,6 +673,7 @@ for _label, _cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]
|
|||||||
_t.minus_btn.clicked.connect(lambda checked=False, t=_t: transport_remove(t))
|
_t.minus_btn.clicked.connect(lambda checked=False, t=_t: transport_remove(t))
|
||||||
_t.learn_btn.clicked.connect(lambda checked=False, t=_t: check_and_start_midi_learn(t))
|
_t.learn_btn.clicked.connect(lambda checked=False, t=_t: check_and_start_midi_learn(t))
|
||||||
_t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
|
_t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
|
||||||
|
_t.on_context_menu_callback = lambda strip, pos: strip_context_menu(strip, pos)
|
||||||
_t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
_t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
||||||
del _t, _label, _cc
|
del _t, _label, _cc
|
||||||
|
|
||||||
@@ -1775,13 +1777,105 @@ def check_and_index_strip_labels():
|
|||||||
separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in toggles))
|
separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in toggles))
|
||||||
separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in toggles))
|
separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in toggles))
|
||||||
|
|
||||||
|
def clear_all_selections():
|
||||||
|
global selected_strips, last_clicked_strip
|
||||||
|
for s in selected_strips:
|
||||||
|
s.set_selected(False)
|
||||||
|
selected_strips = []
|
||||||
|
last_clicked_strip = None
|
||||||
|
label_selected_channel.setText("")
|
||||||
|
|
||||||
|
def _get_zone_strips(strip):
|
||||||
|
if isinstance(strip, FaderWidget):
|
||||||
|
pool = faders
|
||||||
|
elif isinstance(strip, TransportWidget):
|
||||||
|
pool = transports
|
||||||
|
else:
|
||||||
|
pool = toggles
|
||||||
|
same_zone = [s for s in pool if s.zone == strip.zone]
|
||||||
|
if strip.zone is not None:
|
||||||
|
same_zone.sort(key=lambda s: s.zone_index or 0)
|
||||||
|
return same_zone
|
||||||
|
|
||||||
def check_and_update_selected_strip(strip):
|
def check_and_update_selected_strip(strip):
|
||||||
global selected_strip
|
global selected_strips, last_clicked_strip
|
||||||
if selected_strip and selected_strip != strip:
|
from PyQt6.QtWidgets import QApplication
|
||||||
selected_strip.set_selected(False)
|
mods = QApplication.queryKeyboardModifiers()
|
||||||
selected_strip = strip
|
ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier)
|
||||||
strip.set_selected(True)
|
shift = bool(mods & Qt.KeyboardModifier.ShiftModifier)
|
||||||
label_selected_channel.setText(f"Software item selected: {strip.title_edit.text()}")
|
|
||||||
|
if shift and last_clicked_strip and last_clicked_strip != strip:
|
||||||
|
if type(strip) is not type(last_clicked_strip):
|
||||||
|
return
|
||||||
|
if strip.zone != last_clicked_strip.zone:
|
||||||
|
return
|
||||||
|
zone_strips = _get_zone_strips(strip)
|
||||||
|
if last_clicked_strip not in zone_strips or strip not in zone_strips:
|
||||||
|
return
|
||||||
|
a, b = zone_strips.index(last_clicked_strip), zone_strips.index(strip)
|
||||||
|
new_range = zone_strips[min(a,b):max(a,b)+1]
|
||||||
|
for s in selected_strips:
|
||||||
|
s.set_selected(False)
|
||||||
|
selected_strips = new_range
|
||||||
|
for s in selected_strips:
|
||||||
|
s.set_selected(True)
|
||||||
|
elif ctrl:
|
||||||
|
if selected_strips:
|
||||||
|
if type(strip) is not type(selected_strips[0]):
|
||||||
|
return
|
||||||
|
if strip.zone != selected_strips[0].zone:
|
||||||
|
return
|
||||||
|
if strip in selected_strips:
|
||||||
|
selected_strips.remove(strip)
|
||||||
|
strip.set_selected(False)
|
||||||
|
else:
|
||||||
|
selected_strips.append(strip)
|
||||||
|
strip.set_selected(True)
|
||||||
|
last_clicked_strip = strip
|
||||||
|
else:
|
||||||
|
for s in selected_strips:
|
||||||
|
s.set_selected(False)
|
||||||
|
selected_strips = [strip]
|
||||||
|
strip.set_selected(True)
|
||||||
|
last_clicked_strip = strip
|
||||||
|
|
||||||
|
if selected_strips:
|
||||||
|
s = selected_strips[0]
|
||||||
|
if isinstance(s, FaderWidget):
|
||||||
|
kind = "fader" if len(selected_strips) == 1 else "faders"
|
||||||
|
elif isinstance(s, ToggleWidget):
|
||||||
|
kind = "toggle" if len(selected_strips) == 1 else "toggles"
|
||||||
|
elif isinstance(s, TransportWidget):
|
||||||
|
kind = "transport toggle" if len(selected_strips) == 1 else "transport toggles"
|
||||||
|
else:
|
||||||
|
kind = "strip" if len(selected_strips) == 1 else "strips"
|
||||||
|
label_selected_channel.setText(f"{len(selected_strips)} {kind} selected")
|
||||||
|
else:
|
||||||
|
label_selected_channel.setText("")
|
||||||
|
|
||||||
|
def strip_context_menu(strip, global_pos):
|
||||||
|
if strip not in selected_strips:
|
||||||
|
clear_all_selections()
|
||||||
|
return
|
||||||
|
from PyQt6.QtWidgets import QMenu, QInputDialog
|
||||||
|
menu = QMenu()
|
||||||
|
action_cascade = menu.addAction("Cascade CC")
|
||||||
|
action_channel = menu.addAction("Assign Channel")
|
||||||
|
menu.addSeparator()
|
||||||
|
action_deselect = menu.addAction("Deselect All")
|
||||||
|
action = menu.exec(global_pos)
|
||||||
|
if action == action_cascade:
|
||||||
|
start, ok = QInputDialog.getInt(app_window, "Cascade CC", "Starting CC:", 0, 0, 127)
|
||||||
|
if ok:
|
||||||
|
for i, s in enumerate(selected_strips):
|
||||||
|
s.cc_spin.setValue((start + i) % 128)
|
||||||
|
elif action == action_channel:
|
||||||
|
ch, ok = QInputDialog.getInt(app_window, "Assign Channel", "Channel (1–16):", 1, 1, 16)
|
||||||
|
if ok:
|
||||||
|
for s in selected_strips:
|
||||||
|
s.ch_spin.setValue(ch)
|
||||||
|
elif action == action_deselect:
|
||||||
|
clear_all_selections()
|
||||||
|
|
||||||
def handle_toggle_zone_change(toggle):
|
def handle_toggle_zone_change(toggle):
|
||||||
if toggle.zone is not None and toggle.center_index is None:
|
if toggle.zone is not None and toggle.center_index is None:
|
||||||
@@ -1904,6 +1998,7 @@ def toggle_add(insert_after=None):
|
|||||||
toggle.plus_btn.clicked.connect(lambda: toggle_add(insert_after=toggle))
|
toggle.plus_btn.clicked.connect(lambda: toggle_add(insert_after=toggle))
|
||||||
toggle.minus_btn.clicked.connect(lambda: toggle_remove(toggle))
|
toggle.minus_btn.clicked.connect(lambda: toggle_remove(toggle))
|
||||||
toggle.on_select_callback = check_and_update_selected_strip
|
toggle.on_select_callback = check_and_update_selected_strip
|
||||||
|
toggle.on_context_menu_callback = strip_context_menu
|
||||||
toggle.on_zone_change_callback = handle_toggle_zone_change
|
toggle.on_zone_change_callback = handle_toggle_zone_change
|
||||||
toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle))
|
toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle))
|
||||||
update_window_width()
|
update_window_width()
|
||||||
@@ -1911,12 +2006,11 @@ def toggle_add(insert_after=None):
|
|||||||
_update_toggle_placeholder()
|
_update_toggle_placeholder()
|
||||||
|
|
||||||
def toggle_remove(toggle):
|
def toggle_remove(toggle):
|
||||||
global selected_strip
|
|
||||||
if len(toggles) <= 0:
|
if len(toggles) <= 0:
|
||||||
return
|
return
|
||||||
if selected_strip == toggle:
|
if toggle in selected_strips:
|
||||||
selected_strip = None
|
clear_all_selections()
|
||||||
label_selected_channel.setText("Software item selected: None")
|
label_selected_channel.setText("")
|
||||||
hstack_toggle_center.removeWidget(toggle)
|
hstack_toggle_center.removeWidget(toggle)
|
||||||
toggle.deleteLater()
|
toggle.deleteLater()
|
||||||
toggles.remove(toggle)
|
toggles.remove(toggle)
|
||||||
@@ -1938,18 +2032,17 @@ def fader_add(insert_after=None):
|
|||||||
fader.minus_btn.clicked.connect(lambda: fader_remove(fader))
|
fader.minus_btn.clicked.connect(lambda: fader_remove(fader))
|
||||||
fader.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(fader))
|
fader.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(fader))
|
||||||
fader.on_select_callback = check_and_update_selected_strip
|
fader.on_select_callback = check_and_update_selected_strip
|
||||||
|
fader.on_context_menu_callback = strip_context_menu
|
||||||
fader.on_zone_change_callback = handle_fader_zone_change
|
fader.on_zone_change_callback = handle_fader_zone_change
|
||||||
update_window_width()
|
update_window_width()
|
||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
_update_fader_placeholder()
|
_update_fader_placeholder()
|
||||||
|
|
||||||
def fader_remove(fader):
|
def fader_remove(fader):
|
||||||
global selected_strip
|
|
||||||
if len(faders) <= 0:
|
if len(faders) <= 0:
|
||||||
return
|
return
|
||||||
if selected_strip == fader:
|
if fader in selected_strips:
|
||||||
selected_strip = None
|
clear_all_selections()
|
||||||
label_selected_channel.setText("Software item selected: None")
|
|
||||||
hstack_fader_center.removeWidget(fader)
|
hstack_fader_center.removeWidget(fader)
|
||||||
fader.deleteLater()
|
fader.deleteLater()
|
||||||
faders.remove(fader)
|
faders.remove(fader)
|
||||||
@@ -1960,6 +2053,7 @@ def fader_remove(fader):
|
|||||||
def transport_toggle_add(label, default_cc, insert_after=None):
|
def transport_toggle_add(label, default_cc, insert_after=None):
|
||||||
t = TransportWidget(label, default_cc)
|
t = TransportWidget(label, default_cc)
|
||||||
t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
|
t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
|
||||||
|
t.on_context_menu_callback = strip_context_menu
|
||||||
t.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(t))
|
t.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(t))
|
||||||
t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
||||||
t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t))
|
t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t))
|
||||||
@@ -1980,12 +2074,10 @@ def transport_toggle_add(label, default_cc, insert_after=None):
|
|||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
|
|
||||||
def transport_remove(t):
|
def transport_remove(t):
|
||||||
global selected_strip
|
|
||||||
if len(transports) <= 1:
|
if len(transports) <= 1:
|
||||||
return
|
return
|
||||||
if selected_strip == t:
|
if t in selected_strips:
|
||||||
selected_strip = None
|
clear_all_selections()
|
||||||
label_selected_channel.setText("Software item selected: None")
|
|
||||||
for layout in [hstack_transport_zone_left, hstack_transport_center, hstack_transport_zone_right]:
|
for layout in [hstack_transport_zone_left, hstack_transport_center, hstack_transport_zone_right]:
|
||||||
layout.removeWidget(t)
|
layout.removeWidget(t)
|
||||||
t.deleteLater()
|
t.deleteLater()
|
||||||
|
|||||||
+15
-4
@@ -200,21 +200,32 @@ class ToggleWidget(QWidget):
|
|||||||
|
|
||||||
outer.addWidget(self.container)
|
outer.addWidget(self.container)
|
||||||
self.on_select_callback = None
|
self.on_select_callback = None
|
||||||
|
self.on_context_menu_callback = None
|
||||||
|
|
||||||
for child in self.findChildren(QWidget):
|
for child in self.findChildren(QWidget):
|
||||||
child.installEventFilter(self)
|
child.installEventFilter(self)
|
||||||
|
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
if event.type() == QEvent.Type.MouseButtonPress:
|
if event.type() == QEvent.Type.MouseButtonPress:
|
||||||
if self.on_select_callback:
|
if event.button() != Qt.MouseButton.RightButton:
|
||||||
self.on_select_callback(self)
|
if self.on_select_callback:
|
||||||
|
self.on_select_callback(self)
|
||||||
|
elif event.type() == QEvent.Type.ContextMenu:
|
||||||
|
if self.on_context_menu_callback:
|
||||||
|
self.on_context_menu_callback(self, event.globalPos())
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if self.on_select_callback:
|
if event.button() != Qt.MouseButton.RightButton:
|
||||||
self.on_select_callback(self)
|
if self.on_select_callback:
|
||||||
|
self.on_select_callback(self)
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event):
|
||||||
|
if self.on_context_menu_callback:
|
||||||
|
self.on_context_menu_callback(self, event.globalPos())
|
||||||
|
|
||||||
def apply_color(self, hex_color, selected=False):
|
def apply_color(self, hex_color, selected=False):
|
||||||
self.current_color = hex_color
|
self.current_color = hex_color
|
||||||
border = "#00ff88" if selected else "rgba(0,0,0,0.3)"
|
border = "#00ff88" if selected else "rgba(0,0,0,0.3)"
|
||||||
|
|||||||
Reference in New Issue
Block a user