From 2d0ed94f3feb38dfc9645f2c3b9d6a06b92637bb Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 3 Jun 2025 18:45:16 +0200 Subject: [PATCH] fix(color_button_native): popup logic to choose color moved to ColorButtonNative --- .../settings/curve_settings/curve_tree.py | 19 ++-- .../color_button_native.py | 19 +++- tests/unit_tests/test_color_button_native.py | 87 +++++++++++++++++++ 3 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 tests/unit_tests/test_color_button_native.py diff --git a/bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py b/bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py index f5291673..107be6db 100644 --- a/bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +++ b/bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py @@ -22,6 +22,7 @@ from qtpy.QtWidgets import ( QWidget, ) +from bec_widgets import SafeSlot from bec_widgets.utils import ConnectionConfig, EntryValidator from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.colors import Colors @@ -154,7 +155,7 @@ class CurveRow(QTreeWidgetItem): """Create columns 3..6: color button, style combo, width spin, symbol spin.""" # Color in col 3 self.color_button = ColorButtonNative(color=self.config.color) - self.color_button.clicked.connect(lambda: self._select_color(self.color_button)) + self.color_button.color_changed.connect(self._on_color_changed) self.tree.setItemWidget(self, 3, self.color_button) # Style in col 4 @@ -177,20 +178,16 @@ class CurveRow(QTreeWidgetItem): self.symbol_spin.setValue(self.config.symbol_size) self.tree.setItemWidget(self, 6, self.symbol_spin) - def _select_color(self, button): + @SafeSlot(str, verify_sender=True) + def _on_color_changed(self, new_color: str): """ - Selects a new color using a color dialog and applies it to the specified button. Updates - related configuration properties based on the chosen color. + Update configuration when the color button emits a change. Args: - button: The button widget whose color is being modified. + new_color (str): The new color in hex format. """ - current_color = QColor(button.color()) - chosen_color = QColorDialog.getColor(current_color, self.tree, "Select Curve Color") - if chosen_color.isValid(): - button.set_color(chosen_color) - self.config.color = chosen_color.name() - self.config.symbol_color = chosen_color.name() + self.config.color = new_color + self.config.symbol_color = new_color def add_dap_row(self): """Create a new DAP row as a child. Only valid if source='device'.""" diff --git a/bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py b/bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py index b79cf4ec..5b29002f 100644 --- a/bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py +++ b/bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py @@ -1,5 +1,8 @@ +from __future__ import annotations + +from qtpy.QtCore import Signal from qtpy.QtGui import QColor -from qtpy.QtWidgets import QPushButton +from qtpy.QtWidgets import QColorDialog, QPushButton from bec_widgets import BECWidget, SafeProperty, SafeSlot @@ -12,6 +15,8 @@ class ColorButtonNative(BECWidget, QPushButton): to guarantee good readability. """ + color_changed = Signal(str) + RPC = False PLUGIN = True ICON_NAME = "colors" @@ -25,9 +30,10 @@ class ColorButtonNative(BECWidget, QPushButton): """ super().__init__(parent=parent, **kwargs) self.set_color(color) + self.clicked.connect(self._open_color_dialog) @SafeSlot() - def set_color(self, color): + def set_color(self, color: str | QColor): """Set the button's color and update its appearance. Args: @@ -38,6 +44,7 @@ class ColorButtonNative(BECWidget, QPushButton): else: self._color = color self._update_appearance() + self.color_changed.emit(self._color) @SafeProperty("QColor") def color(self): @@ -56,3 +63,11 @@ class ColorButtonNative(BECWidget, QPushButton): text_color = "#000000" if brightness > 0.5 else "#FFFFFF" self.setStyleSheet(f"background-color: {self._color}; color: {text_color};") self.setText(self._color) + + @SafeSlot() + def _open_color_dialog(self): + """Open a QColorDialog and apply the selected color.""" + current_color = QColor(self._color) + chosen_color = QColorDialog.getColor(current_color, self, "Select Curve Color") + if chosen_color.isValid(): + self.set_color(chosen_color) diff --git a/tests/unit_tests/test_color_button_native.py b/tests/unit_tests/test_color_button_native.py new file mode 100644 index 00000000..a3654668 --- /dev/null +++ b/tests/unit_tests/test_color_button_native.py @@ -0,0 +1,87 @@ +from qtpy.QtCore import Qt +from qtpy.QtGui import QColor +from qtpy.QtWidgets import QColorDialog + +from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import ( + ColorButtonNative, +) + +from .conftest import create_widget + + +def test_color_button_native(qtbot): + cb = create_widget(qtbot, ColorButtonNative) + + # Check if the instance is created successfully + assert cb is not None + + # Check if the button has a default color + assert cb.color is not None + + # Check if the button can change color + new_color = QColor(255, 0, 0) # Red + cb.set_color(new_color) + assert cb.color == new_color.name() + + +def test_color_dialog_applies_chosen_color(qtbot, monkeypatch): + """Clicking the button should open the dialog and apply the selected color.""" + cb = create_widget(qtbot, ColorButtonNative) + chosen_color = QColor(0, 255, 0) # Green + + # Force QColorDialog.getColor to return our chosen color + monkeypatch.setattr(QColorDialog, "getColor", lambda *args, **kwargs: chosen_color) + + # Expect the color_changed signal during the click + with qtbot.waitSignal(cb.color_changed, timeout=1000): + qtbot.mouseClick(cb, Qt.LeftButton) + + assert cb.color == chosen_color.name() + + +def test_color_dialog_cancel_keeps_color(qtbot, monkeypatch): + """If the dialog returns an invalid color, the button color should stay the same.""" + cb = create_widget(qtbot, ColorButtonNative) + original_color = cb.color + + # Simulate cancel: return an invalid QColor + monkeypatch.setattr(QColorDialog, "getColor", lambda *args, **kwargs: QColor()) + + qtbot.mouseClick(cb, Qt.LeftButton) + + # No signal emitted, color unchanged + assert cb.color == original_color + + +# Additional tests for color property getter/setter +def test_color_property_getter_setter_hex(qtbot): + """Verify the color property works correctly with hex strings.""" + cb = create_widget(qtbot, ColorButtonNative) + + # Confirm default value is a valid hex string + default_color = cb.color + assert ( + isinstance(default_color, str) and default_color.startswith("#") and len(default_color) == 7 + ) + + # Use property setter with a new hex color + new_color_hex = "#123456" + with qtbot.waitSignal(cb.color_changed, timeout=1000): + cb.color = new_color_hex + + # Getter should reflect the new value + assert cb.color == new_color_hex + # Button text should update as well + assert cb.text() == new_color_hex + + +def test_color_property_setter_qcolor(qtbot): + """Verify the color property accepts QColor and emits the signal.""" + cb = create_widget(qtbot, ColorButtonNative) + q_color = QColor(200, 100, 50) + + with qtbot.waitSignal(cb.color_changed, timeout=1000): + cb.color = q_color + + assert cb.color == q_color.name() + assert cb.text() == q_color.name()