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:
@ -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
|
||||||
|
@ -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
|
||||||
|
0
bec_widgets/widgets/colormap_widget/__init__.py
Normal file
0
bec_widgets/widgets/colormap_widget/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
{'files': ['colormap_widget.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 = """
|
||||||
|
<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()
|
73
bec_widgets/widgets/colormap_widget/colormap_widget.py
Normal file
73
bec_widgets/widgets/colormap_widget/colormap_widget.py
Normal 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())
|
@ -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()
|
@ -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")
|
||||||
|
@ -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/>
|
||||||
|
@ -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
|
||||||
|
69
tests/unit_tests/test_colormap_widget.py
Normal file
69
tests/unit_tests/test_colormap_widget.py
Normal 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
|
Reference in New Issue
Block a user