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

feat(position_indicator): improved design and added more customization options

This commit is contained in:
2024-09-14 18:33:00 +02:00
parent 5557bfe717
commit d15b22250f
3 changed files with 293 additions and 34 deletions

View File

@ -1,67 +1,284 @@
from qtpy.QtCore import Qt, Slot import numpy as np
from qtpy.QtGui import QPainter, QPen from qtpy.QtCore import Property, QSize, Qt, Slot
from qtpy.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen
from qtpy.QtWidgets import QWidget from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import get_accent_colors, get_theme_palette
class PositionIndicator(QWidget):
class PositionIndicator(BECWidget, QWidget):
ICON_NAME = "horizontal_distribute" ICON_NAME = "horizontal_distribute"
def __init__(self, parent=None): def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(parent) super().__init__(client=client, config=config, gui_id=gui_id)
self.position = 0.5 QWidget.__init__(self, parent=parent)
self.position = 50
self.min_value = 0 self.min_value = 0
self.max_value = 100 self.max_value = 100
self.scaling_factor = 0.5 self.scaling_factor = 0.5
self.setMinimumHeight(10) self.is_vertical = False
self._current_indicator_position = 0
self._draw_position = 0
self._rounded_corners = 10
self._indicator_width = 2
self._indicator_color = get_accent_colors().success
self._background_color = get_theme_palette().mid().color()
self._use_color_palette = True
def set_range(self, min_value, max_value): def set_range(self, min_value: float, max_value: float):
"""
Set the range of the position indicator
Args:
min_value(float): Minimum value of the range
max_value(float): Maximum value of the range
"""
self.minimum = min_value
self.maximum = max_value
@Property(float)
def minimum(self):
"""
Property to get the minimum value of the position indicator
"""
return self.min_value
@minimum.setter
def minimum(self, min_value: float):
"""
Setter for the minimum property
Args:
min_value: The minimum value of the position indicator
"""
self.min_value = min_value self.min_value = min_value
self.max_value = max_value self.update()
@Property(float)
def maximum(self):
"""
Property to get the maximum value of the position indicator
"""
return self.max_value
@maximum.setter
def maximum(self, max_value: float):
"""
Setter for the maximum property
Args:
max_value: The maximum value of the position indicator
"""
self.max_value = max_value
self.update()
@Property(bool)
def vertical(self):
"""
Property to determine the orientation of the position indicator
"""
return self.is_vertical
@vertical.setter
def vertical(self, is_vertical: bool):
"""
Setter for the vertical property
Args:
is_vertical: True if the indicator should be vertical, False if horizontal
"""
self.is_vertical = is_vertical
self.update()
@Property(float)
def value(self):
"""
Property to get the current value of the position indicator
"""
return self.position
@value.setter
def value(self, position: float):
"""
Setter for the value property
Args:
position: The new position of the indicator
"""
self.set_value(position)
@Property(int)
def indicator_width(self):
"""
Property to get the width of the indicator
"""
return self._indicator_width
@indicator_width.setter
def indicator_width(self, width: int):
"""
Setter for the indicator width property
Args:
width: The new width of the indicator
"""
self._indicator_width = width
self.update()
@Property(int)
def rounded_corners(self):
"""
Property to get the rounded corners of the position indicator
"""
return self._rounded_corners
@rounded_corners.setter
def rounded_corners(self, value: int):
"""
Setter for the rounded corners property
Args:
value: The new value for the rounded corners
"""
self._rounded_corners = value
self.update()
@Property(QColor)
def indicator_color(self):
"""
Property to get the color of the indicator
"""
return self._indicator_color
@indicator_color.setter
def indicator_color(self, color: QColor):
"""
Setter for the indicator color property
Args:
color: The new color for the indicator
"""
self._indicator_color = color
self.update()
@Property(QColor)
def background_color(self):
"""
Property to get the background color of the position indicator
"""
return self._background_color
@background_color.setter
def background_color(self, color: QColor):
"""
Setter for the background color property
Args:
color: The new background color
"""
self._background_color = color
self.update()
@Property(bool)
def use_color_palette(self):
"""
Property to determine if the indicator should use the color palette or the custom color.
"""
return self._use_color_palette
@use_color_palette.setter
def use_color_palette(self, use_palette: bool):
"""
Setter for the use color palette property
Args:
use_palette: True if the indicator should use the color palette, False if custom color
"""
self._use_color_palette = use_palette
self.update()
# @Property(float)
@Slot(int)
@Slot(float) @Slot(float)
def on_position_update(self, position: float): def set_value(self, position: float):
self.position = position self.position = position
self.update() self.update()
def _get_indicator_color(self):
if self._use_color_palette:
return get_accent_colors().success
return self._indicator_color
def _get_background_brush(self):
if self._use_color_palette:
return get_theme_palette().mid()
return QBrush(self._background_color)
def paintEvent(self, event): def paintEvent(self, event):
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
width = self.width() width = self.width()
height = self.height() height = self.height()
# Draw horizontal line # Set up the brush for the background
painter.setPen(Qt.black) painter.setBrush(self._get_background_brush())
painter.drawLine(0, height // 2, width, height // 2)
# Draw shorter vertical line at the current position # Create a QPainterPath with a rounded rectangle for clipping
x_pos = int(self.position * width) path = QPainterPath()
painter.setPen(QPen(Qt.red, 2)) path.addRoundedRect(0, 0, width, height, self._rounded_corners, self._rounded_corners)
short_line_height = int(height * self.scaling_factor)
painter.drawLine( # Set clipping to the rounded rectangle
x_pos, painter.setClipPath(path)
(height // 2) - (short_line_height // 2),
x_pos, # Draw the rounded rectangle background first
(height // 2) + (short_line_height // 2), painter.setPen(Qt.NoPen)
painter.drawRoundedRect(0, 0, width, height, self._rounded_corners, self._rounded_corners)
# get the position scaled to the defined min and max values
self._current_indicator_position = position = np.interp(
self.position, [self.min_value, self.max_value], [0, 100]
) )
# Draw thicker vertical lines at the ends if self.is_vertical:
end_line_pen = QPen(Qt.blue, 5) # If vertical, rotate the coordinate system by -90 degrees
painter.setPen(end_line_pen) painter.translate(width // 2, height // 2) # Move origin to center
painter.drawLine(0, 0, 0, height) painter.rotate(-90) # Rotate by -90 degrees for vertical drawing
painter.drawLine(width - 1, 0, width - 1, height) painter.translate(-height // 2, -width // 2) # Restore the origin for drawing
# Switch width and height for the vertical orientation
width, height = height, width
# Draw the moving vertical indicator, respecting the clip path
self._draw_position = x_pos = round(
position * width / 100
) # Position for the vertical line
indicator_pen = QPen(self._get_indicator_color(), self._indicator_width)
painter.setPen(indicator_pen)
painter.drawLine(x_pos, 0, x_pos, height)
painter.end()
def minimumSizeHint(self):
# Set the smallest possible size
return QSize(10, 10)
if __name__ == "__main__": if __name__ == "__main__": # pragma: no cover
from bec_qthemes import setup_theme
from qtpy.QtWidgets import QApplication, QSlider, QVBoxLayout from qtpy.QtWidgets import QApplication, QSlider, QVBoxLayout
app = QApplication([]) app = QApplication([])
setup_theme("dark")
# Create position indicator and slider
position_indicator = PositionIndicator() position_indicator = PositionIndicator()
# position_indicator.set_range(0, 1)
slider = QSlider(Qt.Horizontal) slider = QSlider(Qt.Horizontal)
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100)) slider.valueChanged.connect(lambda value: position_indicator.set_value(value))
position_indicator.is_vertical = False
# position_indicator.set_value(100)
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(position_indicator) layout.addWidget(position_indicator)
layout.addWidget(slider) layout.addWidget(slider)

View File

@ -249,7 +249,7 @@ class PositionerBox(BECWidget, QWidget):
self.update_limits(limits) self.update_limits(limits)
if limits is not None and readback_val is not None and limits[0] != limits[1]: if limits is not None and readback_val is not None and limits[0] != limits[1]:
pos = (readback_val - limits[0]) / (limits[1] - limits[0]) pos = (readback_val - limits[0]) / (limits[1] - limits[0])
self.ui.position_indicator.on_position_update(pos) self.ui.position_indicator.set_value(pos)
def update_limits(self, limits: tuple): def update_limits(self, limits: tuple):
"""Update limits """Update limits

View File

@ -0,0 +1,42 @@
import pytest
from bec_widgets.widgets.position_indicator.position_indicator import PositionIndicator
@pytest.fixture
def position_indicator(qtbot):
"""Fixture for PositionIndicator widget"""
pi = PositionIndicator()
qtbot.addWidget(pi)
qtbot.waitExposed(pi)
return pi
def test_position_indicator_set_range(position_indicator):
"""
Test set_range method of PositionIndicator
"""
position_indicator.set_range(0, 20)
assert position_indicator.minimum == 0
assert position_indicator.maximum == 20
def test_position_indicator_set_value(position_indicator):
"""
Test set_value method of PositionIndicator and the correct mapping of the value
within the paintEvent method
"""
# pylint: disable=protected-access
position_indicator.set_value(50)
assert position_indicator.position == 50
position_indicator.paintEvent(None)
assert position_indicator._current_indicator_position == 50
position_indicator.set_value(100)
position_indicator.paintEvent(None)
assert position_indicator._draw_position == position_indicator.width()
position_indicator.vertical = True
position_indicator.paintEvent(None)
assert position_indicator._draw_position == position_indicator.height()