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"
|
||||
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
|
||||
|
@ -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:
|
||||
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
|
||||
|
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"]
|
||||
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")
|
||||
|
@ -231,7 +231,14 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
</layout>
|
||||
</widget>
|
||||
@ -330,7 +337,14 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
</layout>
|
||||
</widget>
|
||||
@ -348,9 +362,9 @@
|
||||
<header>device_line_edit</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ColormapSelector</class>
|
||||
<class>BECColorMapWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>colormap_selector</header>
|
||||
<header>bec_color_map_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
|
@ -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
|
||||
|
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