refactor(color_button): merge ColorButton into ColorButtonNative

This commit is contained in:
wyzula_j authored and Jan Wyzula committed 2026-10-16 16:32:27 +02:00
1 parent 68d5093002
commit 824979f7c4
11 files changed
+70 -168

No files matched your search

-2
View File
@@ -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",
@@ -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
@@ -104,15 +104,15 @@
</widget>
</item>
<item row="5" column="1">
<widget class="ColorButton" name="color_scatter"/>
<widget class="ColorButtonNative" name="color_scatter"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ColorButton</class>
<extends>QWidget</extends>
<header>color_button</header>
<class>ColorButtonNative</class>
<extends>QPushButton</extends>
<header>color_button_native</header>
</customwidget>
</customwidgets>
<resources/>
Whitespace-only changes.
@@ -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()
@@ -1 +0,0 @@
{'files': ['color_button.py']}
@@ -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 = """
<ui language='c++'>
<widget class='ColorButton' name='color_button'>
</widget>
</ui>
"""
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()
@@ -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()
@@ -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()
@@ -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)
+31 -6
View File
@@ -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