def __init__(self, data, color=Qt.green, parent=None): if ("size_x" in data.index) and ("size_y" in data.index): size_x = data["size_x"] size_y = data["size_y"] else: size_x = size_y = data["size"] super().__init__(data["x"] - size_x + 0.5, data["y"] - size_y + 0.5, 2 * size_x, 2 * size_y, parent) pen = QPen() pen.setWidthF(1.25) pen.setCosmetic(True) pen.setColor(color) self.setPen(pen) ttStr = "\n".join([ "{}: {:.2f}".format(k, v) for k, v in data.items() if k != "frame" ]) self.setToolTip(ttStr)
class BaseSymbolIcon(QWidget): """ Base class to be used for all the Symbol Icon widgets. This class holds most of the properties to be exposed and takes care of 90% of the drawing code needed. Parameters ---------- parent : QWidget The parent widget for this widget. """ def __init__(self, parent=None): self._brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) self._original_brush = None self._rotation = 0 self._pen_style = Qt.SolidLine self._pen = QPen(self._pen_style) self._pen.setCosmetic(True) self._pen_width = 1.0 self._pen_color = QColor(0, 0, 0) self._pen.setWidthF(self._pen_width) self._pen.setColor(self._pen_color) self._original_pen_style = self._pen_style self._original_pen_color = self._pen_color super(BaseSymbolIcon, self).__init__(parent) self.setObjectName("icon") def minimumSizeHint(self): return QSize(32, 32) def paintEvent(self, event): """ 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. This method handles the painting with parameters from the stylesheet, configures the brush, pen and calls ```draw_icon``` so the specifics can be performed for each of the drawing classes. Parameters ---------- event : QPaintEvent """ opt = QStyleOption() opt.initFrom(self) painter = QPainter(self) painter.setClipping(True) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) painter.setRenderHint(QPainter.Antialiasing) x = event.rect().x() y = event.rect().y() w = event.rect().width() h = event.rect().height() painter.translate(w / 2.0, h / 2.0) painter.rotate(self._rotation) painter.translate(-w / 2.0, -h / 2.0) painter.translate(self._pen_width / 2.0, self._pen_width / 2.0) painter.scale(w - self._pen_width, h - self._pen_width) painter.translate(x, y) painter.setBrush(self._brush) painter.setPen(self._pen) self.draw_icon(painter) QWidget.paintEvent(self, event) def draw_icon(self, painter): """ Method responsible for the drawing of the icon part of the paintEvent. This method must be implemented by the symbol icon widgets to include their specific drawings. Parameters ---------- painter : QPainter """ raise NotImplementedError("draw_icon must be implemented by subclass") @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: 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 new_style != self._pen_style: self._pen_style = new_style self._pen.setStyle(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 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.setWidth(self._pen_width) self.update() @Property(float) def rotation(self): """ PyQt Property for the rotation. This rotates the icon coordinate system clockwise. Returns ------- angle : float The angle in degrees """ return self._rotation @rotation.setter def rotation(self, angle): """ PyQt Property for the rotation. This rotates the icon coordinate system clockwise. Parameters ---------- angle : float The angle in degrees """ self._rotation = angle self.update()
class BaseSymbolIcon(QWidget): """ Base class to be used for all the Symbol Icon widgets. This class holds most of the properties to be exposed and takes care of 90% of the drawing code needed. Parameters ---------- parent : QWidget The parent widget for this widget. """ clicked = Signal() def __init__(self, parent=None): self._brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) self._original_brush = None self._rotation = 0 self._pen_style = Qt.SolidLine self._pen = QPen(self._pen_style) self._pen.setCosmetic(True) self._pen_width = 1.0 self._pen_color = QColor(0, 0, 0) self._pen.setWidthF(self._pen_width) self._pen.setColor(self._pen_color) self._original_pen_style = self._pen_style self._original_pen_color = self._pen_color super(BaseSymbolIcon, self).__init__(parent=parent) self.setObjectName("icon") if not is_qt_designer(): # We should install the Event Filter only if we are running # and not at the Designer self.installEventFilter(self) def eventFilter(self, obj, event): """ EventFilter to redirect "middle click" to :meth:`.show_address_tooltip` """ # Override the eventFilter to capture all middle mouse button events, # and show a tooltip if needed. if event.type() == QEvent.MouseButtonPress: if event.button() == Qt.MiddleButton: self.show_state_channel(event) return True return False def show_state_channel(self, event): """ Show the State Channel Tooltip and copy address to clipboard This is intended to replicate the behavior of the "middle click" from EDM. If the parent is not PCDSSymbolBase and does not have a valid State Channel nothing will be displayed. """ from ..vacuum.base import PCDSSymbolBase p = find_ancestor_for_widget(self, PCDSSymbolBase) if not p: return state_suffix = getattr(p, '_state_suffix', None) if not state_suffix: return addr = "{}{}".format(p.channelsPrefix, state_suffix) QToolTip.showText(event.globalPos(), addr) # If the address has a protocol, strip it out before putting it on the # clipboard. copy_text = remove_protocol(addr) clipboard = QApplication.clipboard() clipboard.setText(copy_text) event = QEvent(QEvent.Clipboard) QApplication.instance().sendEvent(clipboard, event) def minimumSizeHint(self): return QSize(32, 32) def paintEvent(self, event): """ 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. This method handles the painting with parameters from the stylesheet, configures the brush, pen and calls ```draw_icon``` so the specifics can be performed for each of the drawing classes. Parameters ---------- event : QPaintEvent """ opt = QStyleOption() opt.initFrom(self) painter = QPainter(self) painter.setClipping(True) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) painter.setRenderHint(QPainter.Antialiasing) w = self.width() h = self.height() painter.translate(w / 2.0, h / 2.0) painter.rotate(self._rotation) painter.translate(-w / 2.0, -h / 2.0) painter.translate(self._pen_width / 2.0, self._pen_width / 2.0) painter.scale(w - self._pen_width, h - self._pen_width) painter.translate(0, 0) painter.setBrush(self._brush) painter.setPen(self._pen) self.draw_icon(painter) QWidget.paintEvent(self, event) def draw_icon(self, painter): """ Method responsible for the drawing of the icon part of the paintEvent. This method must be implemented by the symbol icon widgets to include their specific drawings. Parameters ---------- painter : QPainter """ raise NotImplementedError("draw_icon must be implemented by subclass") @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: 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 new_style != self._pen_style: self._pen_style = new_style self._pen.setStyle(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 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.setWidth(self._pen_width) self.update() @Property(float) def rotation(self): """ PyQt Property for the rotation. This rotates the icon coordinate system clockwise. Returns ------- angle : float The angle in degrees """ return self._rotation @rotation.setter def rotation(self, angle): """ PyQt Property for the rotation. This rotates the icon coordinate system clockwise. Parameters ---------- angle : float The angle in degrees """ self._rotation = angle self.update() def mousePressEvent(self, evt): """Clicking the icon fires the ``clicked`` signal""" # Default Qt reaction to the icon press super().mousePressEvent(evt) if evt.button() == Qt.LeftButton: self.clicked.emit()
class MicroViewScene(QGraphicsScene): def __init__(self, parent): super().__init__(parent) self._imageItem = ImageGraphicsItem() self.addItem(self._imageItem) self._roiMode = False self._drawingRoi = False self.roiPen = QPen() self.roiPen.setWidthF(1.25) self.roiPen.setCosmetic(True) self.roiPen.setColor(Qt.yellow) self._roiPolygon = QPolygonF() self._roiItem = QGraphicsPolygonItem(self._roiPolygon) self._roiItem.setPen(self.roiPen) self.addItem(self._roiItem) def setImage(self, img): if isinstance(img, QImage): img = QPixmap.fromImage(img) self._imageItem.setPixmap(img) @Property(ImageGraphicsItem) def imageItem(self): return self._imageItem def enableRoiMode(self, enable): if enable == self._roiMode: return if enable: self._roiPolygon = QPolygonF() self._roiItem.setPolygon(self._roiPolygon) self._tempRoiPolygon = QPolygonF(self._roiPolygon) self._tempRoiPolygon.append(QPointF()) if not enable: self._roiItem.setPolygon(self._roiPolygon) self.roiChanged.emit(self._roiPolygon) self._roiMode = enable self.roiModeChanged.emit(enable) roiModeChanged = Signal(bool) @Property(bool, fset=enableRoiMode) def roiMode(self): return self._roiMode def setRoi(self, roi): if self._roiPolygon == roi: return self._roiPolygon = roi self._roiItem.setPolygon(self._roiPolygon) self.roiChanged.emit(roi) roiChanged = Signal(QPolygonF) @Property(QPolygonF, fset=setRoi, doc="Polygon describing the region of interest (ROI)") def roi(self): return self._roiPolygon def _appendPointToRoi(self, pos, polygon, replace_last=False): br = self._imageItem.boundingRect() topLeft = br.topLeft() bottomRight = br.bottomRight() # Make sure we stay inside the image boundaries xInBr = max(topLeft.x(), pos.x()) xInBr = min(bottomRight.x(), xInBr) yInBr = max(topLeft.y(), pos.y()) yInBr = min(bottomRight.y(), yInBr) pointInBr = QPointF(xInBr, yInBr) if replace_last: polygon[-1] = pointInBr else: polygon.append(pointInBr) def mousePressEvent(self, event): super().mousePressEvent(event) if not self._roiMode: return self._appendPointToRoi(event.scenePos(), self._roiPolygon, False) self._roiItem.setPolygon(self._roiPolygon) self._tempRoiPolygon = QPolygonF(self._roiPolygon) self._tempRoiPolygon.append(QPointF()) def mouseMoveEvent(self, event): super().mouseMoveEvent(event) if not self._roiMode: return self._appendPointToRoi(event.scenePos(), self._tempRoiPolygon, True) self._roiItem.setPolygon(self._tempRoiPolygon) def mouseDoubleClickEvent(self, event): super().mouseDoubleClickEvent(event) # the first click of the double click is a normal mousePressEvent, # thus the current point has already been added. Simply exit ROI mode self.roiMode = False
class TunableMarker(QGraphicsItem): def __init__(self, shape: Shape, size: int, key: Optional[int] = None, **kwargs): super().__init__() self._shape = shape self._size = size self.key = key self.info = kwargs self._pen = QPen() self._pen.setWidthF(0.25) self._brush = QBrush() def _make_rect(self, length): # make a rectangle of width and height equal to 'length' and centered # about (0, 0) return QRectF(-length / 2, -length / 2, length, length) def boundingRect(self): return self._make_rect(self._size + 4) def _paint_ellipse(self, painter): rect = self._make_rect(self._size) painter.drawEllipse(rect) def _paint_diamond(self, painter): size = self._size X = [0, size / 2, 0, -size / 2, 0] Y = [size / 2, 0, -size / 2, 0, size / 2] points = [QPointF(x, y) for x, y in zip(X, Y)] polygon = QPolygonF(points) painter.drawPolygon(polygon, len(points)) def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget): painter.setPen(self._pen) painter.setBrush(self._brush) shape_to_paint_method = { Shape.CIRCLE: self._paint_ellipse, Shape.DIAMOND: self._paint_diamond } drawing_method = shape_to_paint_method[self._shape] drawing_method(painter) def get_marker_color(self): """ Marker color. """ return self._pen.color() def set_marker_color(self, color: Union[QColor, int]): color = QColor(color) self._pen.setColor(color) self._brush.setColor(color) self.update() def get_marker_fill(self) -> bool: """ Whether this marker is filled in or merely an outline. """ return bool(self._brush.style()) def set_marker_fill(self, f: bool): if f: f = Qt.SolidPattern else: f = Qt.NoBrush self._brush.setStyle(f) self.update() def get_marker_shape(self): return self._shape def set_marker_shape(self, shape: Shape): self._shape = shape self.update() def get_marker_size(self): return self._size def set_marker_size(self, sz: int): self._size = sz self.update()
class PyDMDrawing(QWidget, PyDMWidget): """ 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 = None 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