from .base import PyDMWidget, TextFormatter
from qtpy.QtGui import QColor, QPolygon, QPen, QPainter, QPaintEvent
from qtpy.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QWidget, QGridLayout
from qtpy.QtCore import Qt, QPoint, Property
from qtpy.QtWidgets import QWIDGETSIZE_MAX
from typing import Optional
class QScale(QFrame):
"""
A bar-shaped indicator for scalar value.
Configurable features include indicator type (bar/pointer), scale tick
marks and orientation (horizontal/vertical).
Parameters
----------
parent : QWidget
The parent widget for the Scale
"""
def __init__(self, parent: Optional[QWidget] = None) -> None:
super(QScale, self).__init__(parent)
self._value = 1
self._lower_limit = -5
self._upper_limit = 5
self.position = None # unit: pixel
self._bg_color = QColor("darkgray")
self._bg_size_rate = 0.8 # from 0 to 1
self._indicator_color = QColor("black")
self._pointer_width_rate = 0.05
self._barIndicator = False
self._num_divisions = 10
self._show_ticks = True
self._tick_pen = QPen()
self._tick_color = QColor("black")
self._tick_width = 0
self._tick_size_rate = 0.1 # from 0 to 1
self._painter = QPainter()
self._painter_rotation = None
self._painter_translation_y = None
self._painter_translation_x = None
self._painter_scale_x = None
self._flip_traslation_y = None
self._flip_scale_y = None
self._widget_width = self.width()
self._widget_height = self.height()
self._orientation = Qt.Horizontal
self._inverted_appearance = False
self._flip_scale = False
self._scale_height = 35
self._origin_at_zero = False
self._origin_position = 0
self.set_position()
def adjust_transformation(self) -> None:
"""
This method sets parameters for the widget transformations (needed to for
orientation, flipping and appearance inversion).
"""
self.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) # Unset fixed size
if self._orientation == Qt.Horizontal:
self._widget_width = self.width()
self._widget_height = self.height()
self._painter_translation_y = 0
self._painter_rotation = 0
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.setFixedHeight(self._scale_height)
elif self._orientation == Qt.Vertical:
# Invert dimensions for paintEvent()
self._widget_width = self.height()
self._widget_height = self.width()
self._painter_translation_y = self._widget_width
self._painter_rotation = -90
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.setFixedWidth(self._scale_height)
if self._inverted_appearance:
self._painter_translation_x = self._widget_width
self._painter_scale_x = -1
else:
self._painter_translation_x = 0
self._painter_scale_x = 1
if self._flip_scale:
self._flip_traslation_y = self._widget_height
self._flip_scale_y = -1
else:
self._flip_traslation_y = 0
self._flip_scale_y = 1
def set_tick_pen(self) -> None:
"""
Define pen style for drawing scale tick marks.
"""
self._tick_pen.setColor(self._tick_color)
self._tick_pen.setWidth(self._tick_width)
def draw_ticks(self) -> None:
"""
Draw tick marks on the scale.
"""
if not self._show_ticks:
return
self.set_tick_pen()
self._painter.setPen(self._tick_pen)
division_size = self._widget_width / self._num_divisions
tick_y0 = self._widget_height
tick_yf = int((1 - self._tick_size_rate) * self._widget_height)
for i in range(self._num_divisions + 1):
x = int(i * division_size)
self._painter.drawLine(x, tick_y0, x, tick_yf) # x1, y1, x2, y2
def draw_bar(self) -> None:
"""
Draw a bar as indicator of current value.
"""
self.set_origin()
self.set_position()
if self.position < 0 or self.position > self._widget_width:
return
self._painter.setPen(Qt.transparent)
self._painter.setBrush(self._indicator_color)
bar_width = self.position - self._origin_position
bar_height = int(self._bg_size_rate * self._widget_height)
self._painter.drawRect(self._origin_position, 0, bar_width, bar_height)
def draw_pointer(self) -> None:
"""
Draw a pointer as indicator of current value.
"""
self.set_position()
if self.position < 0 or self.position > self._widget_width:
return
self._painter.setPen(Qt.transparent)
self._painter.setBrush(self._indicator_color)
pointer_width = int(self._pointer_width_rate * self._widget_width)
pointer_height = int(self._bg_size_rate * self._widget_height)
points = [
QPoint(self.position, 0),
QPoint(self.position + pointer_width // 2, pointer_height // 2),
QPoint(self.position, pointer_height),
QPoint(self.position - pointer_width // 2, pointer_height // 2),
]
self._painter.drawPolygon(QPolygon(points))
def draw_indicator(self) -> None:
"""
Draw the selected indicator for current value.
"""
if self._barIndicator:
self.draw_bar()
else:
self.draw_pointer()
def draw_background(self) -> None:
"""
Draw the background of the scale.
"""
self._painter.setPen(Qt.transparent)
self._painter.setBrush(self._bg_color)
bg_width = int(self._widget_width)
bg_height = int(self._bg_size_rate * self._widget_height)
self._painter.drawRect(0, 0, bg_width, bg_height)
def paintEvent(self, event: QPaintEvent) -> None:
"""
Paint events are sent to widgets that need to update themselves,
for instance when part of a widget is exposed because a covering
widget was moved.
Parameters
----------
event : QPaintEvent
"""
self.adjust_transformation()
self._painter.begin(self)
self._painter.translate(0, self._painter_translation_y) # Draw vertically if needed
self._painter.rotate(self._painter_rotation)
self._painter.translate(self._painter_translation_x, 0) # Invert appearance if needed
self._painter.scale(self._painter_scale_x, 1)
self._painter.translate(0, self._flip_traslation_y) # Invert scale if needed
self._painter.scale(1, self._flip_scale_y)
self._painter.setRenderHint(QPainter.Antialiasing)
self.draw_background()
self.draw_ticks()
self.draw_indicator()
self._painter.end()
def calculate_position_for_value(self, value: float) -> int:
"""
Calculate the position (pixel) in which the pointer should be drawn for a given value.
"""
if (
value is None
or value < self._lower_limit
or value > self._upper_limit
or self._upper_limit - self._lower_limit == 0
):
proportion = -1 # Invalid
else:
proportion = (value - self._lower_limit) / (self._upper_limit - self._lower_limit)
position = int(proportion * self._widget_width)
return position
def set_origin(self) -> None:
"""
Set the position (pixel) in which the origin should be drawn.
"""
if self._origin_at_zero:
self._origin_position = self.calculate_position_for_value(0)
else:
self._origin_position = 0
def set_position(self) -> None:
"""
Set the position (pixel) in which the pointer should be drawn.
"""
self.position = self.calculate_position_for_value(self._value)
def update_indicator(self) -> None:
"""
Update the position and the drawing of indicator.
"""
self.set_position()
self.repaint()
def set_value(self, value: float) -> None:
"""
Set a new current value for the indicator.
"""
self._value = value
self.update_indicator()
def set_upper_limit(self, new_limit: float) -> None:
"""
Set the scale upper limit.
Parameters
----------
new_limit : float
The upper limit of the scale.
"""
self._upper_limit = new_limit
def set_lower_limit(self, new_limit: float) -> None:
"""
Set the scale lower limit.
Parameters
----------
new_limit : float
The lower limit of the scale.
"""
self._lower_limit = new_limit
def get_show_ticks(self) -> bool:
return self._show_ticks
def set_show_ticks(self, checked: bool) -> None:
if self._show_ticks != bool(checked):
self._show_ticks = checked
self.repaint()
def get_orientation(self) -> Qt.Orientation:
return self._orientation
def set_orientation(self, orientation: Qt.Orientation) -> None:
self._orientation = orientation
self.adjust_transformation()
self.repaint()
def get_flip_scale(self) -> bool:
return self._flip_scale
def set_flip_scale(self, checked: bool) -> None:
self._flip_scale = bool(checked)
self.adjust_transformation()
self.repaint()
def get_inverted_appearance(self) -> bool:
return self._inverted_appearance
def set_inverted_appearance(self, inverted: bool) -> None:
self._inverted_appearance = inverted
self.adjust_transformation()
self.repaint()
def get_bar_indicator(self) -> bool:
return self._barIndicator
def set_bar_indicator(self, checked: bool) -> None:
if self._barIndicator != bool(checked):
self._barIndicator = checked
self.repaint()
def get_background_color(self) -> QColor:
return self._bg_color
def set_background_color(self, color: QColor) -> None:
self._bg_color = color
self.repaint()
def get_indicator_color(self) -> QColor:
return self._indicator_color
def set_indicator_color(self, color: QColor) -> None:
self._indicator_color = color
self.repaint()
def get_tick_color(self) -> QColor:
return self._tick_color
def set_tick_color(self, color: QColor) -> None:
self._tick_color = color
self.repaint()
def get_background_size_rate(self) -> float:
return self._bg_size_rate
def set_background_size_rate(self, rate: float) -> None:
if rate >= 0 and rate <= 1 and self._bg_size_rate != rate:
self._bg_size_rate = rate
self.repaint()
def get_tick_size_rate(self) -> float:
return self._tick_size_rate
def set_tick_size_rate(self, rate: float) -> None:
if rate >= 0 and rate <= 1 and self._tick_size_rate != rate:
self._tick_size_rate = rate
self.repaint()
def get_num_divisions(self) -> int:
return self._num_divisions
def set_num_divisions(self, divisions: int) -> None:
if isinstance(divisions, int) and divisions > 0 and self._num_divisions != divisions:
self._num_divisions = divisions
self.repaint()
def get_scale_height(self) -> int:
return self._scale_height
def set_scale_height(self, value: int) -> None:
self._scale_height = int(value)
self.adjust_transformation()
self.repaint()
def get_origin_at_zero(self) -> bool:
return self._origin_at_zero
def set_origin_at_zero(self, checked: bool) -> None:
if self._origin_at_zero != bool(checked):
self._origin_at_zero = checked
self.repaint()
[docs]class PyDMScaleIndicator(QFrame, TextFormatter, PyDMWidget):
"""
A bar-shaped indicator for scalar value with support for Channels and
more from PyDM.
Configurable features include indicator type (bar/pointer), scale tick
marks and orientation (horizontal/vertical).
Parameters
----------
parent : QWidget
The parent widget for the Scale
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent: Optional[QWidget] = None, init_channel: Optional[str] = None) -> None:
QFrame.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
self._show_value = True
self._show_limits = True
self.scale_indicator = QScale()
self.value_label = QLabel()
self.lower_label = QLabel()
self.upper_label = QLabel()
self.value_label.setText("<val>")
self.lower_label.setText("<min>")
self.upper_label.setText("<max>")
self._value_position = Qt.TopEdge
self._limits_from_channel = True
self._user_lower_limit = 0
self._user_upper_limit = 0
self.value_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setup_widgets_for_orientation(Qt.Horizontal, False, False, self._value_position)
[docs] def update_labels(self) -> None:
"""
Update the limits and value labels with the correct values.
"""
self.lower_label.setText(str(self.scale_indicator._lower_limit))
self.upper_label.setText(str(self.scale_indicator._upper_limit))
self.value_label.setText(self.format_string.format(self.scale_indicator._value))
[docs] def value_changed(self, new_value: float) -> None:
"""
Callback invoked when the Channel value is changed.
Parameters
----------
new_value : int or float
The new value from the channel.
"""
super(PyDMScaleIndicator, self).value_changed(new_value)
self.scale_indicator.set_value(new_value)
self.update_labels()
[docs] def upperCtrlLimitChanged(self, new_limit: float) -> None:
"""
PyQT Slot for changes on the upper control limit value of the Channel
This slot sends the new limit value to the
```ctrl_limit_changed``` callback.
Parameters
----------
new_limit : float
"""
super(PyDMScaleIndicator, self).upperCtrlLimitChanged(new_limit)
if self.limitsFromChannel:
self.scale_indicator.set_upper_limit(new_limit)
self.update_labels()
[docs] def lowerCtrlLimitChanged(self, new_limit: float) -> None:
"""
PyQT Slot for changes on the lower control limit value of the Channel
This slot sends the new limit value to the
```ctrl_limit_changed``` callback.
Parameters
----------
new_limit : float
"""
super(PyDMScaleIndicator, self).lowerCtrlLimitChanged(new_limit)
if self.limitsFromChannel:
self.scale_indicator.set_lower_limit(new_limit)
self.update_labels()
@Property(bool)
def showValue(self) -> bool:
"""
Whether or not the current value should be displayed on the scale.
Returns
-------
bool
"""
return self._show_value
@showValue.setter
def showValue(self, checked: bool) -> None:
"""
Whether or not the current value should be displayed on the scale.
Parameters
----------
checked : bool
"""
if self._show_value != bool(checked):
self._show_value = checked
if checked:
self.value_label.show()
else:
self.value_label.hide()
@Property(bool)
def showLimits(self) -> bool:
"""
Whether or not the high and low limits should be displayed on the scale.
Returns
-------
bool
"""
return self._show_limits
@showLimits.setter
def showLimits(self, checked: bool) -> None:
"""
Whether or not the high and low limits should be displayed on the scale.
Parameters
----------
checked : bool
"""
if self._show_limits != bool(checked):
self._show_limits = checked
if checked:
self.lower_label.show()
self.upper_label.show()
else:
self.lower_label.hide()
self.upper_label.hide()
@Property(bool)
def showTicks(self) -> bool:
"""
Whether or not the tick marks should be displayed on the scale.
Returns
-------
bool
"""
return self.scale_indicator.get_show_ticks()
@showTicks.setter
def showTicks(self, checked: bool) -> None:
"""
Whether or not the tick marks should be displayed on the scale.
Parameters
----------
checked : bool
"""
self.scale_indicator.set_show_ticks(checked)
@Property(Qt.Orientation)
def orientation(self) -> Qt.Orientation:
"""
The scale orientation (Horizontal or Vertical)
Returns
-------
int
Qt.Horizontal or Qt.Vertical
"""
return self.scale_indicator.get_orientation()
@orientation.setter
def orientation(self, orientation: Qt.Orientation) -> None:
"""
The scale orientation (Horizontal or Vertical)
Parameters
----------
new_orientation : int
Qt.Horizontal or Qt.Vertical
"""
self.scale_indicator.set_orientation(orientation)
self.setup_widgets_for_orientation(orientation, self.flipScale, self.invertedAppearance, self._value_position)
@Property(bool)
def flipScale(self) -> bool:
"""
Whether or not the scale should be flipped.
Returns
-------
bool
"""
return self.scale_indicator.get_flip_scale()
@flipScale.setter
def flipScale(self, checked: bool) -> None:
"""
Whether or not the scale should be flipped.
Parameters
----------
checked : bool
"""
self.scale_indicator.set_flip_scale(checked)
self.setup_widgets_for_orientation(self.orientation, checked, self.invertedAppearance, self._value_position)
@Property(bool)
def invertedAppearance(self) -> bool:
"""
Whether or not the scale appearence should be inverted.
Returns
-------
bool
"""
return self.scale_indicator.get_inverted_appearance()
@invertedAppearance.setter
def invertedAppearance(self, inverted: bool) -> None:
"""
Whether or not the scale appearence should be inverted.
Parameters
----------
inverted : bool
"""
self.scale_indicator.set_inverted_appearance(inverted)
self.setup_widgets_for_orientation(self.orientation, self.flipScale, inverted, self._value_position)
@Property(bool)
def barIndicator(self) -> bool:
"""
Whether or not the scale indicator should be a bar instead of a pointer.
Returns
-------
bool
"""
return self.scale_indicator.get_bar_indicator()
@barIndicator.setter
def barIndicator(self, checked: bool) -> None:
"""
Whether or not the scale indicator should be a bar instead of a pointer.
Parameters
----------
checked : bool
"""
self.scale_indicator.set_bar_indicator(checked)
@Property(QColor)
def backgroundColor(self) -> QColor:
"""
The color of the scale background.
Returns
-------
QColor
"""
return self.scale_indicator.get_background_color()
@backgroundColor.setter
def backgroundColor(self, color: QColor) -> None:
"""
The color of the scale background.
Parameters
-------
color : QColor
"""
self.scale_indicator.set_background_color(color)
@Property(QColor)
def indicatorColor(self) -> QColor:
"""
The color of the scale indicator.
Returns
-------
QColor
"""
return self.scale_indicator.get_indicator_color()
@indicatorColor.setter
def indicatorColor(self, color: QColor) -> None:
"""
The color of the scale indicator.
Parameters
-------
color : QColor
"""
self.scale_indicator.set_indicator_color(color)
@Property(QColor)
def tickColor(self) -> QColor:
"""
The color of the scale tick marks.
Returns
-------
QColor
"""
return self.scale_indicator.get_tick_color()
@tickColor.setter
def tickColor(self, color: QColor) -> None:
"""
The color of the scale tick marks.
Parameters
-------
color : QColor
"""
self.scale_indicator.set_tick_color(color)
@Property(float)
def backgroundSizeRate(self) -> float:
"""
The rate of background height size (from top to bottom).
Returns
-------
float
"""
return self.scale_indicator.get_background_size_rate()
@backgroundSizeRate.setter
def backgroundSizeRate(self, rate: float) -> None:
"""
The rate of background height size (from top to bottom).
Parameters
-------
rate : float
Between 0 and 1.
"""
self.scale_indicator.set_background_size_rate(rate)
@Property(float)
def tickSizeRate(self) -> float:
"""
The rate of tick marks height size (from bottom to top).
Returns
-------
float
"""
return self.scale_indicator.get_tick_size_rate()
@tickSizeRate.setter
def tickSizeRate(self, rate: float) -> None:
"""
The rate of tick marks height size (from bottom to top).
Parameters
-------
rate : float
Between 0 and 1.
"""
self.scale_indicator.set_tick_size_rate(rate)
@Property(int)
def numDivisions(self) -> int:
"""
The number in which the scale is divided.
Returns
-------
int
"""
return self.scale_indicator.get_num_divisions()
@numDivisions.setter
def numDivisions(self, divisions: int) -> None:
"""
The number in which the scale is divided.
Parameters
-------
divisions : int
The number of scale divisions.
"""
self.scale_indicator.set_num_divisions(divisions)
@Property(int)
def scaleHeight(self) -> int:
"""
The scale height, fixed so it do not wiggle when value label resizes.
Returns
-------
int
"""
return self.scale_indicator.get_scale_height()
@scaleHeight.setter
def scaleHeight(self, value: int) -> None:
"""
The scale height, fixed so it do not wiggle when value label resizes.
Parameters
-------
divisions : int
The scale height.
"""
self.scale_indicator.set_scale_height(value)
@Property(Qt.Edge)
def valuePosition(self) -> Qt.Edge:
"""
The position of the value label (Top, Bottom, Left or Right).
Returns
-------
int
Qt.TopEdge, Qt.BottomEdge, Qt.LeftEdge or Qt.RightEdge
"""
return self._value_position
@valuePosition.setter
def valuePosition(self, position: Qt.Edge) -> None:
"""
The position of the value label (Top, Bottom, Left or Right).
Parameters
----------
position : int
Qt.TopEdge, Qt.BottomEdge, Qt.LeftEdge or Qt.RightEdge
"""
self._value_position = position
self.setup_widgets_for_orientation(self.orientation, self.flipScale, self.invertedAppearance, position)
@Property(bool)
def originAtZero(self) -> bool:
"""
Whether or not the scale indicator should start at zero value.
Applies only for bar indicator.
Returns
-------
bool
"""
return self.scale_indicator.get_origin_at_zero()
@originAtZero.setter
def originAtZero(self, checked: bool) -> None:
"""
Whether or not the scale indicator should start at zero value.
Applies only for bar indicator.
Parameters
----------
checked : bool
"""
self.scale_indicator.set_origin_at_zero(checked)
@Property(bool)
def limitsFromChannel(self) -> bool:
"""
Whether or not the scale indicator should use the limits information
from the channel.
Returns
-------
bool
"""
return self._limits_from_channel
@limitsFromChannel.setter
def limitsFromChannel(self, checked: bool) -> None:
"""
Whether or not the scale indicator should use the limits information
from the channel.
Parameters
----------
checked : bool
True to use the limits from the Channel, False to use the user-defined
values.
"""
if self._limits_from_channel != checked:
self._limits_from_channel = checked
if checked:
if self._lower_ctrl_limit:
self.scale_indicator.set_lower_limit(self._lower_ctrl_limit)
if self._upper_ctrl_limit:
self.scale_indicator.set_upper_limit(self._upper_ctrl_limit)
else:
self.scale_indicator.set_lower_limit(self._user_lower_limit)
self.scale_indicator.set_upper_limit(self._user_upper_limit)
self.update_labels()
@Property(float)
def userLowerLimit(self) -> float:
"""
The user-defined lower limit for the scale.
Returns
-------
float
"""
return self._user_lower_limit
@userLowerLimit.setter
def userLowerLimit(self, value: float) -> None:
"""
The user-defined lower limit for the scale.
Parameters
----------
value : float
The new lower limit value.
"""
if self._limits_from_channel:
return
self._user_lower_limit = value
self.scale_indicator.set_lower_limit(self._user_lower_limit)
self.update_labels()
@Property(float)
def userUpperLimit(self) -> float:
"""
The user-defined upper limit for the scale.
Returns
-------
float
"""
return self._user_upper_limit
@userUpperLimit.setter
def userUpperLimit(self, value: float) -> None:
"""
The user-defined upper limit for the scale.
Parameters
----------
value : float
The new upper limit value.
"""
if self._limits_from_channel:
return
self._user_upper_limit = value
self.scale_indicator.set_upper_limit(self._user_upper_limit)
self.update_labels()