import ast
import math
import os
import logging
from qtpy.QtWidgets import QWidget, QStyle, QStyleOption
from qtpy.QtGui import QColor, QPainter, QBrush, QPen, QPolygonF, QPixmap, QMovie
from qtpy.QtCore import Property, Qt, QPoint, QPointF, QSize, Slot, QTimer, QRectF
from qtpy.QtDesigner import QDesignerFormWindowInterface
from .base import PyDMWidget
from ..utilities import is_qt_designer, find_file
from typing import List, Optional
logger = logging.getLogger(__name__)
_penRuleProperties = {
"Set Pen Color": ["penColor", QColor],
"Set Pen Style": ["penStyle", int],
"Set Pen Width": ["penWidth", float],
"Set Brush Color": ["brush", QBrush],
}
def deg_to_qt(deg):
"""
Converts from degrees to QT degrees.
16 deg = 1 QTdeg
Parameters
----------
deg : float
The value to convert.
Returns
-------
float
The value converted.
"""
# Angles for Qt are in units of 1/16 of a degree
return deg * 16
def qt_to_deg(deg):
"""
Converts from QT degrees to degrees.
16 deg = 1 QTdeg
Parameters
----------
deg : float
The value to convert.
Returns
-------
float
The value converted.
"""
# Angles for Qt are in units of 1/16 of a degree
return deg / 16.0
class PyDMDrawing(QWidget, PyDMWidget, new_properties=_penRuleProperties):
"""
Base class to be used for all PyDM Drawing Widgets.
This class inherits from QWidget and PyDMWidget.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
self._rotation = 0.0
self._brush = QBrush(Qt.SolidPattern)
self._original_brush = self._brush
self._painter = QPainter()
self._pen = QPen(Qt.NoPen)
self._pen_style = Qt.NoPen
self._pen_cap_style = Qt.SquareCap
self._pen_join_style = Qt.MiterJoin
self._pen_width = 0
self._pen_color = QColor(0, 0, 0)
self._pen.setCapStyle(self._pen_cap_style)
self._pen.setJoinStyle(self._pen_join_style)
self._original_pen_style = self._pen_style
self._original_pen_color = self._pen_color
QWidget.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
self.alarmSensitiveBorder = False
def sizeHint(self):
return QSize(100, 100)
def paintEvent(self, _):
"""
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.
At PyDMDrawing this method handles the alarm painting with parameters
from the stylesheet, configures the brush, pen and calls ```draw_item```
so the specifics can be performed for each of the drawing classes.
Parameters
----------
event : QPaintEvent
"""
painter = QPainter(self)
opt = QStyleOption()
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(self._brush)
painter.setPen(self._pen)
self.draw_item(painter)
def draw_item(self, painter):
"""
The classes inheriting from PyDMDrawing must overwrite this method.
This method translate the painter to the center point given by
```get_center``` and rotate the canvas by the given amount of
degrees.
"""
xc, yc = self.get_center()
painter.translate(xc, yc)
painter.rotate(-self._rotation)
def get_center(self):
"""
Simple calculation of the canvas' center point.
Returns
-------
x, y : float
Tuple with X and Y coordinates of the center.
"""
return self.width() * 0.5, self.height() * 0.5
def get_bounds(self, maxsize=False, force_no_pen=False):
"""
Returns a tuple containing the useful area for the drawing.
Parameters
----------
maxsize : bool, default is False
If True, width and height information are based on the
maximum inner rectangle dimensions given by ```get_inner_max```,
otherwise width and height will receive the widget size.
force_no_pen : bool, default is False
If True the pen width will not be considered when calculating
the bounds.
Returns
-------
x, y, w, h : tuple
Tuple with X and Y coordinates followed by the maximum width
and height.
"""
w, h = self.width(), self.height()
if maxsize:
w, h = self.get_inner_max()
xc, yc = w * 0.5, h * 0.5
if self.has_border() and not force_no_pen:
w = max(0, w - 2 * self._pen_width)
h = max(0, h - 2 * self._pen_width)
x = max(0, self._pen_width)
y = max(0, self._pen_width)
else:
x = 0
y = 0
return x - xc, y - yc, w, h
def has_border(self):
"""
Check whether or not the drawing have a border based on the
Pen Style and Pen width.
Returns
-------
bool
True if the drawing has a border, False otherwise.
"""
if self._pen.style() != Qt.NoPen and self._pen_width > 0:
return True
else:
return False
def is_square(self):
"""
Check if the widget has the same width and height values.
Returns
-------
bool
True in case the widget has a square shape, False otherwise.
"""
return self.height() == self.width()
def get_inner_max(self):
"""
Calculates the largest inner rectangle in a rotated rectangle.
This implementation was based on https://stackoverflow.com/a/18402507
Returns
-------
w, h : tuple
The width and height of the largest rectangle.
"""
# Based on https://stackoverflow.com/a/18402507
w0 = 0
h0 = 0
angle = math.radians(self._rotation)
origWidth = self.width()
origHeight = self.height()
if origWidth == 0:
logger.error("Invalid width. The value must be greater than {0}".format(origWidth))
return
if origHeight == 0:
logger.error("Invalid height. The value must be greater than {0}".format(origHeight))
return
if origWidth <= origHeight:
w0 = origWidth
h0 = origHeight
else:
w0 = origHeight
h0 = origWidth
# Angle normalization in range [-PI..PI)
ang = angle - math.floor((angle + math.pi) / (2 * math.pi)) * 2 * math.pi
ang = math.fabs(ang)
if ang > math.pi / 2:
ang = math.pi - ang
c = w0 / (h0 * math.sin(ang) + w0 * math.cos(ang))
w = 0
h = 0
if origWidth <= origHeight:
w = w0 * c
h = h0 * c
else:
w = h0 * c
h = w0 * c
return w, h
@Property(QBrush)
def brush(self):
"""
PyQT Property for the brush object to be used when coloring the
drawing
Returns
-------
QBrush
"""
return self._brush
@brush.setter
def brush(self, new_brush):
"""
PyQT Property for the brush object to be used when coloring the
drawing
Parameters
----------
new_brush : QBrush
"""
if new_brush != self._brush:
if self._alarm_state == PyDMWidget.ALARM_NONE:
self._original_brush = new_brush
self._brush = new_brush
self.update()
@Property(Qt.PenStyle)
def penStyle(self):
"""
PyQT Property for the pen style to be used when drawing the border
Returns
-------
int
Index at Qt.PenStyle enum
"""
return self._pen_style
@penStyle.setter
def penStyle(self, new_style):
"""
PyQT Property for the pen style to be used when drawing the border
Parameters
----------
new_style : int
Index at Qt.PenStyle enum
"""
if self._alarm_state == PyDMWidget.ALARM_NONE:
self._original_pen_style = new_style
if new_style != self._pen_style:
self._pen_style = new_style
self._pen.setStyle(new_style)
self.update()
@Property(Qt.PenCapStyle)
def penCapStyle(self):
"""
PyQT Property for the pen cap to be used when drawing the border
Returns
-------
int
Index at Qt.PenCapStyle enum
"""
return self._pen_cap_style
@penCapStyle.setter
def penCapStyle(self, new_style):
"""
PyQT Property for the pen cap style to be used when drawing the border
Parameters
----------
new_style : int
Index at Qt.PenStyle enum
"""
if new_style != self._pen_cap_style:
self._pen_cap_style = new_style
self._pen.setCapStyle(new_style)
self.update()
@Property(Qt.PenJoinStyle)
def penJoinStyle(self):
"""
PyQT Property for the pen join style to be used when drawing the border
Returns
-------
int
Index at Qt.PenJoinStyle enum
"""
return self._pen_join_style
@penJoinStyle.setter
def penJoinStyle(self, new_style):
"""
PyQT Property for the pen join style to be used when drawing the border
Parameters
----------
new_style : int
Index at Qt.PenStyle enum
"""
if new_style != self._pen_join_style:
self._pen_join_style = new_style
self._pen.setJoinStyle(new_style)
self.update()
@Property(QColor)
def penColor(self):
"""
PyQT Property for the pen color to be used when drawing the border
Returns
-------
QColor
"""
return self._pen_color
@penColor.setter
def penColor(self, new_color):
"""
PyQT Property for the pen color to be used when drawing the border
Parameters
----------
new_color : QColor
"""
if self._alarm_state == PyDMWidget.ALARM_NONE:
self._original_pen_color = new_color
if new_color != self._pen_color:
self._pen_color = new_color
self._pen.setColor(new_color)
self.update()
@Property(float)
def penWidth(self):
"""
PyQT Property for the pen width to be used when drawing the border
Returns
-------
float
"""
return self._pen_width
@penWidth.setter
def penWidth(self, new_width):
"""
PyQT Property for the pen width to be used when drawing the border
Parameters
----------
new_width : float
"""
if new_width < 0:
return
if new_width != self._pen_width:
self._pen_width = new_width
self._pen.setWidthF(float(self._pen_width))
self.update()
@Property(float)
def rotation(self):
"""
PyQT Property for the counter-clockwise rotation in degrees
to be applied to the drawing.
Returns
-------
float
"""
return self._rotation
@rotation.setter
def rotation(self, new_angle):
"""
PyQT Property for the counter-clockwise rotation in degrees
to be applied to the drawing.
Parameters
----------
new_angle : float
"""
if new_angle != self._rotation:
self._rotation = new_angle
self.update()
def alarm_severity_changed(self, new_alarm_severity):
PyDMWidget.alarm_severity_changed(self, new_alarm_severity)
if new_alarm_severity == PyDMWidget.ALARM_NONE:
if self._original_brush is not None:
self.brush = self._original_brush
if self._original_pen_color is not None:
self.penColor = self._original_pen_color
if self._original_pen_style is not None:
self.penStyle = self._original_pen_style
class PyDMDrawingLineBase(PyDMDrawing):
"""
A base class for single and poly line widgets.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingLineBase, self).__init__(parent, init_channel)
self.penStyle = Qt.SolidLine
self.penWidth = 1
self._arrow_size = 6 # 6 is arbitrary size that looked good for default, not in any specific 'units'
self._arrow_end_point_selection = False
self._arrow_start_point_selection = False
self._arrow_mid_point_selection = False
self._arrow_mid_point_flipped = False
@Property(int)
def arrowSize(self) -> int:
"""
Size to render line arrows.
Returns
-------
bool
"""
return self._arrow_size
@arrowSize.setter
def arrowSize(self, new_size) -> None:
"""
Size to render line arrows.
Parameters
-------
new_selection : bool
"""
if self._arrow_size != new_size:
self._arrow_size = new_size
self.update()
@Property(bool)
def arrowEndPoint(self) -> bool:
"""
If True, an arrow will be drawn at the end of the line.
Returns
-------
bool
"""
return self._arrow_end_point_selection
@arrowEndPoint.setter
def arrowEndPoint(self, new_selection) -> None:
"""
If True, an arrow will be drawn at the end of the line.
Parameters
-------
new_selection : bool
"""
if self._arrow_end_point_selection != new_selection:
self._arrow_end_point_selection = new_selection
self.update()
@Property(bool)
def arrowStartPoint(self) -> bool:
"""
If True, an arrow will be drawn at the start of the line.
Returns
-------
bool
"""
return self._arrow_start_point_selection
@arrowStartPoint.setter
def arrowStartPoint(self, new_selection) -> None:
"""
If True, an arrow will be drawn at the start of the line.
Parameters
-------
new_selection : bool
"""
if self._arrow_start_point_selection != new_selection:
self._arrow_start_point_selection = new_selection
self.update()
@Property(bool)
def arrowMidPoint(self) -> bool:
"""
If True, an arrow will be drawn at the midpoint of the line.
Returns
-------
bool
"""
return self._arrow_mid_point_selection
@arrowMidPoint.setter
def arrowMidPoint(self, new_selection) -> None:
"""
If True, an arrow will be drawn at the midpoint of the line.
Parameters
-------
new_selection : bool
"""
if self._arrow_mid_point_selection != new_selection:
self._arrow_mid_point_selection = new_selection
self.update()
@Property(bool)
def flipMidPointArrow(self) -> bool:
"""
Flips the direction of the midpoint arrow.
Returns
-------
bool
"""
return self._arrow_mid_point_flipped
@flipMidPointArrow.setter
def flipMidPointArrow(self, new_selection) -> None:
"""
Flips the direction of the midpoint arrow.
Parameters
-------
new_selection : bool
"""
if self._arrow_mid_point_flipped != new_selection:
self._arrow_mid_point_flipped = new_selection
self.update()
@staticmethod
def _arrow_points(startpoint, endpoint, height, width) -> QPolygonF:
"""
Returns the three points needed to make a triangle with .drawPolygon
"""
diff_x = startpoint.x() - endpoint.x()
diff_y = startpoint.y() - endpoint.y()
length = max(math.sqrt(diff_x**2 + diff_y**2), 1.0)
norm_x = diff_x / length
norm_y = diff_y / length
perp_x = -norm_y
perp_y = norm_x
left_x = endpoint.x() + height * norm_x + width * perp_x
left_y = endpoint.y() + height * norm_y + width * perp_y
right_x = endpoint.x() + height * norm_x - width * perp_x
right_y = endpoint.y() + height * norm_y - width * perp_y
left = QPointF(left_x, left_y)
right = QPointF(right_x, right_y)
return QPolygonF([left, endpoint, right])
[docs]class PyDMDrawingLine(PyDMDrawingLineBase, new_properties=_penRuleProperties):
"""
A widget with a line drawn in it.
This class inherits from PyDMDrawingLineBase.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingLine, self).__init__(parent, init_channel)
[docs] def draw_item(self, painter) -> None:
"""
Draws the line after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingLine, self).draw_item(painter)
x, y, w, h = self.get_bounds()
# Figure out how long to make the line to touch the bounding box
# Length varies depending on rotation
# Find the quadrant 1 angle equivalent
angle = self._rotation % 360
if 90 < angle <= 180:
angle = 180 - angle
elif 180 < angle <= 270:
angle = angle - 180
elif 270 < angle <= 360:
angle = 360 - angle
angle_rad = math.radians(angle)
# Find the angle of the rop right corner of the bounding box
try:
critical_angle = math.atan(h / w)
except ZeroDivisionError:
critical_angle = math.pi / 2
# Pick a length based on which side we intersect with
if angle_rad > critical_angle:
try:
length = h / math.sin(angle_rad)
except ZeroDivisionError:
length = w
else:
try:
length = w / math.cos(angle_rad)
except ZeroDivisionError:
length = h
# Define endpoints potentially outside the bounding box
# Will land on the bounding box after rotation
midpoint = x + w / 2
start_point = QPointF(midpoint - length / 2, 0)
end_point = QPointF(midpoint + length / 2, 0)
mid_point = QPointF(midpoint, 0)
# Draw the line
painter.drawLine(start_point, end_point)
# Draw the arrows
if self._arrow_end_point_selection:
points = self._arrow_points(start_point, end_point, self._arrow_size, self._arrow_size)
painter.drawPolygon(points)
if self._arrow_start_point_selection:
points = self._arrow_points(end_point, start_point, self._arrow_size, self._arrow_size)
painter.drawPolygon(points)
if self._arrow_mid_point_selection:
if self._arrow_mid_point_flipped:
points = self._arrow_points(start_point, mid_point, self._arrow_size, self._arrow_size)
else:
points = self._arrow_points(end_point, mid_point, self._arrow_size, self._arrow_size)
painter.drawPolygon(points)
[docs]class PyDMDrawingPolyline(PyDMDrawingLineBase):
"""
A widget with a multi-segment, piecewise-linear line drawn in it.
This class inherits from PyDMDrawingLineBase.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingPolyline, self).__init__(parent, init_channel)
self._points = []
[docs] def draw_item(self, painter) -> None:
"""
Draws the segmented line after setting up the canvas with a call to
``PyDMDrawing.draw_item``.
"""
super(PyDMDrawingPolyline, self).draw_item(painter)
x, y, w, h = self.get_bounds()
def p2d(pt):
"convert point to drawing coordinates"
# drawing coordinates are centered: (0,0) is in center
# our points are absolute: (0,0) is upper-left corner
if isinstance(pt, str):
# 2022-05-11: needed for backwards compatibility support
# PyDM releases up to v1.15.1
# adl2pydm tags up to 0.0.2
pt = tuple(map(int, pt.split(",")))
u, v = pt
return QPointF(u + x, v + y)
if len(self._points) > 1:
for i, p1 in enumerate(self._points[:-1]):
painter.drawLine(p2d(p1), p2d(self._points[i + 1]))
if self._arrow_mid_point_selection:
point1 = p2d(p1)
point2 = p2d(self._points[i + 1])
if self._arrow_mid_point_flipped:
point1, point2 = point2, point1 # swap values
# arrow points at midpoint of line
midpoint_x = (point1.x() + point2.x()) / 2
midpoint_y = (point1.y() + point2.y()) / 2
midpoint = QPointF(midpoint_x, midpoint_y)
points = self._arrow_points(
point1, midpoint, self._arrow_size, self._arrow_size
) # 6 = arbitrary arrow size
painter.drawPolygon(points)
# Draw the arrows
# While we enforce >=2 points when user adds points, we need to check '(len(self._points) > 0)' here so we
# don't break trying to add arrows to new polyline with no points yet.
if self._arrow_end_point_selection and (len(self._points) > 0) and (len(self._points[1]) >= 2):
points = self._arrow_points(p2d(self._points[1]), p2d(self._points[0]), self._arrow_size, self._arrow_size)
painter.drawPolygon(points)
if self._arrow_start_point_selection and (len(self._points) > 0) and (len(self._points[1]) >= 2):
points = self._arrow_points(
p2d(self._points[len(self._points) - 2]),
p2d(self._points[len(self._points) - 1]),
self._arrow_size,
self._arrow_size,
)
painter.drawPolygon(points)
[docs] def getPoints(self) -> List[str]:
"""Convert internal points representation for use as QStringList."""
points = [f"{pt[0]}, {pt[1]}" for pt in self._points]
return points
def _validator(self, value) -> bool:
"""
ensure that `value` has correct form
Parameters
----------
value : [ordered pairs]
List of strings representing ordered pairs
of integer coordinates. Each ordered pair
is a tuple or list.
Returns
----------
verified : [ordered pairs]
List of `tuple(number, number)`.
"""
def isfloat(value) -> bool:
if isinstance(value, str):
value = value.strip()
try:
float(value)
return True
except Exception:
return False
def validate_point(i, point) -> Optional[List[float]]:
"""Ignore (instead of fail on) any of these pathologies."""
if isinstance(point, str):
try:
point = ast.literal_eval(point)
except SyntaxError:
logger.error(
"point %d must be two numbers, comma-separated, received '%s'",
i,
pt,
)
return
if not isinstance(point, (list, tuple)) or len(point) != 2:
logger.error(
"point %d must be two numbers, comma-separated, received '%s'",
i,
pt,
)
return
try:
point = list(map(float, point)) # ensure all values are float
except ValueError:
logger.error("point %d content must be numeric, received '%s'", i, pt)
return
return point
verified = []
for i, pt in enumerate(value, start=1):
point = validate_point(i, pt)
if point is not None:
verified.append(point)
return verified
def setPoints(self, value) -> None:
verified = self._validator(value)
if verified is not None:
if len(verified) < 2:
logger.error("Must have two or more points")
return
self._points = verified
self.update()
def resetPoints(self) -> None:
self._points = []
self.update()
points = Property("QStringList", getPoints, setPoints, resetPoints)
[docs]class PyDMDrawingImage(PyDMDrawing):
"""
Renders an image given by the ``filename`` property.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
Attributes
----------
null_color : Qt.Color
QColor to fill the image if the filename is not found.
"""
null_color = Qt.gray
def __init__(self, parent=None, init_channel=None, filename=""):
super(PyDMDrawingImage, self).__init__(parent, init_channel)
hint = super(PyDMDrawingImage, self).sizeHint()
self._pixmap = QPixmap(hint)
self._pixmap.fill(self.null_color)
self._aspect_ratio_mode = Qt.KeepAspectRatio
self._movie = None
self._file = None
# Make sure we don't set a non-existant file
if filename:
self.filename = filename
# But we always have an internal value to reference
else:
self._file = filename
if is_qt_designer(): # pragma: no cover
designer_window = self.get_designer_window()
if designer_window is not None:
designer_window.fileNameChanged.connect(self.designer_form_saved)
QTimer.singleShot(200, self.reload_image)
def get_designer_window(self): # pragma: no cover
# Internal function to find the designer window that owns this widget.
p = self.parent()
while p is not None:
if isinstance(p, QDesignerFormWindowInterface):
return p
p = p.parent()
return None
@Slot(str)
def designer_form_saved(self, filename): # pragma: no cover
self.filename = self._file
def reload_image(self) -> None:
self.filename = self._file
@Property(str)
def filename(self) -> str:
"""
The filename of the image to be displayed.
This can be an absolute or relative path to the display file.
Returns
-------
str
The filename configured.
"""
return self._file
@filename.setter
def filename(self, new_file) -> None:
"""
The filename of the image to be displayed.
This file can be either relative to the ``.ui`` file or absolute. If
the path does not exist, a shape of ``.null_color`` will be displayed
instead.
Parameters
-------
new_file : str
The filename to be used
"""
# Expand user (~ or ~user) and environment variables.
pixmap = None
self._file = new_file
abs_path = os.path.expanduser(os.path.expandvars(self._file))
# Find the absolute path relative to UI
if not os.path.isabs(abs_path):
parent_display = self.find_parent_display()
base_path = None
if parent_display:
base_path = os.path.dirname(parent_display.loaded_file())
abs_path = find_file(abs_path, base_path=base_path)
if not abs_path:
logger.error("Unable to find full filepath for %s", self._file)
return
# Check that the path exists
if os.path.isfile(abs_path):
if self._movie is not None:
self._movie.stop()
self._movie.deleteLater()
self._movie = None
if not abs_path.endswith(".gif"):
pixmap = QPixmap(abs_path)
else:
self._movie = QMovie(abs_path, parent=self)
self._movie.setCacheMode(QMovie.CacheAll)
self._movie.frameChanged.connect(self.movie_frame_changed)
if self._movie.frameCount() > 1:
self._movie.finished.connect(self.movie_finished)
self._movie.start()
# Return a blank image if we don't have a valid path
else:
# Warn the user loudly if their file does not exist, but avoid
# doing this in Designer as this spams the user as they are typing
if not is_qt_designer(): # pragma: no cover
logger.error("Image file %r does not exist", abs_path)
pixmap = QPixmap(self.sizeHint())
pixmap.fill(self.null_color)
# Update the display
if pixmap is not None:
self._pixmap = pixmap
self.update()
[docs] def sizeHint(self):
if self._pixmap.size().isEmpty():
return super(PyDMDrawingImage, self).sizeHint()
return self._pixmap.size()
@Property(Qt.AspectRatioMode)
def aspectRatioMode(self):
"""
PyQT Property for aspect ratio mode to be used when rendering
the image
Returns
-------
int
Index at Qt.AspectRatioMode enum
"""
return self._aspect_ratio_mode
@aspectRatioMode.setter
def aspectRatioMode(self, new_mode):
"""
PyQT Property for aspect ratio mode to be used when rendering
the image
Parameters
----------
new_mode : int
Index at Qt.AspectRatioMode enum
"""
if new_mode != self._aspect_ratio_mode:
self._aspect_ratio_mode = new_mode
self.update()
[docs] def draw_item(self, painter):
"""
Draws the image after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingImage, self).draw_item(painter)
x, y, w, h = self.get_bounds(maxsize=True, force_no_pen=True)
if not isinstance(self._pixmap, QMovie):
_scaled = self._pixmap.scaled(int(w), int(h), self._aspect_ratio_mode, Qt.SmoothTransformation)
# Make sure the image is centered if smaller than the widget itself
if w > _scaled.width():
logger.debug("Centering image horizontally ...")
x += (w - _scaled.width()) / 2
if h > _scaled.height():
logger.debug("Centering image vertically ...")
y += (h - _scaled.height()) / 2
painter.drawPixmap(QPointF(x, y), _scaled)
[docs] def movie_frame_changed(self, frame_no):
"""
Callback executed when a new frame is available at the QMovie.
Parameters
----------
frame_no : int
The new frame index
Returns
-------
None
"""
if self._movie is None:
return
curr_pixmap = self._movie.currentPixmap()
self._pixmap = curr_pixmap
self.update()
[docs] def movie_finished(self):
"""
Callback executed when the movie is finished.
Returns
-------
None
"""
if self._movie is None:
return
self._movie.start()
[docs]class PyDMDrawingRectangle(PyDMDrawing):
"""
A widget with a rectangle drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingRectangle, self).__init__(parent, init_channel)
[docs] def draw_item(self, painter):
"""
Draws the rectangle after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingRectangle, self).draw_item(painter)
x, y, w, h = self.get_bounds(maxsize=True)
painter.drawRect(QRectF(x, y, w, h))
[docs]class PyDMDrawingTriangle(PyDMDrawing):
"""
A widget with a triangle drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingTriangle, self).__init__(parent, init_channel)
def _calculate_drawing_points(self, x, y, w, h):
return [QPointF(x, h / 2.0), QPointF(x, y), QPointF(w / 2.0, y)]
[docs] def draw_item(self, painter):
"""
Draws the triangle after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingTriangle, self).draw_item(painter)
x, y, w, h = self.get_bounds(maxsize=True)
points = self._calculate_drawing_points(x, y, w, h)
painter.drawPolygon(QPolygonF(points))
[docs]class PyDMDrawingEllipse(PyDMDrawing):
"""
A widget with an ellipse drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingEllipse, self).__init__(parent, init_channel)
[docs] def draw_item(self, painter):
"""
Draws the ellipse after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingEllipse, self).draw_item(painter)
maxsize = not self.is_square()
_, _, w, h = self.get_bounds(maxsize=maxsize)
painter.drawEllipse(QPoint(0, 0), w / 2.0, h / 2.0)
[docs]class PyDMDrawingCircle(PyDMDrawing):
"""
A widget with a circle drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingCircle, self).__init__(parent, init_channel)
def _calculate_radius(self, width, height):
return min(width, height) / 2.0
[docs] def draw_item(self, painter):
"""
Draws the circle after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingCircle, self).draw_item(painter)
_, _, w, h = self.get_bounds()
r = self._calculate_radius(w, h)
painter.drawEllipse(QPoint(0, 0), r, r)
[docs]class PyDMDrawingArc(PyDMDrawing):
"""
A widget with an arc drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingArc, self).__init__(parent, init_channel)
self.penStyle = Qt.SolidLine
self.penWidth = 1.0
self._start_angle = 0
self._span_angle = deg_to_qt(90)
@Property(float)
def startAngle(self):
"""
PyQT Property for the start angle in degrees
Returns
-------
float
Angle in degrees
"""
return qt_to_deg(self._start_angle)
@startAngle.setter
def startAngle(self, new_angle):
"""
PyQT Property for the start angle in degrees
Parameters
----------
new_angle : float
Angle in degrees
"""
if deg_to_qt(new_angle) != self._start_angle:
self._start_angle = deg_to_qt(new_angle)
self.update()
@Property(float)
def spanAngle(self):
"""
PyQT Property for the span angle in degrees
Returns
-------
float
Angle in degrees
"""
return qt_to_deg(self._span_angle)
@spanAngle.setter
def spanAngle(self, new_angle):
"""
PyQT Property for the span angle in degrees
Parameters
----------
new_angle : float
Angle in degrees
"""
if deg_to_qt(new_angle) != self._span_angle:
self._span_angle = deg_to_qt(new_angle)
self.update()
[docs] def draw_item(self, painter):
"""
Draws the arc after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingArc, self).draw_item(painter)
maxsize = not self.is_square()
x, y, w, h = self.get_bounds(maxsize=maxsize)
painter.drawArc(QRectF(x, y, w, h), int(self._start_angle), int(self._span_angle))
[docs]class PyDMDrawingPie(PyDMDrawingArc):
"""
A widget with a pie drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingPie, self).__init__(parent, init_channel)
[docs] def draw_item(self, painter):
"""
Draws the pie after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingPie, self).draw_item(painter)
maxsize = not self.is_square()
x, y, w, h = self.get_bounds(maxsize=maxsize)
painter.drawPie(QRectF(x, y, w, h), int(self._start_angle), int(self._span_angle))
[docs]class PyDMDrawingChord(PyDMDrawingArc):
"""
A widget with a chord drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingChord, self).__init__(parent, init_channel)
[docs] def draw_item(self, painter):
"""
Draws the chord after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingChord, self).draw_item(painter)
maxsize = not self.is_square()
x, y, w, h = self.get_bounds(maxsize=maxsize)
painter.drawChord(QRectF(x, y, w, h), int(self._start_angle), int(self._span_angle))
[docs]class PyDMDrawingPolygon(PyDMDrawing):
"""
A widget with a polygon drawn in it.
This class inherits from PyDMDrawing.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingPolygon, self).__init__(parent, init_channel)
self._num_points = 3
@Property(int)
def numberOfPoints(self):
"""
PyQT Property for the number of points
Returns
-------
int
Number of Points
"""
return self._num_points
@numberOfPoints.setter
def numberOfPoints(self, points):
if points >= 3 and points != self._num_points:
self._num_points = points
self.update()
def _calculate_drawing_points(self, x, y, w, h):
# (x + r*cos(theta), y + r*sin(theta))
r = min(w, h) / 2.0
deg_step = 360.0 / self._num_points
points = []
for i in range(self._num_points):
xp = r * math.cos(math.radians(deg_step * i))
yp = r * math.sin(math.radians(deg_step * i))
points.append(QPointF(xp, yp))
return points
[docs] def draw_item(self, painter):
"""
Draws the Polygon after setting up the canvas with a call to
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingPolygon, self).draw_item(painter)
not self.is_square()
x, y, w, h = self.get_bounds(maxsize=not self.is_square())
poly = self._calculate_drawing_points(x, y, w, h)
painter.drawPolygon(QPolygonF(poly))
[docs]class PyDMDrawingIrregularPolygon(PyDMDrawingPolyline):
"""
A widget contains an irregular polygon (arbitrary number of vertices, arbitrary lengths).
This is a special case of the PyDMDrawingPolyline, adding the requirement that
the last point is always identical to the first point.
This widget is created for compatibility with MEDM's *polygon* widget.
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
[docs] def getPoints(self):
return super(PyDMDrawingIrregularPolygon, self).getPoints()
def resetPoints(self):
super(PyDMDrawingIrregularPolygon, self).resetPoints()
def setPoints(self, points):
verified = self._validator(points)
if verified is not None:
if len(verified) > 1:
if verified[0] != verified[-1]:
verified.append(verified[0]) # close the polygon
if len(verified) < 3:
logger.error("Must have three or more points")
return
self._points = verified
self.update()
points = Property("QStringList", getPoints, setPoints, resetPoints)