diff --git a/bec_widgets/cli/designer_plugins.py b/bec_widgets/cli/designer_plugins.py index 9c51d995..18427e9d 100644 --- a/bec_widgets/cli/designer_plugins.py +++ b/bec_widgets/cli/designer_plugins.py @@ -24,7 +24,6 @@ designer_plugins = { "BeamlineStateManager", ), "BecConsole": ("bec_widgets.widgets.editors.bec_console.bec_console", "BecConsole"), - "ColorButton": ("bec_widgets.widgets.utility.visual.color_button.color_button", "ColorButton"), "ColorButtonNative": ( "bec_widgets.widgets.utility.visual.color_button_native.color_button_native", "ColorButtonNative", @@ -132,7 +131,6 @@ widget_icons = { "BECStatusBox": "widgets", "BeamlineStateManager": "format_list_bulleted", "BecConsole": "terminal", - "ColorButton": "colors", "ColorButtonNative": "colors", "ColormapSelector": "palette", "DapComboBox": "data_exploration", diff --git a/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py b/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py index e67aecaf..2f2a166f 100644 --- a/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +++ b/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py @@ -1,5 +1,6 @@ import os +from qtpy.QtGui import QColor from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget from bec_widgets.utils.error_popups import SafeSlot @@ -55,7 +56,7 @@ class MotorMapSettings(SettingWidget): def connect_all_signals(self): for widget in self.ui_widget_list: WidgetIO.connect_widget_change_signal(widget, self.set_property) - self.ui.color_scatter.color_selected.connect( + self.ui.color_scatter.color_changed.connect( lambda color: self.target_widget.setProperty("color_scatter", color) ) @@ -98,8 +99,7 @@ class MotorMapSettings(SettingWidget): if widget_to_set is None: return if widget_to_set is self.ui.color_scatter: - # Update the color scatter button - self.ui.color_scatter.set_color(value) + self._show_color(value) return # Block signals to avoid triggering set_property again was_blocked = widget_to_set.blockSignals(True) @@ -115,7 +115,18 @@ class MotorMapSettings(SettingWidget): value = getattr(self.target_widget, property_name) WidgetIO.set_value(widget, value) - self.ui.color_scatter.set_color(self.target_widget.color) + self._show_color(self.target_widget.color) + + def _show_color(self, color: tuple | str): + """ + Show a color on the color button without sending it back to the target widget. + + Args: + color(tuple|str): The color to show, as an RGBA tuple or a HEX string. + """ + was_blocked = self.ui.color_scatter.blockSignals(True) + self.ui.color_scatter.set_color(color) + self.ui.color_scatter.blockSignals(was_blocked) def accept_changes(self): """ @@ -126,4 +137,6 @@ class MotorMapSettings(SettingWidget): value = WidgetIO.get_value(widget) setattr(self.target_widget, property_name, value) - self.target_widget.color_scatter = self.ui.color_scatter.get_color() + # The button only holds RGB, so keep the target's alpha when the color is unchanged + if QColor(self.ui.color_scatter.color).rgb() != self.target_widget.color_scatter.rgb(): + self.target_widget.color_scatter = self.ui.color_scatter.color diff --git a/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui b/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui index c320a5f0..13602aca 100644 --- a/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +++ b/bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui @@ -104,15 +104,15 @@ - + - ColorButton - QWidget -
color_button
+ ColorButtonNative + QPushButton +
color_button_native
diff --git a/bec_widgets/widgets/utility/visual/color_button/__init__.py b/bec_widgets/widgets/utility/visual/color_button/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/widgets/utility/visual/color_button/color_button.py b/bec_widgets/widgets/utility/visual/color_button/color_button.py deleted file mode 100644 index 8f0e8b58..00000000 --- a/bec_widgets/widgets/utility/visual/color_button/color_button.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from typing import Literal - -import pyqtgraph as pg -from qtpy.QtCore import Qt, Signal -from qtpy.QtWidgets import QHBoxLayout, QWidget - -from bec_widgets.utils.error_popups import SafeSlot - - -class ColorButton(QWidget): - """ - A ColorButton that opens a dialog to select a color. Inherits from pyqtgraph.ColorButton. - Patches event loop of the ColorDialog, if opened in another QDialog. - """ - - color_selected = Signal(str) - - PLUGIN = True - ICON_NAME = "colors" - - def __init__(self, parent=None, *args, **kwargs): - super().__init__(parent=parent, *args, **kwargs) - - self.layout = QHBoxLayout(self) - self.layout.setSpacing(0) - self.layout.setContentsMargins(0, 0, 0, 0) - self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) - - self.button = pg.ColorButton() - self.button.setFlat(True) - self.button.clicked.connect(self.select_color) - self.layout.addWidget(self.button) - - @SafeSlot() - def select_color(self): - self.origColor = self.button.color() - self.button.colorDialog.setCurrentColor(self.button.color()) - self.button.colorDialog.open() - self.button.colorDialog.exec() - self.color_selected.emit(self.button.color().name()) - - @SafeSlot(str) - def set_color(self, color: tuple | str): - """ - Set the color of the button. - - Args: - color(tuple|str): The color to set. - """ - self.button.setColor(color) - - def get_color(self, format: Literal["RGBA", "HEX"] = "RGBA") -> tuple | str: - """ - Get the color of the button in the specified format. - - Args: - format(Literal["RGBA", "HEX"]): The format of the returned color. - - Returns: - tuple|str: The color in the specified format. - """ - if format == "RGBA": - return self.button.color().getRgb() - if format == "HEX": - return self.button.color().name() - - def cleanup(self): - """ - Clean up the ColorButton. - """ - self.button.colorDialog.close() - self.button.colorDialog.deleteLater() diff --git a/bec_widgets/widgets/utility/visual/color_button/color_button.pyproject b/bec_widgets/widgets/utility/visual/color_button/color_button.pyproject deleted file mode 100644 index 1136989d..00000000 --- a/bec_widgets/widgets/utility/visual/color_button/color_button.pyproject +++ /dev/null @@ -1 +0,0 @@ -{'files': ['color_button.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/utility/visual/color_button/color_button_plugin.py b/bec_widgets/widgets/utility/visual/color_button/color_button_plugin.py deleted file mode 100644 index 80398c9c..00000000 --- a/bec_widgets/widgets/utility/visual/color_button/color_button_plugin.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -from qtpy.QtDesigner import QDesignerCustomWidgetInterface -from qtpy.QtWidgets import QWidget - -from bec_widgets.utils.bec_designer import designer_material_icon -from bec_widgets.widgets.utility.visual.color_button.color_button import ColorButton - -DOM_XML = """ - - - - -""" - - -class ColorButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover - def __init__(self): - super().__init__() - self._form_editor = None - - def createWidget(self, parent): - if parent is None: - return QWidget() - t = ColorButton(parent) - return t - - def domXml(self): - return DOM_XML - - def group(self): - return "BEC Visual Utils" - - def icon(self): - return designer_material_icon(ColorButton.ICON_NAME) - - def includeFile(self): - return "color_button" - - def initialize(self, form_editor): - self._form_editor = form_editor - - def isContainer(self): - return False - - def isInitialized(self): - return self._form_editor is not None - - def name(self): - return "ColorButton" - - def toolTip(self): - return "" - - def whatsThis(self): - return self.toolTip() diff --git a/bec_widgets/widgets/utility/visual/color_button/register_color_button.py b/bec_widgets/widgets/utility/visual/color_button/register_color_button.py deleted file mode 100644 index 33df0093..00000000 --- a/bec_widgets/widgets/utility/visual/color_button/register_color_button.py +++ /dev/null @@ -1,17 +0,0 @@ -def main(): # pragma: no cover - from qtpy import PYSIDE6 - - if not PYSIDE6: - print("PYSIDE6 is not available in the environment. Cannot patch designer.") - return - from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - - from bec_widgets.widgets.utility.visual.color_button.color_button_plugin import ( - ColorButtonPlugin, - ) - - QPyDesignerCustomWidgetCollection.addCustomWidget(ColorButtonPlugin()) - - -if __name__ == "__main__": # pragma: no cover - main() 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 7769a44f..07449de0 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 @@ -33,14 +33,17 @@ class ColorButtonNative(QPushButton): self.clicked.connect(self._open_color_dialog) @SafeSlot() - def set_color(self, color: str | QColor): + def set_color(self, color: str | tuple | QColor): """Set the button's color and update its appearance. Args: - color (str or QColor): The new color to assign. + color (str, tuple or QColor): The new color to assign, as a hex string, + an RGB(A) tuple or a QColor. The alpha channel is not shown. """ if isinstance(color, QColor): self._color = color.name() + elif isinstance(color, (tuple, list)): + self._color = QColor(*color).name() else: self._color = color self._update_appearance() diff --git a/tests/unit_tests/test_color_button_native.py b/tests/unit_tests/test_color_button_native.py index a3654668..5ff5d1ac 100644 --- a/tests/unit_tests/test_color_button_native.py +++ b/tests/unit_tests/test_color_button_native.py @@ -24,6 +24,18 @@ def test_color_button_native(qtbot): assert cb.color == new_color.name() +def test_color_button_native_accepts_rgba_tuple(qtbot): + """RGB(A) tuples are shown as their hex code; the alpha channel is dropped.""" + cb = create_widget(qtbot, ColorButtonNative) + + with qtbot.waitSignal(cb.color_changed, timeout=1000) as blocker: + cb.set_color((255, 128, 0, 100)) + + assert cb.color == "#ff8000" + assert cb.text() == "#ff8000" + assert blocker.args == ["#ff8000"] + + 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) diff --git a/tests/unit_tests/test_motor_map_next_gen.py b/tests/unit_tests/test_motor_map_next_gen.py index 152de7e1..b6ac67b7 100644 --- a/tests/unit_tests/test_motor_map_next_gen.py +++ b/tests/unit_tests/test_motor_map_next_gen.py @@ -5,6 +5,7 @@ from qtpy.QtTest import QSignalSpy from bec_widgets.tests.client_mocks import mocked_client from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap +from bec_widgets.widgets.plots.motor_map.settings.motor_map_settings import MotorMapSettings from .conftest import create_widget @@ -373,18 +374,42 @@ def test_motor_map_settings_dialog(qtbot, mocked_client): assert mm.motor_map_settings.widget.ui.scatter_size.value() == 10 assert mm.motor_map_settings.widget.ui.background_value.value() == 50 assert mm.motor_map_settings.widget.ui.num_dim_points.value() == 20 - assert mm.motor_map_settings.widget.ui.color_scatter.get_color(format="RGBA") == ( - 255, - 0, - 0, - 255, - ) + assert mm.motor_map_settings.widget.ui.color_scatter.color == "#ff0000" mm.motor_map_settings.close() qtbot.wait(200) assert mm.motor_map_settings is None +def test_motor_map_settings_color_live_sync(qtbot, mocked_client): + """The side panel color button and the motor map color follow each other.""" + mm = create_widget(qtbot, MotorMap, client=mocked_client) + settings = create_widget(qtbot, MotorMapSettings, target_widget=mm, popup=False) + + mm.color = (0, 0, 255, 255) + assert settings.ui.color_scatter.color == "#0000ff" + + settings.ui.color_scatter.set_color("#00ff00") + assert mm.color == (0, 255, 0, 255) + assert settings.ui.color_scatter.color == "#00ff00" + + +def test_motor_map_settings_color_accept(qtbot, mocked_client): + """Applying the dialog sets a new color and keeps the alpha of an unchanged one.""" + mm = create_widget(qtbot, MotorMap, client=mocked_client) + mm.color = (255, 0, 0, 100) + settings = create_widget(qtbot, MotorMapSettings, target_widget=mm, popup=True) + assert settings.ui.color_scatter.color == "#ff0000" + + settings.accept_changes() + assert mm.color == (255, 0, 0, 100) + + settings.ui.color_scatter.set_color("#00ff00") + assert mm.color == (255, 0, 0, 100) + settings.accept_changes() + assert mm.color == (0, 255, 0, 255) + + def test_motor_map_cleanup_detaches_update_proxy(qtbot, mocked_client): """ A plot update still queued in proxy_update_plot when the MotorMap is cleaned up, or