diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py
index 9388aca7..8b8d3594 100644
--- a/bec_widgets/cli/client.py
+++ b/bec_widgets/cli/client.py
@@ -16,6 +16,7 @@ class Widgets(str, enum.Enum):
"""
AbortButton = "AbortButton"
+ BECColorMapWidget = "BECColorMapWidget"
BECDock = "BECDock"
BECDockArea = "BECDockArea"
BECFigure = "BECFigure"
@@ -34,6 +35,7 @@ class Widgets(str, enum.Enum):
PositionIndicator = "PositionIndicator"
PositionerBox = "PositionerBox"
PositionerControlLine = "PositionerControlLine"
+ PositionerGroup = "PositionerGroup"
ResetButton = "ResetButton"
ResumeButton = "ResumeButton"
RingProgressBar = "RingProgressBar"
@@ -64,6 +66,15 @@ class AbortButton(RPCBase):
"""
+class BECColorMapWidget(RPCBase):
+ @property
+ @rpc_call
+ def colormap(self):
+ """
+ Get the current colormap name.
+ """
+
+
class BECCurve(RPCBase):
@rpc_call
def remove(self):
@@ -2690,6 +2701,16 @@ class PositionerControlLine(RPCBase):
"""
+class PositionerGroup(RPCBase):
+ @rpc_call
+ def set_positioners(self, device_names: "str"):
+ """
+ Redraw grid with positioners from device_names string
+
+ Device names must be separated by space
+ """
+
+
class ResetButton(RPCBase):
@property
@rpc_call
diff --git a/bec_widgets/utils/colors.py b/bec_widgets/utils/colors.py
index 8ce8c609..a6d9d365 100644
--- a/bec_widgets/utils/colors.py
+++ b/bec_widgets/utils/colors.py
@@ -468,7 +468,7 @@ class Colors:
return color
@staticmethod
- def validate_color_map(color_map: str) -> str:
+ def validate_color_map(color_map: str, return_error: bool = True) -> str | bool:
"""
Validate the colormap input if it is supported by pyqtgraph. Can be used in any pydantic model as a field validator. If validation fails it prints all available colormaps from pyqtgraph instance.
@@ -476,13 +476,20 @@ class Colors:
color_map(str): The colormap to be validated.
Returns:
- str: The validated colormap.
+ str: The validated colormap, if colormap is valid.
+ bool: False, if colormap is invalid.
+
+ Raises:
+ PydanticCustomError: If colormap is invalid.
"""
available_colormaps = pg.colormap.listMaps()
if color_map not in available_colormaps:
- raise PydanticCustomError(
- "unsupported colormap",
- f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.",
- {"wrong_value": color_map},
- )
+ if return_error:
+ raise PydanticCustomError(
+ "unsupported colormap",
+ f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.",
+ {"wrong_value": color_map},
+ )
+ else:
+ return False
return color_map
diff --git a/bec_widgets/widgets/colormap_widget/__init__.py b/bec_widgets/widgets/colormap_widget/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bec_widgets/widgets/colormap_widget/bec_color_map_widget.pyproject b/bec_widgets/widgets/colormap_widget/bec_color_map_widget.pyproject
new file mode 100644
index 00000000..02243eb8
--- /dev/null
+++ b/bec_widgets/widgets/colormap_widget/bec_color_map_widget.pyproject
@@ -0,0 +1 @@
+{'files': ['colormap_widget.py']}
\ No newline at end of file
diff --git a/bec_widgets/widgets/colormap_widget/bec_color_map_widget_plugin.py b/bec_widgets/widgets/colormap_widget/bec_color_map_widget_plugin.py
new file mode 100644
index 00000000..4b648f3d
--- /dev/null
+++ b/bec_widgets/widgets/colormap_widget/bec_color_map_widget_plugin.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from qtpy.QtDesigner import QDesignerCustomWidgetInterface
+
+from bec_widgets.utils.bec_designer import designer_material_icon
+from bec_widgets.widgets.colormap_widget.colormap_widget import BECColorMapWidget
+
+DOM_XML = """
+
+
+
+
+"""
+
+
+class BECColorMapWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
+ def __init__(self):
+ super().__init__()
+ self._form_editor = None
+
+ def createWidget(self, parent):
+ t = BECColorMapWidget(parent)
+ return t
+
+ def domXml(self):
+ return DOM_XML
+
+ def group(self):
+ return "BEC Buttons"
+
+ def icon(self):
+ return designer_material_icon(BECColorMapWidget.ICON_NAME)
+
+ def includeFile(self):
+ return "bec_color_map_widget"
+
+ 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 "BECColorMapWidget"
+
+ def toolTip(self):
+ return "BECColorMapWidget"
+
+ def whatsThis(self):
+ return self.toolTip()
diff --git a/bec_widgets/widgets/colormap_widget/colormap_widget.py b/bec_widgets/widgets/colormap_widget/colormap_widget.py
new file mode 100644
index 00000000..e6af4ee1
--- /dev/null
+++ b/bec_widgets/widgets/colormap_widget/colormap_widget.py
@@ -0,0 +1,73 @@
+from pyqtgraph.widgets.ColorMapButton import ColorMapButton
+from qtpy.QtCore import Property, Signal, Slot
+from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
+
+from bec_widgets.utils import Colors
+from bec_widgets.utils.bec_widget import BECWidget
+
+
+class BECColorMapWidget(BECWidget, QWidget):
+ colormap_changed_signal = Signal(str)
+ ICON_NAME = "palette"
+ USER_ACCESS = ["colormap"]
+
+ def __init__(self, parent=None, cmap: str = "magma"):
+ super().__init__()
+ QWidget.__init__(self, parent=parent)
+
+ # Create the ColorMapButton
+ self.button = ColorMapButton()
+
+ # Set the size policy and minimum width
+ size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
+ self.button.setSizePolicy(size_policy)
+ self.button.setMinimumWidth(100)
+ self.button.setMinimumHeight(30)
+
+ # Create the layout
+ self.layout = QVBoxLayout(self)
+ self.layout.addWidget(self.button)
+ self.layout.setSpacing(0)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+
+ # Set the initial colormap
+ self.button.setColorMap(cmap)
+ self._cmap = cmap
+
+ # Connect the signal
+ self.button.sigColorMapChanged.connect(self.colormap_changed)
+
+ @Property(str)
+ def colormap(self):
+ """Get the current colormap name."""
+ return self._cmap
+
+ @colormap.setter
+ def colormap(self, name):
+ """Set the colormap by name."""
+ if self._cmap != name:
+ if Colors.validate_color_map(name, return_error=False) is False:
+ return
+ self.button.setColorMap(name)
+ self._cmap = name
+ self.colormap_changed_signal.emit(name)
+
+ @Slot()
+ def colormap_changed(self):
+ """
+ Emit the colormap changed signal with the current colormap selected in the button.
+ """
+ cmap = self.button.colorMap().name
+ self._cmap = cmap
+ self.colormap_changed_signal.emit(cmap)
+
+
+if __name__ == "__main__": # pragma: no cover
+ import sys
+
+ from qtpy.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+ window = BECColorMapWidget()
+ window.show()
+ sys.exit(app.exec())
diff --git a/bec_widgets/widgets/colormap_widget/register_bec_color_map_widget.py b/bec_widgets/widgets/colormap_widget/register_bec_color_map_widget.py
new file mode 100644
index 00000000..fbf05edf
--- /dev/null
+++ b/bec_widgets/widgets/colormap_widget/register_bec_color_map_widget.py
@@ -0,0 +1,17 @@
+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.colormap_widget.bec_color_map_widget_plugin import (
+ BECColorMapWidgetPlugin,
+ )
+
+ QPyDesignerCustomWidgetCollection.addCustomWidget(BECColorMapWidgetPlugin())
+
+
+if __name__ == "__main__": # pragma: no cover
+ main()
diff --git a/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.py b/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.py
index c7f3ea80..4b924bb3 100644
--- a/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.py
+++ b/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.py
@@ -53,7 +53,7 @@ class CurveSettings(SettingWidget):
x_entry = self.target_widget.waveform._x_axis_mode["entry"]
self._enable_ui_elements(x_name, x_entry)
cm = self.target_widget.config.color_palette
- self.ui.color_map_selector_scan.combo.setCurrentText(cm)
+ self.ui.color_map_selector_scan.colormap = cm
# Scan Curve Table
for source in ["scan_segment", "async"]:
@@ -115,10 +115,10 @@ class CurveSettings(SettingWidget):
@Slot()
def change_colormap(self, target: Literal["scan", "dap"]):
if target == "scan":
- cm = self.ui.color_map_selector_scan.combo.currentText()
+ cm = self.ui.color_map_selector_scan.colormap
table = self.ui.scan_table
if target == "dap":
- cm = self.ui.color_map_selector_dap.combo.currentText()
+ cm = self.ui.color_map_selector_dap.colormap
table = self.ui.dap_table
rows = table.rowCount()
colors = Colors.golden_angle_color(colormap=cm, num=max(10, rows + 1), format="HEX")
diff --git a/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.ui b/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.ui
index 62a36f47..1f087e90 100644
--- a/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.ui
+++ b/bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.ui
@@ -231,7 +231,14 @@
-
-
+
+
+
+ 0
+ 0
+
+
+
@@ -330,7 +337,14 @@
-
-
+
+
+
+ 0
+ 0
+
+
+
@@ -348,9 +362,9 @@
- ColormapSelector
+ BECColorMapWidget
QWidget
-
+
diff --git a/docs/user/widgets/buttons_appearance/buttons_appearance.md b/docs/user/widgets/buttons_appearance/buttons_appearance.md
index cba5c2e9..31af3c8a 100644
--- a/docs/user/widgets/buttons_appearance/buttons_appearance.md
+++ b/docs/user/widgets/buttons_appearance/buttons_appearance.md
@@ -39,6 +39,17 @@ The `Colormap Selector` is a specialized combobox that allows users to select a
**Key Features:**
- **Colormap Selection**: Provides a dropdown to select from all available colormaps in `pyqtgraph`.
- **Visual Preview**: Displays a small preview of the colormap next to its name, enhancing usability.
+
+## Colormap Button
+
+The `Colormap Button` is a custom widget that displays the current colormap and, upon clicking, shows a nested menu for selecting a different colormap. It integrates the `ColorMapMenu` from `pyqtgraph`, providing an intuitive and interactive way for users to choose colormaps within the GUI.
+
+**Key Features:**
+- **Current Colormap Display**: Shows the name and a gradient icon of the current colormap directly on the button.
+- **Nested Menu Selection**: Offers a nested menu with categorized colormaps, making it easy to find and select the desired colormap.
+- **Signal Emission**: Emits a signal when the colormap changes, providing the new colormap name as a string.
+- **Qt Designer Integration**: Exposes properties and signals to be used within Qt Designer, allowing for customization within the designer interface.
+- **Resizable and Styled**: Features adjustable size policies and styles to match the look and feel of standard `QPushButton` widgets, including rounded edges.
`````
````{tab} Examples
@@ -104,6 +115,33 @@ class MyGui(QWidget):
my_gui = MyGui()
my_gui.show()
```
+
+## Example 4 - Adding a Colormap Button
+
+```python
+from qtpy.QtWidgets import QWidget, QVBoxLayout
+from bec_widgets.widgets.buttons import ColormapButton
+
+class MyGui(QWidget):
+ def __init__(self):
+ super().__init__()
+ self.setLayout(QVBoxLayout(self))
+
+ # Create and add the ColormapButton to the layout
+ self.colormap_button = ColormapButton()
+ self.layout().addWidget(self.colormap_button)
+
+ # Connect the signal to handle colormap changes
+ self.colormap_button.colormap_changed_signal.connect(self.on_colormap_changed)
+
+ def on_colormap_changed(self, colormap_name):
+ print(f"Selected colormap: {colormap_name}")
+
+# Example of how this custom GUI might be used:
+my_gui = MyGui()
+my_gui.show()
+```
+
````
````{tab} API
diff --git a/tests/unit_tests/test_colormap_widget.py b/tests/unit_tests/test_colormap_widget.py
new file mode 100644
index 00000000..553cecd2
--- /dev/null
+++ b/tests/unit_tests/test_colormap_widget.py
@@ -0,0 +1,69 @@
+import pytest
+from pyqtgraph.widgets.ColorMapButton import ColorMapButton
+
+from bec_widgets.widgets.colormap_widget.colormap_widget import BECColorMapWidget
+
+
+@pytest.fixture
+def color_map_widget(qtbot):
+ widget = BECColorMapWidget()
+ qtbot.addWidget(widget)
+ qtbot.waitExposed(widget)
+ yield widget
+
+
+def test_color_map_widget_init(color_map_widget):
+ """Test that the widget initializes correctly."""
+ assert color_map_widget is not None
+ assert isinstance(color_map_widget, BECColorMapWidget)
+ assert color_map_widget.colormap == "magma"
+ assert isinstance(color_map_widget.button, ColorMapButton)
+ # Check that the button has the correct initial colormap
+ assert color_map_widget.button.colorMap().name == "magma"
+
+
+def test_color_map_widget_set_valid_colormap(color_map_widget):
+ """
+ Test setting a valid colormap.
+ """
+ new_cmap = "viridis"
+ color_map_widget.colormap = new_cmap
+ assert color_map_widget.colormap == new_cmap
+ assert color_map_widget.button.colorMap().name == new_cmap
+
+
+def test_color_map_widget_set_invalid_colormap(color_map_widget):
+ """Test setting an invalid colormap."""
+ invalid_cmap = "invalid_colormap_name"
+ old_cmap = color_map_widget.colormap
+ color_map_widget.colormap = invalid_cmap
+ # Since invalid, the colormap should not change
+ assert color_map_widget.colormap == old_cmap
+ assert color_map_widget.button.colorMap().name == old_cmap
+
+
+def test_color_map_widget_signal_emitted(color_map_widget, qtbot):
+ """Test that the signal is emitted when the colormap changes."""
+ new_cmap = "plasma"
+ with qtbot.waitSignal(color_map_widget.colormap_changed_signal, timeout=1000) as blocker:
+ color_map_widget.colormap = new_cmap
+ assert blocker.signal_triggered
+ assert blocker.args == [new_cmap]
+ assert color_map_widget.colormap == new_cmap
+
+
+def test_color_map_widget_signal_not_emitted_for_invalid_colormap(color_map_widget, qtbot):
+ """Test that the signal is not emitted when an invalid colormap is set."""
+ invalid_cmap = "invalid_colormap_name"
+ with qtbot.assertNotEmitted(color_map_widget.colormap_changed_signal):
+ color_map_widget.colormap = invalid_cmap
+ # The colormap should remain unchanged
+ assert color_map_widget.colormap == "magma"
+
+
+def test_color_map_widget_resize(color_map_widget):
+ """Test that the widget resizes properly."""
+ width, height = 200, 50
+ color_map_widget.resize(width, height)
+ assert color_map_widget.width() == width
+ assert color_map_widget.height() == height