refactor before main_maindow tear down

This commit is contained in:
Paul Lipscomb
2026-06-17 01:31:09 -04:00
parent 3e2147fa02
commit a347b7bcb6
8 changed files with 1237 additions and 1146 deletions
+389
View File
@@ -0,0 +1,389 @@
import uuid
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QSlider,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy,
QPushButton, QCheckBox
)
from PyQt6.QtCore import Qt, QEvent, QTimer
from palette import (
PALETTE_NAMES,
PALETTE_HEX,
PALETTE_VIVID,
lighten_hex,
darken_hex,
)
from styles import FADER_STYLE, TITLE_STYLE
from zonebutton import ZoneButton
from colorswatchpopup import ColorSwatchPopup
from midi_sender import send_cc
class FaderWidget(QWidget):
def __init__(self, label, default_cc):
super().__init__()
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
self.setFixedWidth(100)
self.current_color = "transparent"
self.uid = str(uuid.uuid4())
# Pickup mode state
self.pickup_mode = False
self.last_sent = 64
self.prev_position = 64
self.hunting = False
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(2)
self.container = QFrame()
self.container.setFrameShape(QFrame.Shape.StyledPanel)
self.apply_color(self.current_color)
inner = QVBoxLayout(self.container)
inner.setContentsMargins(4, 6, 4, 6)
inner.setSpacing(4)
inner.setAlignment(Qt.AlignmentFlag.AlignHCenter)
# Learn button
self.hw_cc = None
self.is_learning = False
self.learn_btn = QPushButton("Learn")
self.learn_btn.setFixedHeight(20)
self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu)
inner.addWidget(self.learn_btn)
# CC In label
self.cc_input_lbl = QLabel("")
self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
inner.addWidget(self.cc_input_lbl)
# CC spin
self.cc_spin = QSpinBox()
self.cc_spin.setMinimum(1)
self.cc_spin.setMaximum(127)
self.cc_spin.setValue(default_cc)
self.cc_spin.setFixedWidth(75)
inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
self.cc_output_lbl = QLabel("")
self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
inner.addWidget(self.cc_output_lbl)
# Fader
self.fader = QSlider(Qt.Orientation.Vertical)
self.fader.setMinimum(0)
self.fader.setMaximum(127)
self.fader.setValue(64)
self.fader.setFixedHeight(200)
self.fader.setFixedWidth(40)
self.fader.setStyleSheet(FADER_STYLE)
inner.addWidget(self.fader, alignment=Qt.AlignmentFlag.AlignHCenter)
# Readout
# readout kept for pickup blink logic but not shown
self.readout = QLabel(f"CC {default_cc}{self.fader.value()}")
self.readout.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.readout.setVisible(False)
# Pickup checkbox
self.pickup_checkbox = QCheckBox("Pickup")
self.pickup_checkbox.setChecked(False)
self.pickup_checkbox.stateChanged.connect(self.on_pickup_changed)
inner.addWidget(self.pickup_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
# Scribble strip
self.title_edit = QLineEdit(label)
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.title_edit.setStyleSheet(TITLE_STYLE)
inner.addWidget(self.title_edit)
# Color swatch button
self.color_name = "Gray"
self.color_swatch_btn = QPushButton()
self.color_swatch_btn.setFixedHeight(10)
self.color_swatch_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID['Gray']}; border-radius: 2px; border: none;")
self.color_swatch_btn.clicked.connect(self.show_color_popup)
inner.addWidget(self.color_swatch_btn)
# + / - buttons
btn_row = QHBoxLayout()
btn_row.setContentsMargins(0, 0, 0, 0)
btn_row.setSpacing(2)
self.minus_btn = QPushButton("-")
self.minus_btn.setFixedSize(30, 20)
self.plus_btn = QPushButton("+")
self.plus_btn.setFixedSize(30, 20)
btn_row.addWidget(self.minus_btn)
btn_row.addStretch()
btn_row.addWidget(self.plus_btn)
inner.addLayout(btn_row)
# Zone button
self.zone_btn = ZoneButton()
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
self.zone_btn.right_callback = lambda active: self.on_zone_checked("right", active)
inner.addWidget(self.zone_btn)
# Separator before ID label
id_sep = QWidget()
id_sep.setFixedHeight(1)
id_sep.setStyleSheet("background-color: #3a3a3a;")
inner.addWidget(id_sep)
# Channel ID label
self.id_label = QLabel("")
self.id_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.id_label.setStyleSheet("color: #ffffff; font-size: 13px; font-weight: bold; background: transparent;")
inner.addWidget(self.id_label)
outer.addWidget(self.container)
self.fader.valueChanged.connect(self.on_fader_moved)
self.cc_spin.valueChanged.connect(self.on_cc_changed)
self.on_select_callback = None
self.hw_cc = None # bound hardware CC number
self.is_learning = False
self.zone = None # None, "left", or "right"
self.center_index = None # saved position in center row before zoning
self.zone_index = None # position within zone slot
self.on_zone_change_callback = None
# Install event filter on all children to forward clicks to select
for child in self.findChildren(QWidget):
child.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.MouseButtonPress:
if self.on_select_callback:
self.on_select_callback(self)
return False
def mousePressEvent(self, event):
if self.on_select_callback:
self.on_select_callback(self)
super().mousePressEvent(event)
def on_pickup_changed(self, state):
self.pickup_mode = bool(state)
if self.pickup_mode:
self.hunting = True
self.prev_position = self.fader.value()
self.pickup_checkbox.setStyleSheet("color: orange; font-weight: bold;")
self._start_blink()
else:
self.hunting = False
self.pickup_checkbox.setStyleSheet("color: black;")
self._stop_blink()
self._set_readout_style("normal")
def _set_readout_style(self, mode):
if mode == "normal":
self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
self.pickup_checkbox.setStyleSheet("color: black;") if not self.pickup_mode else None
elif mode == "orange":
self.cc_output_lbl.setStyleSheet("background-color: #000000; color: orange; font-size: 10px; font-weight: bold; padding: 1px;")
elif mode == "clear":
self.cc_output_lbl.setStyleSheet("background-color: #000000; color: transparent; font-size: 10px; padding: 1px;")
def _start_blink(self):
if not hasattr(self, '_blink_timer'):
self._blink_timer = QTimer()
self._blink_timer.timeout.connect(self._do_blink)
self._blink_state = False
self._blink_timer.start(400)
def _stop_blink(self):
if hasattr(self, '_blink_timer'):
self._blink_timer.stop()
self._set_readout_style("normal")
def _do_blink(self):
self._blink_state = not self._blink_state
if self._blink_state:
self._set_readout_style("orange")
else:
self._set_readout_style("clear")
def on_fader_moved(self, value):
cc_num = self.cc_spin.value()
self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
if self.pickup_mode and self.hunting:
crossed = (
(self.prev_position < self.last_sent <= value) or
(self.prev_position > self.last_sent >= value)
)
self.prev_position = value
if crossed:
self.hunting = False
self._stop_blink()
self._set_readout_style("orange")
self.last_sent = value
send_cc(cc_num, value)
else:
self.readout.setText(f"CC {cc_num}{value}")
return
else:
self.last_sent = value
self.prev_position = value
send_cc(cc_num, value)
self.readout.setText(f"CC {cc_num}{value}")
def on_cc_changed(self):
cc_num = self.cc_spin.value()
value = self.fader.value()
self.readout.setText(f"CC {cc_num}{value}")
self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
if self.pickup_mode:
self.hunting = True
self._start_blink()
def apply_color(self, hex_color, selected=False):
self.current_color = hex_color
border = "#00ff88" if selected else "rgba(0,0,0,0.3)"
border_width = "3px" if selected else "2px"
self.container.setObjectName("stripContainer")
self.container.setStyleSheet(f"""
QFrame#stripContainer {{
background-color: {hex_color};
border-radius: 6px;
border: {border_width} solid {border};
}}
""")
def set_selected(self, selected):
self.apply_color(self.current_color, selected=selected)
def update_id_label(self, zone, index):
if zone == "left":
self.id_label.setText(f"L{index + 1}")
elif zone == "right":
self.id_label.setText(f"R{index + 1}")
else:
self.id_label.setText(f"{index + 1}")
def on_zone_checked(self, zone, active):
if active:
self.zone = zone
else:
self.zone = None
if self.on_zone_change_callback:
self.on_zone_change_callback(self)
def show_color_popup(self):
popup = ColorSwatchPopup(self.on_color_change, self)
btn_pos = self.color_swatch_btn.mapToGlobal(self.color_swatch_btn.rect().bottomLeft())
popup.move(btn_pos)
popup.show()
def on_color_change(self, name, hex_color=None):
if hex_color is None:
hex_color = PALETTE_HEX.get(name, self.current_color)
self.color_name = name
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(self.color_name, hex_color)}; border-radius: 2px; border: none;")
self.apply_color(hex_color)
def on_zone_checked(self, zone, active):
if active:
self.zone = zone
else:
self.zone = None
if self.on_zone_change_callback:
self.on_zone_change_callback(self)
def on_learn_context_menu(self, pos):
from PyQt6.QtWidgets import QMenu
if self.hw_cc is None:
return
menu = QMenu(self)
clear_action = menu.addAction("Clear Input Assignment")
action = menu.exec(self.learn_btn.mapToGlobal(pos))
if action == clear_action:
self.hw_cc = None
self.is_learning = False
self.learn_btn.setText("Learn")
self.learn_btn.setStyleSheet("")
self.cc_input_lbl.setText("")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
def update_id_label(self, zone, index):
if zone == "left":
self.id_label.setText(f"L{index + 1}")
elif zone == "right":
self.id_label.setText(f"R{index + 1}")
else:
self.id_label.setText(f"{index + 1}")
def start_learn(self):
self.is_learning = True
self.learn_btn.setText("Listening...")
self.learn_btn.setStyleSheet("color: orange; font-weight: bold;")
def stop_learn(self):
self.is_learning = False
self.learn_btn.setStyleSheet("")
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
def bind_hw_cc(self, cc_number):
self.hw_cc = cc_number
self.stop_learn()
def get_state(self):
return {
"uid": self.uid,
"label": self.title_edit.text(),
"cc": self.cc_spin.value(),
"value": self.fader.value(),
"color": self.color_name,
"pickup": self.pickup_checkbox.isChecked(),
"last_sent": self.last_sent,
"hw_cc": self.hw_cc,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index
}
def set_state(self, state):
if "uid" in state:
self.uid = state["uid"]
self.title_edit.setText(state.get("label", ""))
self.cc_spin.setValue(state.get("cc", 1))
self.last_sent = state.get("last_sent", 64)
self.fader.setValue(state.get("value", 64))
color_name = state.get("color", "Gray")
hex_color = PALETTE_HEX.get(color_name, "transparent")
self.color_name = color_name
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(self.color_name, hex_color)}; border-radius: 2px; border: none;")
self.apply_color(hex_color)
pickup = state.get("pickup", False)
self.pickup_checkbox.setChecked(pickup)
if pickup:
self.hunting = True
self._start_blink()
self.zone = state.get("zone", None)
self.center_index = state.get("center_index", None)
self.zone_index = state.get("zone_index", None)
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
self.hw_cc = state.get("hw_cc", None)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
+11 -1042
View File
File diff suppressed because it is too large Load Diff
View File
+14
View File
@@ -0,0 +1,14 @@
from PyQt6.QtCore import QObject, pyqtSignal
class OSCReceiver(QObject):
track_name_signal = pyqtSignal(str)
preset_name_signal = pyqtSignal(str)
tempo_signal = pyqtSignal(float)
position_signal = pyqtSignal(str)
play_signal = pyqtSignal(int)
record_signal = pyqtSignal(int)
raw_message_signal = pyqtSignal(str, str)
osc_out_signal = pyqtSignal(str, str)
osc_receiver = OSCReceiver()
+39
View File
@@ -0,0 +1,39 @@
from pythonosc import udp_client
from midi_receiver import midi_receiver
from osc_receiver import osc_receiver
_osc_client = None
_osc_send_port = 8000
_osc_send_ip = "127.0.0.1"
def get_osc_client():
global _osc_client
if _osc_client is None:
_osc_client = udp_client.SimpleUDPClient(_osc_send_ip, _osc_send_port)
return _osc_client
def set_osc_target(ip=None, port=None):
global _osc_client, _osc_send_ip, _osc_send_port
if ip is not None:
_osc_send_ip = ip
if port is not None:
_osc_send_port = port
_osc_client = None
def send_osc_message(address, value=1.0):
try:
get_osc_client().send_message(address, float(value))
midi_receiver.transport_out_signal.emit()
osc_receiver.osc_out_signal.emit(address, str(value))
print(f"[OSC] → {address} {value}")
except Exception as e:
print(f"[OSC send] Error: {e}")
+130 -104
View File
@@ -1,105 +1,131 @@
{
"__last_used__": "test1",
"presets": {
"test1": {
"midi_port": "Microsoft GS Wavetable Synth 0",
"midi_input": "MPKmini2 1",
"transport_port": "Microsoft GS Wavetable Synth 0",
"faders": [
{
"uid": "5ca2258a-24da-41b1-8d4a-464b52fc620a",
"label": "Fader 1",
"cc": 1,
"value": 102,
"color": "Green",
"pickup": false,
"last_sent": 102,
"hw_cc": null,
"zone": null,
"center_index": null,
"zone_index": null
}
],
"toggles": [
{
"uid": "2e87ec73-fcdc-4e7f-841b-9b701d03a576",
"label": "Toggle 1",
"cc": 20,
"state": false,
"color": "Blue",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null
}
],
"transports": [
{
"uid": "c5e25c55-f0b3-464f-aba9-7ff44963543b",
"label": "Play",
"cc": 117,
"state": false,
"color": "Green",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "2b2b4e59-643a-44dc-ad54-71f565ad3bc7",
"label": "Stop",
"cc": 116,
"state": false,
"color": "Yellow",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "b2b81948-59c9-4b1f-b185-d7ab244a195b",
"label": "Record",
"cc": 118,
"state": false,
"color": "Red",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "e27dec15-9de0-4eb6-a776-d3d4bc216cb8",
"label": "Arm",
"cc": 119,
"state": false,
"color": "Red",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
}
],
"browser": {
"back_cc": 22,
"fwd_cc": 23
},
"osc": {
"ip": "127.0.0.1",
"listen_port": 9000,
"send_port": 8000
}
}
}
{
"__last_used__": "test1",
"presets": {
"test1": {
"midi_port": "IAC Driver Bus 1",
"midi_input": "VMPK Output",
"transport_port": "IAC Driver Bus 1",
"faders": [
{
"uid": "5ca2258a-24da-41b1-8d4a-464b52fc620a",
"label": "Fader 1",
"cc": 1,
"value": 127,
"color": "Green",
"pickup": false,
"last_sent": 127,
"hw_cc": 1,
"zone": null,
"center_index": null,
"zone_index": null
},
{
"uid": "9dd10748-366b-4305-80ae-70b087bd4900",
"label": "Fader 2",
"cc": 1,
"value": 64,
"color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
"zone": null,
"center_index": null,
"zone_index": null
},
{
"uid": "a5b03539-5a4b-44cf-9e90-0d97c4c18292",
"label": "Fader 3",
"cc": 1,
"value": 64,
"color": "Gray",
"pickup": false,
"last_sent": 64,
"hw_cc": null,
"zone": null,
"center_index": null,
"zone_index": null
}
],
"toggles": [
{
"uid": "2e87ec73-fcdc-4e7f-841b-9b701d03a576",
"label": "Toggle 1",
"cc": 20,
"state": false,
"color": "Blue",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null
}
],
"transports": [
{
"uid": "c5e25c55-f0b3-464f-aba9-7ff44963543b",
"label": "Play",
"cc": 117,
"state": false,
"color": "Green",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "2b2b4e59-643a-44dc-ad54-71f565ad3bc7",
"label": "Stop",
"cc": 116,
"state": false,
"color": "Yellow",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "b2b81948-59c9-4b1f-b185-d7ab244a195b",
"label": "Record",
"cc": 118,
"state": false,
"color": "Red",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "e27dec15-9de0-4eb6-a776-d3d4bc216cb8",
"label": "Arm",
"cc": 119,
"state": false,
"color": "Red",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
}
],
"browser": {
"back_cc": 22,
"fwd_cc": 23
},
"osc": {
"ip": "127.0.0.1",
"listen_port": 9000,
"send_port": 8000
}
}
}
}
+282
View File
@@ -0,0 +1,282 @@
import uuid
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt, QEvent
from palette import PALETTE_HEX, PALETTE_VIVID
from styles import TITLE_STYLE
from zonebutton import ZoneButton
from colorswatchpopup import ColorSwatchPopup
from midi_sender import send_cc
class ToggleWidget(QWidget):
def __init__(self, label, default_cc):
super().__init__()
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
self.setFixedWidth(100)
self.current_color = "transparent"
self.toggle_state = False
self.uid = str(uuid.uuid4())
self.zone = None
self.center_index = None
self.zone_index = None
self.on_zone_change_callback = None
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(2)
self.container = QFrame()
self.container.setFrameShape(QFrame.Shape.StyledPanel)
self.apply_color(self.current_color)
inner = QVBoxLayout(self.container)
inner.setContentsMargins(4, 6, 4, 6)
inner.setSpacing(4)
inner.setAlignment(Qt.AlignmentFlag.AlignHCenter)
# Learn button
self.hw_cc = None
self.is_learning = False
self.learn_btn = QPushButton("Learn")
self.learn_btn.setFixedHeight(20)
self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu)
inner.addWidget(self.learn_btn)
# CC In label
self.cc_input_lbl = QLabel("")
self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
inner.addWidget(self.cc_input_lbl)
# CC spin
self.cc_spin = QSpinBox()
self.cc_spin.setMinimum(1)
self.cc_spin.setMaximum(127)
self.cc_spin.setValue(default_cc)
self.cc_spin.setFixedWidth(75)
inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
self.cc_output_lbl = QLabel(f"Out 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;")
inner.addWidget(self.cc_output_lbl)
# Toggle button with LED dot overlay
btn_container = QWidget()
btn_container.setFixedSize(60, 60)
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)
self.led_dot.setFixedSize(6, 6)
self.led_dot.move(5, 49)
self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")
inner.addWidget(btn_container, alignment=Qt.AlignmentFlag.AlignHCenter)
# Scribble strip
self.title_edit = QLineEdit(label)
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.title_edit.setStyleSheet(TITLE_STYLE)
inner.addWidget(self.title_edit)
# Color swatch button
self.color_name = "Gray"
self.color_swatch_btn = QPushButton()
self.color_swatch_btn.setFixedHeight(10)
self.color_swatch_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID['Gray']}; border-radius: 2px; border: none;")
self.color_swatch_btn.clicked.connect(self.show_color_popup)
inner.addWidget(self.color_swatch_btn)
# + / - buttons inside container below color swatch
toggle_btn_row = QHBoxLayout()
toggle_btn_row.setContentsMargins(0, 0, 0, 0)
toggle_btn_row.setSpacing(2)
self.minus_btn = QPushButton("-")
self.minus_btn.setFixedSize(30, 20)
self.plus_btn = QPushButton("+")
self.plus_btn.setFixedSize(30, 20)
toggle_btn_row.addWidget(self.minus_btn)
toggle_btn_row.addStretch()
toggle_btn_row.addWidget(self.plus_btn)
inner.addLayout(toggle_btn_row)
# Zone button
self.zone_btn = ZoneButton()
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
self.zone_btn.right_callback = lambda active: self.on_zone_checked("right", active)
inner.addWidget(self.zone_btn)
# Separator before ID label
id_sep = QWidget()
id_sep.setFixedHeight(1)
id_sep.setStyleSheet("background-color: #3a3a3a;")
inner.addWidget(id_sep)
# Channel ID label
self.id_label = QLabel("")
self.id_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.id_label.setStyleSheet("color: #ffffff; font-size: 13px; font-weight: bold; background: transparent;")
inner.addWidget(self.id_label)
outer.addWidget(self.container)
self.on_select_callback = None
for child in self.findChildren(QWidget):
child.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.MouseButtonPress:
if self.on_select_callback:
self.on_select_callback(self)
return False
def mousePressEvent(self, event):
if self.on_select_callback:
self.on_select_callback(self)
super().mousePressEvent(event)
def apply_color(self, hex_color, selected=False):
self.current_color = hex_color
border = "#00ff88" if selected else "rgba(0,0,0,0.3)"
border_width = "3px" if selected else "2px"
self.container.setObjectName("stripContainer")
self.container.setStyleSheet(f"""
QFrame#stripContainer {{
background-color: {hex_color};
border-radius: 6px;
border: {border_width} solid {border};
}}
""")
def set_selected(self, selected):
self.apply_color(self.current_color, selected=selected)
def start_learn(self):
self.is_learning = True
self.learn_btn.setText("Listening...")
self.learn_btn.setStyleSheet("color: orange; font-weight: bold;")
def stop_learn(self):
self.is_learning = False
self.learn_btn.setStyleSheet("")
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
def bind_hw_cc(self, cc_number):
self.hw_cc = cc_number
self.stop_learn()
def on_learn_context_menu(self, pos):
from PyQt6.QtWidgets import QMenu
if self.hw_cc is None:
return
menu = QMenu(self)
clear_action = menu.addAction("Clear Input Assignment")
action = menu.exec(self.learn_btn.mapToGlobal(pos))
if action == clear_action:
self.hw_cc = None
self.is_learning = False
self.learn_btn.setText("Learn")
self.learn_btn.setStyleSheet("")
self.cc_input_lbl.setText("")
def update_id_label(self, zone, index):
if zone == "left":
self.id_label.setText(f"L{index + 1}")
elif zone == "right":
self.id_label.setText(f"R{index + 1}")
else:
self.id_label.setText(f"{index + 1}")
def on_zone_checked(self, zone, active):
if active:
self.zone = zone
else:
self.zone = None
if self.on_zone_change_callback:
self.on_zone_change_callback(self)
def show_color_popup(self):
popup = ColorSwatchPopup(self.on_color_change, self)
btn_pos = self.color_swatch_btn.mapToGlobal(self.color_swatch_btn.rect().bottomLeft())
popup.move(btn_pos)
popup.show()
def on_color_change(self, name, hex_color=None):
if hex_color is None:
hex_color = PALETTE_HEX.get(name, self.current_color)
self.color_name = name
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(self.color_name, hex_color)}; border-radius: 2px; border: none;")
self.apply_color(hex_color)
self._update_btn_style()
def on_click(self):
self.toggle_state = not self.toggle_state
cc_num = self.cc_spin.value()
value = 127 if self.toggle_state else 0
send_cc(cc_num, value)
self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
self._update_btn_style()
def _update_btn_style(self):
if self.toggle_state:
self.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
else:
self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")
def get_state(self):
return {
"uid": self.uid,
"label": self.title_edit.text(),
"cc": self.cc_spin.value(),
"state": self.toggle_state,
"color": self.color_name,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index,
"hw_cc": self.hw_cc
}
def set_state(self, state):
if "uid" in state:
self.uid = state["uid"]
self.title_edit.setText(state.get("label", ""))
self.cc_spin.setValue(state.get("cc", 1))
color_name = state.get("color", "Gray")
hex_color = PALETTE_HEX.get(color_name, "transparent")
self.color_name = color_name
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(self.color_name, hex_color)}; border-radius: 2px; border: none;")
self.apply_color(hex_color)
self.toggle_state = state.get("state", False)
self._update_btn_style()
cc_num = self.cc_spin.value()
value = 127 if self.toggle_state else 0
self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
self.zone = state.get("zone", None)
self.center_index = state.get("center_index", None)
self.zone_index = state.get("zone_index", None)
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
self.hw_cc = state.get("hw_cc", None)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
+372
View File
@@ -0,0 +1,372 @@
import uuid
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt, QEvent
from palette import PALETTE_HEX, PALETTE_VIVID
from styles import TITLE_STYLE
from zonebutton import ZoneButton
from colorswatchpopup import ColorSwatchPopup
from midi_sender import send_transport_cc
from osc_sender import send_osc_message
class TransportWidget(QWidget):
"""Transport button strip — same pattern as ToggleWidget."""
def __init__(self, label, default_cc):
super().__init__()
self.uid = str(uuid.uuid4())
self.zone = None
self.center_index = None
self.zone_index = None
self.toggle_state = False
self.hw_cc = None
self.is_learning = False
self.current_color = "transparent"
self.color_name = "Gray"
self.on_select_callback = None
self.on_zone_change_callback = None
self._label = label
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
self.setFixedWidth(100)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(2)
self.container = QFrame()
self.container.setFrameShape(QFrame.Shape.StyledPanel)
self.apply_color(self.current_color)
inner = QVBoxLayout(self.container)
inner.setContentsMargins(4, 6, 4, 6)
inner.setSpacing(4)
inner.setAlignment(Qt.AlignmentFlag.AlignHCenter)
# Learn button
self.learn_btn = QPushButton("Learn")
self.learn_btn.setFixedHeight(20)
self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu)
inner.addWidget(self.learn_btn)
# CC In label
self.cc_input_lbl = QLabel("")
self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;")
inner.addWidget(self.cc_input_lbl)
# CC spin
self.cc_spin = QSpinBox()
self.cc_spin.setMinimum(1)
self.cc_spin.setMaximum(127)
self.cc_spin.setValue(default_cc)
self.cc_spin.setFixedWidth(75)
inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
# CC Out label
self.cc_output_lbl = QLabel(f"Out 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;")
inner.addWidget(self.cc_output_lbl)
# MIDI / OSC mode selector
mode_row = QHBoxLayout()
mode_row.setContentsMargins(0, 0, 0, 0)
mode_row.setSpacing(2)
self.midi_btn = QPushButton("MIDI")
self.midi_btn.setFixedHeight(18)
self.midi_btn.setCheckable(True)
self.midi_btn.setChecked(True)
self.osc_btn = QPushButton("OSC")
self.osc_btn.setFixedHeight(18)
self.osc_btn.setCheckable(True)
self.osc_btn.setChecked(False)
self.midi_btn.clicked.connect(lambda: self.set_output_mode("midi"))
self.osc_btn.clicked.connect(lambda: self.set_output_mode("osc"))
mode_row.addWidget(self.midi_btn)
mode_row.addWidget(self.osc_btn)
inner.addLayout(mode_row)
# Momentary / Toggle selector
trigger_row = QHBoxLayout()
trigger_row.setContentsMargins(0, 0, 0, 0)
trigger_row.setSpacing(2)
self.momentary_btn = QPushButton("Mom.")
self.momentary_btn.setFixedHeight(18)
self.momentary_btn.setCheckable(True)
self.momentary_btn.setChecked(True)
self.toggle_mode_btn = QPushButton("Tog.")
self.toggle_mode_btn.setFixedHeight(18)
self.toggle_mode_btn.setCheckable(True)
self.toggle_mode_btn.setChecked(False)
self.momentary_btn.clicked.connect(lambda: self.set_trigger_mode("momentary"))
self.toggle_mode_btn.clicked.connect(lambda: self.set_trigger_mode("toggle"))
trigger_row.addWidget(self.momentary_btn)
trigger_row.addWidget(self.toggle_mode_btn)
inner.addLayout(trigger_row)
self.trigger_mode = "momentary" # "momentary" or "toggle"
# OSC address field (hidden by default)
self.osc_addr_edit = QLineEdit("/play")
self.osc_addr_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.osc_addr_edit.setFixedWidth(75)
self.osc_addr_edit.setPlaceholderText("/address")
self.osc_addr_edit.setVisible(False)
inner.addWidget(self.osc_addr_edit, alignment=Qt.AlignmentFlag.AlignHCenter)
self.output_mode = "midi" # "midi" or "osc"
# Transport button with LED dot
btn_container = QWidget()
btn_container.setFixedSize(60, 60)
self.btn = QPushButton(label, btn_container)
self.btn.setFixedSize(60, 60)
self.btn.clicked.connect(self.on_click)
self.led_dot = QLabel(btn_container)
self.led_dot.setFixedSize(6, 6)
self.led_dot.move(5, 49)
self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")
inner.addWidget(btn_container, alignment=Qt.AlignmentFlag.AlignHCenter)
# Scribble strip
self.title_edit = QLineEdit(label)
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.title_edit.setStyleSheet(TITLE_STYLE)
inner.addWidget(self.title_edit)
# Color swatch
self.color_swatch_btn = QPushButton()
self.color_swatch_btn.setFixedHeight(10)
self.color_swatch_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID['Gray']}; border-radius: 2px; border: none;")
self.color_swatch_btn.clicked.connect(self.show_color_popup)
inner.addWidget(self.color_swatch_btn)
# + / - buttons
btn_row = QHBoxLayout()
btn_row.setContentsMargins(0, 0, 0, 0)
btn_row.setSpacing(2)
self.minus_btn = QPushButton("-")
self.minus_btn.setFixedSize(30, 20)
self.plus_btn = QPushButton("+")
self.plus_btn.setFixedSize(30, 20)
btn_row.addWidget(self.minus_btn)
btn_row.addStretch()
btn_row.addWidget(self.plus_btn)
inner.addLayout(btn_row)
# Zone button
self.zone_btn = ZoneButton()
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
self.zone_btn.right_callback = lambda active: self.on_zone_checked("right", active)
inner.addWidget(self.zone_btn)
# Separator + ID label
id_sep = QWidget()
id_sep.setFixedHeight(1)
id_sep.setStyleSheet("background-color: #3a3a3a;")
inner.addWidget(id_sep)
self.id_label = QLabel("")
self.id_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.id_label.setStyleSheet("color: #ffffff; font-size: 13px; font-weight: bold; background: transparent;")
inner.addWidget(self.id_label)
outer.addWidget(self.container)
for child in self.findChildren(QWidget):
child.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.MouseButtonPress:
if self.on_select_callback:
self.on_select_callback(self)
return False
def mousePressEvent(self, event):
if self.on_select_callback:
self.on_select_callback(self)
super().mousePressEvent(event)
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 set_output_mode(self, mode):
self.output_mode = mode
if mode == "midi":
self.midi_btn.setChecked(True)
self.osc_btn.setChecked(False)
self.midi_btn.setStyleSheet("font-weight: bold; color: #00e676;")
self.osc_btn.setStyleSheet("")
self.cc_spin.setVisible(True)
self.cc_output_lbl.setVisible(True)
self.osc_addr_edit.setVisible(False)
else:
self.osc_btn.setChecked(True)
self.midi_btn.setChecked(False)
self.osc_btn.setStyleSheet("font-weight: bold; color: #00e676;")
self.midi_btn.setStyleSheet("")
self.cc_spin.setVisible(False)
self.cc_output_lbl.setVisible(False)
self.osc_addr_edit.setVisible(True)
def on_click(self):
if self.trigger_mode == "momentary":
if self.output_mode == "osc":
addr = self.osc_addr_edit.text().strip() or "/play"
send_osc_message(addr, 1.0)
self.cc_output_lbl.setText(f"OSC {addr}")
else:
cc_num = self.cc_spin.value()
send_transport_cc(cc_num, 127)
self.cc_output_lbl.setText(f"Out CC{cc_num} [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
if self.output_mode == "osc":
addr = self.osc_addr_edit.text().strip() or "/play"
send_osc_message(addr, 1.0 if self.toggle_state else 0.0)
self.cc_output_lbl.setText(f"OSC {addr}")
else:
cc_num = self.cc_spin.value()
send_transport_cc(cc_num, value)
self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]")
self._update_btn_style()
def _update_btn_style(self):
if self.toggle_state:
self.led_dot.setStyleSheet("background-color: #00e676; border-radius: 3px;")
else:
self.led_dot.setStyleSheet("background-color: #3c3c3c; border-radius: 3px;")
def apply_color(self, hex_color, selected=False):
self.current_color = hex_color
border = "#00ff88" if selected else "rgba(0,0,0,0.3)"
border_width = "3px" if selected else "2px"
self.container.setObjectName("stripContainer")
self.container.setStyleSheet(f"""
QFrame#stripContainer {{
background-color: {hex_color};
border-radius: 6px;
border: {border_width} solid {border};
}}
""")
def set_selected(self, selected):
self.apply_color(self.current_color, selected=selected)
def update_id_label(self, zone, index):
text = f"L{index+1}" if zone == "left" else f"R{index+1}" if zone == "right" else f"{index+1}"
self.id_label.setText(text)
def show_color_popup(self):
popup = ColorSwatchPopup(self.on_color_change, self)
btn_pos = self.color_swatch_btn.mapToGlobal(self.color_swatch_btn.rect().bottomLeft())
popup.move(btn_pos)
popup.show()
def on_color_change(self, name, hex_color=None):
if hex_color is None:
hex_color = PALETTE_HEX.get(name, self.current_color)
self.color_name = name
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(name, hex_color)}; border-radius: 2px; border: none;")
self.apply_color(hex_color)
def on_zone_checked(self, zone, active):
self.zone = zone if active else None
if self.on_zone_change_callback:
self.on_zone_change_callback(self)
def start_learn(self):
self.is_learning = True
self.learn_btn.setText("Listening...")
self.learn_btn.setStyleSheet("color: orange; font-weight: bold;")
def stop_learn(self):
self.is_learning = False
self.learn_btn.setStyleSheet("")
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
else:
self.learn_btn.setText("Learn")
self.cc_input_lbl.setText("")
def bind_hw_cc(self, cc_number):
self.hw_cc = cc_number
self.stop_learn()
def on_learn_context_menu(self, pos):
from PyQt6.QtWidgets import QMenu
if self.hw_cc is None:
return
menu = QMenu(self)
clear_action = menu.addAction("Clear Input Assignment")
action = menu.exec(self.learn_btn.mapToGlobal(pos))
if action == clear_action:
self.hw_cc = None
self.learn_btn.setText("Learn")
self.learn_btn.setStyleSheet("")
self.cc_input_lbl.setText("")
def get_state(self):
return {
"uid": self.uid,
"label": self.title_edit.text(),
"cc": self.cc_spin.value(),
"state": self.toggle_state,
"color": self.color_name,
"zone": self.zone,
"center_index": self.center_index,
"zone_index": self.zone_index,
"hw_cc": self.hw_cc,
"output_mode": self.output_mode,
"osc_addr": self.osc_addr_edit.text(),
"trigger_mode": self.trigger_mode,
}
def set_state(self, state):
if "uid" in state:
self.uid = state["uid"]
self.title_edit.setText(state.get("label", self._label))
self.cc_spin.setValue(state.get("cc", 1))
color_name = state.get("color", "Gray")
hex_color = PALETTE_HEX.get(color_name, "transparent")
self.color_name = color_name
self.color_swatch_btn.setStyleSheet(f"background-color: {PALETTE_VIVID.get(color_name, hex_color)}; border-radius: 2px; border: none;")
self.apply_color(hex_color)
self.toggle_state = state.get("state", False)
self._update_btn_style()
self.zone = state.get("zone", None)
self.center_index = state.get("center_index", None)
self.zone_index = state.get("zone_index", None)
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
self.hw_cc = state.get("hw_cc", None)
if self.hw_cc is not None:
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]")
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
self.osc_addr_edit.setText(state.get("osc_addr", "/play"))
self.set_output_mode(state.get("output_mode", "midi"))
self.set_trigger_mode(state.get("trigger_mode", "momentary"))