Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 327ca7e00a | |||
| d8f4cbcdd4 | |||
| b68e5e3ac2 |
@@ -0,0 +1,155 @@
|
|||||||
|
# Virtual Controller — Project Context
|
||||||
|
|
||||||
|
## What this app is
|
||||||
|
|
||||||
|
A PyQt6 desktop application that acts as a MIDI/OSC routing hub and control surface. It sits between hardware MIDI controllers (AKAI, JL Cooper motorized fader bank, iPad via Network MIDI) and a DAW (REAPER). It translates incoming CC numbers to different output CC numbers and channels, applies momentary/toggle logic, and manages named presets per instrument/VSTi.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Files
|
||||||
|
- `main.py` — app entry point, all layout assembly, routing logic, preset load/save
|
||||||
|
- `faderwidget.py` — fader strip widget (slider + CC/CH translation + learn)
|
||||||
|
- `togglewidget.py` — toggle button widget (CC/CH translation + learn)
|
||||||
|
- `transportwidget.py` — transport button widget (MIDI or OSC output, Mom/Tog mode)
|
||||||
|
- `presetwidget.py` — preset browser widget (Back/Fwd, CC emit for VSTi preset control)
|
||||||
|
- `midi_sender.py` — `send_cc()` and `send_transport_cc()`, emits `midi_out_signal`
|
||||||
|
- `midi_receiver.py` — `MidiReceiver` QObject with signals: `midi_in_signal(list)`, `midi_out_signal(list)`, `hw_cc_signal(str, int)`
|
||||||
|
- `osc_sender.py` — `send_osc_message(addr, val)`
|
||||||
|
- `osc_receiver.py` — `OSCReceiver` QObject with signals for DAW feedback
|
||||||
|
- `zonebutton.py` — split L/R zone assignment button (orange=left, green=right)
|
||||||
|
- `transportwidget.py` — transport buttons, MIDI or OSC output mode
|
||||||
|
- `styles.py` — shared style constants
|
||||||
|
- `palette.py` — color palette definitions
|
||||||
|
- `presets.py` — preset file load/save helpers
|
||||||
|
|
||||||
|
### Key globals in main.py
|
||||||
|
- `faders` — list of FaderWidget
|
||||||
|
- `toggles` — list of ToggleWidget
|
||||||
|
- `transports` — list of TransportWidget
|
||||||
|
- `preset_browsers` — [preset_browser_back, preset_browser_fwd]
|
||||||
|
- `learning_strip` — whichever widget is currently in MIDI learn mode
|
||||||
|
- `preset_switch_locked` — bool, prevents DAW OSC from auto-switching presets (default True on launch)
|
||||||
|
- `fader_show_hide_config`, `toggle_show_hide_config`, `transport_show_hide_config`, `preset_browser_show_hide_config`
|
||||||
|
|
||||||
|
## Widget design pattern
|
||||||
|
|
||||||
|
All widgets follow the same two-group lasso layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ grp_learn ──────────────┐
|
||||||
|
│ learn_btn │
|
||||||
|
│ cc_input_lbl │
|
||||||
|
└──────────────────────────┘
|
||||||
|
▼ (arrow_lbl)
|
||||||
|
┌─ grp_cc ─────────────────┐
|
||||||
|
│ CC spin / OSC addr │
|
||||||
|
│ CH spin │
|
||||||
|
│ cc_output_lbl │
|
||||||
|
└──────────────────────────┘
|
||||||
|
[main button/slider]
|
||||||
|
[title_edit scribble strip]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `grp_learn` and `grp_cc` are QFrame with `border: 1px solid rgba(255,255,255,0.2)`
|
||||||
|
- `arrow_lbl` is a `▼` label between groups
|
||||||
|
- Show/hide hides the groups and arrow, leaves the button/slider visible
|
||||||
|
- `cc_input_lbl` shows incoming HW CC and channel
|
||||||
|
- `cc_output_lbl` shows outgoing CC, channel, and last value
|
||||||
|
|
||||||
|
## MIDI routing (route_midi_message_input)
|
||||||
|
|
||||||
|
```
|
||||||
|
CC in → check learning_strip → bind and return
|
||||||
|
→ check bound to fader/toggle → emit hw_cc_signal
|
||||||
|
→ check bound to transport → emit hw_cc_signal
|
||||||
|
→ check bound to preset browser → on_hw_trigger()
|
||||||
|
→ unbound CC → drop (NO passthrough)
|
||||||
|
|
||||||
|
Note on/off → pass through to midi_out unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** Unbound CCs are silently dropped. All CC that the app should act on must be explicitly learned/bound to a widget. Notes pass through freely.
|
||||||
|
|
||||||
|
## CC translation pattern
|
||||||
|
|
||||||
|
Every widget has:
|
||||||
|
- `hw_cc` / `hw_channel` — what the hardware sends
|
||||||
|
- `cc_spin` / `ch_spin` — what to send out
|
||||||
|
- `bind_hw_cc(cc_number, channel=None)` — called by router on learn
|
||||||
|
- `_cc_input_label(vel)` — formats input readout
|
||||||
|
- `_cc_output_label(vel)` — formats output readout
|
||||||
|
|
||||||
|
## Momentary vs Toggle
|
||||||
|
|
||||||
|
- **Momentary** — every trigger sends 127, ignore 0. Used in transport and preset browser.
|
||||||
|
- **Toggle** — tracks on/off state, alternates 127/0 on positive edge.
|
||||||
|
- Hardware toggle pads (send 127 then 0 alternating): in momentary mode, EVERY incoming value (0 or 127) fires 127 out — so every pad press does something regardless of pad mode.
|
||||||
|
|
||||||
|
## Preset system
|
||||||
|
|
||||||
|
Presets saved in `presets/presets.json`. Structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"__last_used__": "name",
|
||||||
|
"presets": {
|
||||||
|
"name": {
|
||||||
|
"faders": [...],
|
||||||
|
"toggles": [...],
|
||||||
|
"browser": { "back": {...}, "fwd": {...} },
|
||||||
|
"show_hide": { "fader": bool, "toggle": bool, "transport": bool, "browser": bool },
|
||||||
|
"preset_uuid": "uuid-string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transport_preset": [...], ← GLOBAL, not per-preset
|
||||||
|
"transport_show_hide": bool, ← GLOBAL
|
||||||
|
"io_config": {...} ← GLOBAL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport is GLOBAL across all presets. Browser, faders, toggles are per-preset.
|
||||||
|
|
||||||
|
`preset_switch_locked` is NOT saved — always defaults to True (locked) on launch.
|
||||||
|
|
||||||
|
## DAW integration
|
||||||
|
|
||||||
|
- **OSC in** from REAPER: track name, preset name, play/record state, bar/position, UUID for auto preset switching
|
||||||
|
- **OSC out** to REAPER: transport button triggers (play, stop, record, arm)
|
||||||
|
- **MIDI in**: hardware CC from controllers
|
||||||
|
- **MIDI out**: translated CC to VSTi/DAW instruments
|
||||||
|
- **Transport MIDI out**: separate port for transport CC
|
||||||
|
|
||||||
|
## Log windows
|
||||||
|
|
||||||
|
Four floating windows toggled by "Messages" button:
|
||||||
|
- OSC In (green) — incoming from REAPER
|
||||||
|
- OSC Out (blue) — sent to REAPER
|
||||||
|
- MIDI In (orange) — incoming CC/notes from hardware
|
||||||
|
- MIDI Out (cyan) — outgoing translated CC
|
||||||
|
|
||||||
|
## Zone system
|
||||||
|
|
||||||
|
Faders and toggles can be assigned to Left or Right zones. The app shows vertical separators between zones. Zone button: left half = mango orange (#ff8c00), right half = green (#00c853).
|
||||||
|
|
||||||
|
## Planned: Android tablet performance surface
|
||||||
|
|
||||||
|
Architecture discussed but not yet built:
|
||||||
|
- PyQt6 app adds a WebSocket server (QWebSocketServer)
|
||||||
|
- Android app (Java) connects via WebSocket
|
||||||
|
- App broadcasts preset JSON on load/switch
|
||||||
|
- Android renders playable-only widgets (no config UI): faders, toggles, transport, preset browser
|
||||||
|
- Widgets are free-floating, draggable, lockable layout saved per-preset back to app
|
||||||
|
- Android app sends `{ type, uid, value }` messages back; app routes through existing CC/OSC logic
|
||||||
|
- No MIDI knowledge needed on Android side
|
||||||
|
|
||||||
|
## Planned: JL Cooper motorized fader sync
|
||||||
|
|
||||||
|
JL Cooper fader bank connected via MIDI. On preset load, app should send stored fader values back out to JL Cooper so motors move to correct positions. Need to confirm JL Cooper MIDI feedback channel spec.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Width: faders 115px, toggles 115px, transport 100px, preset browser 115px
|
||||||
|
- All spinners: CC 0-127, CH 1-16
|
||||||
|
- Colors: active/learned green `#00e676`, unlearned gray `#888888`, warning orange `#ff8c00`
|
||||||
|
- No "In"/"Out" prefix on readout labels — implicit from position
|
||||||
|
- Show/hide hides config groups, leaves playable controls visible
|
||||||
|
- `check_and_start_midi_learn(strip)` must be called (not just `strip.start_learn()`) to wire the global router
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -165,6 +166,14 @@ buttonImport = QPushButton("Import")
|
|||||||
buttonImport.setFixedWidth(55)
|
buttonImport.setFixedWidth(55)
|
||||||
buttonImport.clicked.connect(lambda: import_preset())
|
buttonImport.clicked.connect(lambda: import_preset())
|
||||||
|
|
||||||
|
buttonDuplicate = QPushButton("Duplicate")
|
||||||
|
buttonDuplicate.setFixedWidth(75)
|
||||||
|
buttonDuplicate.clicked.connect(lambda: duplicate_preset())
|
||||||
|
|
||||||
|
buttonRename = QPushButton("Rename")
|
||||||
|
buttonRename.setFixedWidth(65)
|
||||||
|
buttonRename.clicked.connect(lambda: rename_preset())
|
||||||
|
|
||||||
checkbox_lock_preset = QCheckBox("Lock Preset")
|
checkbox_lock_preset = QCheckBox("Lock Preset")
|
||||||
checkbox_lock_preset.setStyleSheet("color: #aaa; font-size: 11px;")
|
checkbox_lock_preset.setStyleSheet("color: #aaa; font-size: 11px;")
|
||||||
checkbox_lock_preset.setChecked(True)
|
checkbox_lock_preset.setChecked(True)
|
||||||
@@ -346,6 +355,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 +567,51 @@ 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_btn_style = (
|
||||||
|
"QPushButton { background: transparent; border: none; color: #ffffff;"
|
||||||
|
" font-size: 12px; font-weight: bold; text-align: left; padding: 4px 6px; }"
|
||||||
|
"QPushButton:!checked { color: #444444; font-weight: normal; }"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_section_row(label_text):
|
||||||
|
btn = QPushButton(label_text)
|
||||||
|
btn.setCheckable(True)
|
||||||
|
btn.setChecked(True)
|
||||||
|
btn.setFlat(True)
|
||||||
|
btn.setStyleSheet(_section_btn_style)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
chk_section_preset_browser = _make_section_row("Preset Browser")
|
||||||
|
chk_section_big_title = _make_section_row("Big Title")
|
||||||
|
chk_section_fader_row = _make_section_row("Fader Row")
|
||||||
|
chk_section_toggle_row = _make_section_row("Toggle Row")
|
||||||
|
chk_section_transport_row = _make_section_row("Transport Row")
|
||||||
|
|
||||||
|
for _btn in (chk_section_preset_browser, chk_section_big_title, chk_section_fader_row, chk_section_toggle_row, chk_section_transport_row):
|
||||||
|
vstack_layout_panel.addWidget(_btn)
|
||||||
|
del _btn
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -801,6 +860,8 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_preset_row.addWidget(buttonSave) #save button
|
hstack_preset_row.addWidget(buttonSave) #save button
|
||||||
hstack_preset_row.addWidget(buttonSaveAs) #save as button
|
hstack_preset_row.addWidget(buttonSaveAs) #save as button
|
||||||
hstack_preset_row.addWidget(buttonRemove) #remove button
|
hstack_preset_row.addWidget(buttonRemove) #remove button
|
||||||
|
hstack_preset_row.addWidget(buttonDuplicate)
|
||||||
|
hstack_preset_row.addWidget(buttonRename)
|
||||||
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(checkbox_lock_preset)
|
||||||
@@ -931,6 +992,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 +1006,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,7 +1124,16 @@ 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)
|
||||||
|
|
||||||
main_layout.addWidget(make_separator())
|
#MARK: LAYOUT PANEL
|
||||||
|
container_layout_panel.setVisible(True)
|
||||||
|
button_layout_panel.setChecked(True)
|
||||||
|
|
||||||
|
container_transport_row = QWidget()
|
||||||
|
_vbox_transport_row = QVBoxLayout(container_transport_row)
|
||||||
|
_vbox_transport_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
_vbox_transport_row.setSpacing(0)
|
||||||
|
_vbox_transport_row.addWidget(make_separator())
|
||||||
|
_vbox_transport_row.addSpacing(4)
|
||||||
|
|
||||||
# MARK: TRANSPORT ROW CONTROLS
|
# MARK: TRANSPORT ROW CONTROLS
|
||||||
global hstack_row_transport_control_buttons
|
global hstack_row_transport_control_buttons
|
||||||
@@ -1070,7 +1142,7 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_row_transport_control_buttons.addWidget(button_transport_show_hide_config)
|
hstack_row_transport_control_buttons.addWidget(button_transport_show_hide_config)
|
||||||
hstack_row_transport_control_buttons.addStretch()
|
hstack_row_transport_control_buttons.addStretch()
|
||||||
button_transport_show_hide_config.clicked.connect(transport_show_hide_config_toggle)
|
button_transport_show_hide_config.clicked.connect(transport_show_hide_config_toggle)
|
||||||
main_layout.addLayout(hstack_row_transport_control_buttons)
|
_vbox_transport_row.addLayout(hstack_row_transport_control_buttons)
|
||||||
|
|
||||||
#MARK: TRANSPORT ROW
|
#MARK: TRANSPORT ROW
|
||||||
|
|
||||||
@@ -1091,7 +1163,9 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_full_transport.addWidget(vsep_transport_right)
|
hstack_full_transport.addWidget(vsep_transport_right)
|
||||||
hstack_full_transport.addLayout(hstack_transport_zone_right)
|
hstack_full_transport.addLayout(hstack_transport_zone_right)
|
||||||
|
|
||||||
main_layout.addLayout(hstack_full_transport)
|
_vbox_transport_row.addLayout(hstack_full_transport)
|
||||||
|
main_layout.addWidget(container_transport_row)
|
||||||
|
chk_section_transport_row.toggled.connect(container_transport_row.setVisible)
|
||||||
|
|
||||||
#MARK: PRESET BROWSER CONTAINER
|
#MARK: PRESET BROWSER CONTAINER
|
||||||
global container_preset_browser
|
global container_preset_browser
|
||||||
@@ -1112,22 +1186,29 @@ class MainWindow(QMainWindow):
|
|||||||
button_preset_browser_show_hide_config.clicked.connect(preset_browser_show_hide_config_toggle)
|
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.addLayout(hstack_preset_browser_controls)
|
vstack_preset_browser_container.addLayout(hstack_preset_browser_controls)
|
||||||
|
vstack_preset_browser_container.addWidget(label_preset_browser_title)
|
||||||
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)
|
||||||
|
main_layout.addWidget(make_separator())
|
||||||
|
|
||||||
|
chk_section_preset_browser.toggled.connect(container_preset_browser.setVisible)
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
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())
|
container_fader_row = QWidget()
|
||||||
|
_vbox_fader_row = QVBoxLayout(container_fader_row)
|
||||||
|
_vbox_fader_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
_vbox_fader_row.setSpacing(0)
|
||||||
|
|
||||||
# MARK: FADER ROW CONTROLS
|
# MARK: FADER ROW CONTROLS
|
||||||
global hstack_row_fader_control_buttons
|
global hstack_row_fader_control_buttons
|
||||||
@@ -1138,7 +1219,19 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_row_fader_control_buttons.addStretch()
|
hstack_row_fader_control_buttons.addStretch()
|
||||||
button_fader_show_hide_config.clicked.connect(fader_show_hide_config_toggle)
|
button_fader_show_hide_config.clicked.connect(fader_show_hide_config_toggle)
|
||||||
button_fader_clear_all.clicked.connect(clear_all_faders)
|
button_fader_clear_all.clicked.connect(clear_all_faders)
|
||||||
controls_layout.addLayout(hstack_row_fader_control_buttons)
|
_vbox_fader_row.addLayout(hstack_row_fader_control_buttons)
|
||||||
|
|
||||||
|
# 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;"
|
||||||
|
" color: #ffffff; font-size: 28px; font-weight: bold; padding: 4px 0; }"
|
||||||
|
"QLineEdit:focus { border-bottom: 1px solid #00e676; }"
|
||||||
|
)
|
||||||
|
_vbox_fader_row.addWidget(big_title_edit)
|
||||||
|
chk_section_big_title.toggled.connect(big_title_edit.setVisible)
|
||||||
|
|
||||||
#MARK: FADERS ROW
|
#MARK: FADERS ROW
|
||||||
global hstack_full_fader
|
global hstack_full_fader
|
||||||
@@ -1155,7 +1248,7 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_full_fader.addWidget(separator_vertical_fader_zone_right)
|
hstack_full_fader.addWidget(separator_vertical_fader_zone_right)
|
||||||
hstack_full_fader.addLayout(hstack_fader_zone_right)
|
hstack_full_fader.addLayout(hstack_fader_zone_right)
|
||||||
|
|
||||||
controls_layout.addLayout(hstack_full_fader)
|
_vbox_fader_row.addLayout(hstack_full_fader)
|
||||||
|
|
||||||
global placeholder_fader_empty
|
global placeholder_fader_empty
|
||||||
placeholder_fader_empty = QWidget()
|
placeholder_fader_empty = QWidget()
|
||||||
@@ -1171,7 +1264,10 @@ class MainWindow(QMainWindow):
|
|||||||
_pf_btn.clicked.connect(fader_add)
|
_pf_btn.clicked.connect(fader_add)
|
||||||
_pf_layout.addWidget(_pf_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
_pf_layout.addWidget(_pf_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
_pf_layout.addWidget(_pf_lbl)
|
_pf_layout.addWidget(_pf_lbl)
|
||||||
controls_layout.addWidget(placeholder_fader_empty)
|
_vbox_fader_row.addWidget(placeholder_fader_empty)
|
||||||
|
|
||||||
|
controls_layout.addWidget(container_fader_row)
|
||||||
|
chk_section_fader_row.toggled.connect(container_fader_row.setVisible)
|
||||||
|
|
||||||
controls_layout.addWidget(make_separator())
|
controls_layout.addWidget(make_separator())
|
||||||
|
|
||||||
@@ -1183,6 +1279,11 @@ class MainWindow(QMainWindow):
|
|||||||
global separator_vertical_toggle_zone_right
|
global separator_vertical_toggle_zone_right
|
||||||
global hstack_toggle_zone_right
|
global hstack_toggle_zone_right
|
||||||
|
|
||||||
|
container_toggle_row = QWidget()
|
||||||
|
_vbox_toggle_row = QVBoxLayout(container_toggle_row)
|
||||||
|
_vbox_toggle_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
_vbox_toggle_row.setSpacing(0)
|
||||||
|
|
||||||
# MARK: TOGGLE ROW CONTROLS
|
# MARK: TOGGLE ROW CONTROLS
|
||||||
global hstack_row_toggle_control_buttons
|
global hstack_row_toggle_control_buttons
|
||||||
global button_toggle_show_hide_config
|
global button_toggle_show_hide_config
|
||||||
@@ -1192,7 +1293,7 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_row_toggle_control_buttons.addStretch()
|
hstack_row_toggle_control_buttons.addStretch()
|
||||||
button_toggle_show_hide_config.clicked.connect(toggle_show_hide_config_toggle)
|
button_toggle_show_hide_config.clicked.connect(toggle_show_hide_config_toggle)
|
||||||
button_toggle_clear_all.clicked.connect(clear_all_toggles)
|
button_toggle_clear_all.clicked.connect(clear_all_toggles)
|
||||||
controls_layout.addLayout(hstack_row_toggle_control_buttons)
|
_vbox_toggle_row.addLayout(hstack_row_toggle_control_buttons)
|
||||||
|
|
||||||
#MARK: TOGGLES ROW
|
#MARK: TOGGLES ROW
|
||||||
# assemble full toggle row
|
# assemble full toggle row
|
||||||
@@ -1202,7 +1303,7 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_full_toggle.addWidget(separator_vertical_toggle_zone_right)
|
hstack_full_toggle.addWidget(separator_vertical_toggle_zone_right)
|
||||||
hstack_full_toggle.addLayout(hstack_toggle_zone_right)
|
hstack_full_toggle.addLayout(hstack_toggle_zone_right)
|
||||||
|
|
||||||
controls_layout.addLayout(hstack_full_toggle)
|
_vbox_toggle_row.addLayout(hstack_full_toggle)
|
||||||
|
|
||||||
global placeholder_toggle_empty
|
global placeholder_toggle_empty
|
||||||
placeholder_toggle_empty = QWidget()
|
placeholder_toggle_empty = QWidget()
|
||||||
@@ -1218,7 +1319,10 @@ class MainWindow(QMainWindow):
|
|||||||
_pt_btn.clicked.connect(toggle_add)
|
_pt_btn.clicked.connect(toggle_add)
|
||||||
_pt_layout.addWidget(_pt_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
_pt_layout.addWidget(_pt_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
_pt_layout.addWidget(_pt_lbl)
|
_pt_layout.addWidget(_pt_lbl)
|
||||||
controls_layout.addWidget(placeholder_toggle_empty)
|
_vbox_toggle_row.addWidget(placeholder_toggle_empty)
|
||||||
|
|
||||||
|
controls_layout.addWidget(container_toggle_row)
|
||||||
|
chk_section_toggle_row.toggled.connect(container_toggle_row.setVisible)
|
||||||
|
|
||||||
controls_layout.addStretch()
|
controls_layout.addStretch()
|
||||||
|
|
||||||
@@ -1288,11 +1392,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 +1455,54 @@ 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("")
|
||||||
|
chk_section_preset_browser.setChecked(True)
|
||||||
|
chk_section_big_title.setChecked(True)
|
||||||
|
chk_section_fader_row.setChecked(True)
|
||||||
|
chk_section_toggle_row.setChecked(True)
|
||||||
|
chk_section_transport_row.setChecked(True)
|
||||||
|
show_save_status()
|
||||||
|
|
||||||
|
def rename_preset():
|
||||||
|
from PyQt6.QtWidgets import QInputDialog
|
||||||
|
current_name = menuPresets.currentText()
|
||||||
|
if not current_name:
|
||||||
|
return
|
||||||
|
new_name, ok = QInputDialog.getText(app_window, "Rename Preset", "New name:", text=current_name)
|
||||||
|
if not ok or not new_name.strip() or new_name.strip() == current_name:
|
||||||
|
return
|
||||||
|
new_name = new_name.strip()
|
||||||
|
loaded_presets["presets"][new_name] = loaded_presets["presets"].pop(current_name)
|
||||||
|
if loaded_presets.get("__last_used__") == current_name:
|
||||||
|
loaded_presets["__last_used__"] = new_name
|
||||||
|
save_presets_file(loaded_presets)
|
||||||
|
populate_preset_dropdown_menu()
|
||||||
|
menuPresets.setCurrentText(new_name)
|
||||||
|
show_save_status()
|
||||||
|
|
||||||
|
def duplicate_preset():
|
||||||
|
from PyQt6.QtWidgets import QInputDialog
|
||||||
|
current_name = menuPresets.currentText()
|
||||||
|
if not current_name:
|
||||||
|
return
|
||||||
|
new_name, ok = QInputDialog.getText(app_window, "Duplicate Preset", "Name for duplicate:", text=f"{current_name} Copy")
|
||||||
|
if not ok or not new_name.strip():
|
||||||
|
return
|
||||||
|
new_name = new_name.strip()
|
||||||
|
snapshot = preset_build_snapshot(current_name)
|
||||||
|
snapshot["preset_uuid"] = str(uuid.uuid4())
|
||||||
|
loaded_presets["presets"][new_name] = snapshot
|
||||||
|
loaded_presets["__last_used__"] = new_name
|
||||||
|
save_presets_file(loaded_presets)
|
||||||
|
populate_preset_dropdown_menu()
|
||||||
|
menuPresets.setCurrentText(new_name)
|
||||||
|
label_preset_uuid.setText(snapshot["preset_uuid"])
|
||||||
show_save_status()
|
show_save_status()
|
||||||
|
|
||||||
def show_save_status():
|
def show_save_status():
|
||||||
@@ -1904,12 +2054,20 @@ 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,
|
||||||
"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,
|
"browser": preset_browser_show_hide_config,
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": chk_section_preset_browser.isChecked(),
|
||||||
|
"big_title": chk_section_big_title.isChecked(),
|
||||||
|
"fader_row": chk_section_fader_row.isChecked(),
|
||||||
|
"toggle_row": chk_section_toggle_row.isChecked(),
|
||||||
|
"transport_row": chk_section_transport_row.isChecked(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2024,7 +2182,16 @@ 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", ""))
|
||||||
|
|
||||||
|
sections = preset.get("sections", {})
|
||||||
|
chk_section_preset_browser.setChecked(sections.get("preset_browser", True))
|
||||||
|
chk_section_big_title.setChecked(sections.get("big_title", True))
|
||||||
|
chk_section_fader_row.setChecked(sections.get("fader_row", True))
|
||||||
|
chk_section_toggle_row.setChecked(sections.get("toggle_row", True))
|
||||||
|
chk_section_transport_row.setChecked(sections.get("transport_row", True))
|
||||||
|
|
||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
|
|
||||||
def export_preset():
|
def export_preset():
|
||||||
@@ -2249,15 +2416,21 @@ 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}")
|
||||||
should_be_on = value > 63
|
toggle.cc_input_lbl.setText(toggle._cc_input_label(value))
|
||||||
if toggle.toggle_state != should_be_on:
|
if toggle.trigger_mode == "momentary":
|
||||||
toggle.toggle_state = should_be_on
|
send_cc(out_cc, 127, channel=out_ch)
|
||||||
out_val = 127 if should_be_on else 0
|
toggle.cc_output_lbl.setText(toggle._cc_output_label(127))
|
||||||
send_cc(out_cc, out_val, channel=out_ch)
|
toggle.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
|
||||||
toggle.cc_input_lbl.setText(toggle._cc_input_label(value))
|
QTimer.singleShot(150, lambda: toggle.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;"))
|
||||||
toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val))
|
else:
|
||||||
toggle._update_btn_style()
|
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
|
return
|
||||||
for t in transports:
|
for t in transports:
|
||||||
if t.uid == fader_uid:
|
if t.uid == fader_uid:
|
||||||
@@ -2312,6 +2485,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()
|
||||||
@@ -2506,4 +2693,5 @@ app.setPalette(dark_palette)
|
|||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
set_window(window)
|
set_window(window)
|
||||||
window.show()
|
window.show()
|
||||||
|
layout_panel_window_calc()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
+62
-16
@@ -143,14 +143,29 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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": "f230a4fd-edc3-4c4b-8585-f73c82168fee",
|
"preset_uuid": "8cbceb6b-ba5d-4b2c-8989-f921e4e3150d",
|
||||||
"show_hide": {
|
"show_hide": {
|
||||||
"fader": false,
|
"fader": false,
|
||||||
"toggle": false,
|
"toggle": false,
|
||||||
"transport": false
|
"transport": false,
|
||||||
|
"browser": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preset2": {
|
"preset2": {
|
||||||
@@ -229,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",
|
||||||
@@ -248,11 +278,11 @@
|
|||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preset_uuid": "e658b6bf-4bb7-4d68-bd8b-07775ee80c53",
|
"preset_uuid": "7483fb5b-f47f-49e4-bccb-e8d20195078c",
|
||||||
"show_hide": {
|
"show_hide": {
|
||||||
"fader": false,
|
"fader": false,
|
||||||
"toggle": false,
|
"toggle": false,
|
||||||
"transport": true,
|
"transport": false,
|
||||||
"browser": true
|
"browser": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -268,14 +298,14 @@
|
|||||||
{
|
{
|
||||||
"uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e",
|
"uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e",
|
||||||
"label": "Fader 1",
|
"label": "Fader 1",
|
||||||
"cc": 1,
|
"cc": 90,
|
||||||
"ch": 1,
|
"ch": 15,
|
||||||
"value": 64,
|
"value": 85,
|
||||||
"color": "Gray",
|
"color": "Gray",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"last_sent": 85,
|
||||||
"hw_cc": null,
|
"hw_cc": 67,
|
||||||
"hw_channel": null,
|
"hw_channel": 1,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null
|
||||||
@@ -371,7 +401,22 @@
|
|||||||
"zone_index": null
|
"zone_index": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"toggles": [],
|
"toggles": [
|
||||||
|
{
|
||||||
|
"uid": "46f17f40-1bb2-4527-b6c5-fd26e535beed",
|
||||||
|
"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",
|
||||||
@@ -390,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,
|
||||||
@@ -481,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,
|
||||||
|
|||||||
+50
-8
@@ -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,15 +309,35 @@ 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()
|
||||||
value = 127 if self.toggle_state else 0
|
if self.trigger_mode == "momentary":
|
||||||
print(f"[toggle] CC{cc_num} CH{out_ch} ({value})")
|
print(f"[toggle momentary] CC{cc_num} CH{out_ch} (127)")
|
||||||
send_cc(cc_num, value, channel=out_ch)
|
send_cc(cc_num, 127, channel=out_ch)
|
||||||
self.cc_output_lbl.setText(self._cc_output_label(value))
|
self.cc_output_lbl.setText(self._cc_output_label(127))
|
||||||
self._update_btn_style()
|
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):
|
def _update_btn_style(self):
|
||||||
if self.toggle_state:
|
if self.toggle_state:
|
||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user