import uuid from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QFrame, QSizePolicy, QCheckBox ) from PyQt6.QtCore import Qt, QEvent from palette import PALETTE_HEX, PALETTE_VIVID from styles import TITLE_STYLE from colorswatchpopup import ColorSwatchPopup class FocusFeedbackWidget(QWidget): """Read-only OSC feedback display for Channel Focus — Learn an address, show whatever value arrives. No output, no interaction.""" def __init__(self, label): super().__init__() self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) self.setFixedWidth(115) self.current_color = PALETTE_HEX.get("Gray", "#263238") self.uid = str(uuid.uuid4()) self.is_learning = 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) _grp_style = "QFrame { border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }" # Group: OSC input (feedback from the DAW) self.grp_input = QFrame() self.grp_input.setStyleSheet(_grp_style) _grp_input_layout = QVBoxLayout(self.grp_input) _grp_input_layout.setContentsMargins(4, 4, 4, 4) _grp_input_layout.setSpacing(3) self.learn_btn = QPushButton("Learn") self.learn_btn.setFixedHeight(20) self.learn_btn.setStyleSheet("border: none;") _grp_input_layout.addWidget(self.learn_btn) self.osc_addr_in_edit = QLineEdit("/3/trackname") self.osc_addr_in_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.osc_addr_in_edit.setPlaceholderText("/address") self.osc_addr_in_edit.setStyleSheet("border: none;") _grp_input_layout.addWidget(self.osc_addr_in_edit) # Custom: shows a fixed, manually-typed string instead of live OSC # feedback — disables Learn and the OSC address input while checked. self.custom_checkbox = QCheckBox("Custom") self.custom_checkbox.setChecked(False) self.custom_checkbox.stateChanged.connect(self._on_custom_changed) _grp_input_layout.addWidget(self.custom_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter) self.custom_text_edit = QLineEdit("") self.custom_text_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.custom_text_edit.setPlaceholderText("Custom text") self.custom_text_edit.setStyleSheet("border: none;") self.custom_text_edit.textChanged.connect(self._on_custom_text_changed) self.custom_text_edit.setVisible(False) _grp_input_layout.addWidget(self.custom_text_edit) inner.addWidget(self.grp_input) # Readout — the live feedback value self.readout_lbl = QLabel("—") self.readout_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) self.readout_lbl.setWordWrap(True) self.readout_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 14px; font-weight: bold; padding: 6px 2px; border-radius: 4px;") self.readout_lbl.setMinimumHeight(48) inner.addWidget(self.readout_lbl) # Scribble strip (caption — what this feedback represents, e.g. "Track Name") 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) outer.addWidget(self.container) self.on_select_callback = None self.on_context_menu_callback = None for child in self.findChildren(QWidget): child.installEventFilter(self) def eventFilter(self, obj, event): if event.type() == QEvent.Type.MouseButtonPress: if event.button() != Qt.MouseButton.RightButton: 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 def mousePressEvent(self, event): if event.button() != Qt.MouseButton.RightButton: if self.on_select_callback: self.on_select_callback(self) super().mousePressEvent(event) def contextMenuEvent(self, event): if self.on_context_menu_callback: self.on_context_menu_callback(self, event.globalPos()) 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.setText("Learn") self.learn_btn.setStyleSheet("") def bind_osc_addr(self, address): self.osc_addr_in_edit.setText(address) self.stop_learn() def _update_custom_visibility(self): is_custom = self.custom_checkbox.isChecked() self.learn_btn.setVisible(not is_custom) self.osc_addr_in_edit.setVisible(not is_custom) self.custom_text_edit.setVisible(is_custom) def _on_custom_changed(self, _state): self._update_custom_visibility() if self.custom_checkbox.isChecked(): self.readout_lbl.setText(self.custom_text_edit.text()) def _on_custom_text_changed(self, text): if self.custom_checkbox.isChecked(): self.readout_lbl.setText(text) def receive_osc_value(self, value): """Display whatever feedback arrives on the bound address, verbatim.""" if self.custom_checkbox.isChecked(): return self.readout_lbl.setText(value) 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 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 get_state(self): return { "uid": self.uid, "label": self.title_edit.text(), "text": self.readout_lbl.text(), "color": self.color_name, "osc_addr_in": self.osc_addr_in_edit.text(), "custom_enabled": self.custom_checkbox.isChecked(), "custom_text": self.custom_text_edit.text(), } def set_state(self, state): if "uid" in state: self.uid = state["uid"] self.title_edit.setText(state.get("label", "")) self.readout_lbl.setText(state.get("text", "—")) 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.osc_addr_in_edit.setText(state.get("osc_addr_in", "/3/trackname")) self.custom_text_edit.setText(state.get("custom_text", "")) self.custom_checkbox.setChecked(state.get("custom_enabled", False)) self._update_custom_visibility()