0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat(colormap_button): colormap button with menu to select colormap filtered by the colormap type

This commit is contained in:
2024-10-27 14:42:44 +01:00
parent d8c80293c7
commit b039933405
11 changed files with 308 additions and 14 deletions

View File

@ -16,6 +16,7 @@ class Widgets(str, enum.Enum):
""" """
AbortButton = "AbortButton" AbortButton = "AbortButton"
BECColorMapWidget = "BECColorMapWidget"
BECDock = "BECDock" BECDock = "BECDock"
BECDockArea = "BECDockArea" BECDockArea = "BECDockArea"
BECFigure = "BECFigure" BECFigure = "BECFigure"
@ -34,6 +35,7 @@ class Widgets(str, enum.Enum):
PositionIndicator = "PositionIndicator" PositionIndicator = "PositionIndicator"
PositionerBox = "PositionerBox" PositionerBox = "PositionerBox"
PositionerControlLine = "PositionerControlLine" PositionerControlLine = "PositionerControlLine"
PositionerGroup = "PositionerGroup"
ResetButton = "ResetButton" ResetButton = "ResetButton"
ResumeButton = "ResumeButton" ResumeButton = "ResumeButton"
RingProgressBar = "RingProgressBar" 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): class BECCurve(RPCBase):
@rpc_call @rpc_call
def remove(self): 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): class ResetButton(RPCBase):
@property @property
@rpc_call @rpc_call

View File

@ -468,7 +468,7 @@ class Colors:
return color return color
@staticmethod @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. 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. color_map(str): The colormap to be validated.
Returns: 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() available_colormaps = pg.colormap.listMaps()
if color_map not in available_colormaps: if color_map not in available_colormaps:
raise PydanticCustomError( if return_error:
"unsupported colormap", raise PydanticCustomError(
f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.", "unsupported colormap",
{"wrong_value": color_map}, 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 return color_map

View File

@ -0,0 +1 @@
{'files': ['colormap_widget.py']}

View File

@ -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 = """
<ui language='c++'>
<widget class='BECColorMapWidget' name='bec_color_map_widget'>
</widget>
</ui>
"""
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()

View File

@ -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())

View File

@ -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()

View File

@ -53,7 +53,7 @@ class CurveSettings(SettingWidget):
x_entry = self.target_widget.waveform._x_axis_mode["entry"] x_entry = self.target_widget.waveform._x_axis_mode["entry"]
self._enable_ui_elements(x_name, x_entry) self._enable_ui_elements(x_name, x_entry)
cm = self.target_widget.config.color_palette 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 # Scan Curve Table
for source in ["scan_segment", "async"]: for source in ["scan_segment", "async"]:
@ -115,10 +115,10 @@ class CurveSettings(SettingWidget):
@Slot() @Slot()
def change_colormap(self, target: Literal["scan", "dap"]): def change_colormap(self, target: Literal["scan", "dap"]):
if target == "scan": 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 table = self.ui.scan_table
if target == "dap": 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 table = self.ui.dap_table
rows = table.rowCount() rows = table.rowCount()
colors = Colors.golden_angle_color(colormap=cm, num=max(10, rows + 1), format="HEX") colors = Colors.golden_angle_color(colormap=cm, num=max(10, rows + 1), format="HEX")

View File

@ -231,7 +231,14 @@
</widget> </widget>
</item> </item>
<item row="0" column="3"> <item row="0" column="3">
<widget class="ColormapSelector" name="color_map_selector_scan"/> <widget class="BECColorMapWidget" name="color_map_selector_scan">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -330,7 +337,14 @@
</widget> </widget>
</item> </item>
<item row="0" column="3"> <item row="0" column="3">
<widget class="ColormapSelector" name="color_map_selector_dap"/> <widget class="BECColorMapWidget" name="color_map_selector_dap">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -348,9 +362,9 @@
<header>device_line_edit</header> <header>device_line_edit</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>ColormapSelector</class> <class>BECColorMapWidget</class>
<extends>QWidget</extends> <extends>QWidget</extends>
<header>colormap_selector</header> <header>bec_color_map_widget</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources/> <resources/>

View File

@ -39,6 +39,17 @@ The `Colormap Selector` is a specialized combobox that allows users to select a
**Key Features:** **Key Features:**
- **Colormap Selection**: Provides a dropdown to select from all available colormaps in `pyqtgraph`. - **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. - **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 ````{tab} Examples
@ -104,6 +115,33 @@ class MyGui(QWidget):
my_gui = MyGui() my_gui = MyGui()
my_gui.show() 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 ````{tab} API

View File

@ -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