class ProjectItemIcon(QGraphicsRectItem): ITEM_EXTENT = 64 def __init__(self, toolbox, x, y, project_item, icon_file, icon_color, background_color): """Base class for project item icons drawn in Design View. Args: toolbox (ToolBoxUI): QMainWindow instance x (float): Icon x coordinate y (float): Icon y coordinate project_item (ProjectItem): Item icon_file (str): Path to icon resource icon_color (QColor): Icon's color background_color (QColor): Background color """ super().__init__() self._toolbox = toolbox self._project_item = project_item self._moved_on_scene = False self._previous_pos = QPointF() self._current_pos = QPointF() self.icon_group = {self} self.renderer = QSvgRenderer() self.svg_item = QGraphicsSvgItem(self) self.colorizer = QGraphicsColorizeEffect() self.setRect( QRectF(x - self.ITEM_EXTENT / 2, y - self.ITEM_EXTENT / 2, self.ITEM_EXTENT, self.ITEM_EXTENT)) self.text_font_size = 10 # point size # Make item name graphics item. name = project_item.name if project_item else "" self.name_item = QGraphicsSimpleTextItem(name, self) self.set_name_attributes() # Set font, size, position, etc. # Make connector buttons self.connectors = dict( bottom=ConnectorButton(self, toolbox, position="bottom"), left=ConnectorButton(self, toolbox, position="left"), right=ConnectorButton(self, toolbox, position="right"), ) # Make exclamation and rank icons self.exclamation_icon = ExclamationIcon(self) self.rank_icon = RankIcon(self) brush = QBrush(background_color) self._setup(brush, icon_file, icon_color) self.activate() def activate(self): """Adds items to scene and setup graphics effect. Called in the constructor and when re-adding the item to the project in the context of undo/redo. """ scene = self._toolbox.ui.graphicsView.scene() scene.addItem(self) shadow_effect = QGraphicsDropShadowEffect() shadow_effect.setOffset(1) shadow_effect.setEnabled(False) self.setGraphicsEffect(shadow_effect) def _setup(self, brush, svg, svg_color): """Setup item's attributes. Args: brush (QBrush): Used in filling the background rectangle svg (str): Path to SVG icon file svg_color (QColor): Color of SVG icon """ self.setPen(QPen(Qt.black, 1, Qt.SolidLine)) self.setBrush(brush) self.colorizer.setColor(svg_color) # Load SVG loading_ok = self.renderer.load(svg) if not loading_ok: self._toolbox.msg_error.emit( "Loading SVG icon from resource:{0} failed".format(svg)) return size = self.renderer.defaultSize() self.svg_item.setSharedRenderer(self.renderer) self.svg_item.setElementId( "") # guess empty string loads the whole file dim_max = max(size.width(), size.height()) rect_w = self.rect().width() # Parent rect width margin = 32 self.svg_item.setScale((rect_w - margin) / dim_max) x_offset = (rect_w - self.svg_item.sceneBoundingRect().width()) / 2 y_offset = (rect_w - self.svg_item.sceneBoundingRect().height()) / 2 self.svg_item.setPos(self.rect().x() + x_offset, self.rect().y() + y_offset) self.svg_item.setGraphicsEffect(self.colorizer) self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True) self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True) self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True) self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True) self.setAcceptHoverEvents(True) self.setCursor(Qt.PointingHandCursor) # Set exclamation and rank icons position self.exclamation_icon.setPos( self.rect().topRight() - self.exclamation_icon.sceneBoundingRect().topRight()) self.rank_icon.setPos(self.rect().topLeft()) def name(self): """Returns name of the item that is represented by this icon.""" return self._project_item.name def update_name_item(self, new_name): """Set a new text to name item. Used when a project item is renamed.""" self.name_item.setText(new_name) self.set_name_attributes() def set_name_attributes(self): """Set name QGraphicsSimpleTextItem attributes (font, size, position, etc.)""" # Set font size and style font = self.name_item.font() font.setPointSize(self.text_font_size) font.setBold(True) self.name_item.setFont(font) # Set name item position (centered on top of the master icon) name_width = self.name_item.boundingRect().width() name_height = self.name_item.boundingRect().height() self.name_item.setPos( self.rect().x() + self.rect().width() / 2 - name_width / 2, self.rect().y() - name_height - 4) def conn_button(self, position="left"): """Returns items connector button (QWidget).""" return self.connectors.get(position, self.connectors["left"]) def outgoing_links(self): return [ l for conn in self.connectors.values() for l in conn.outgoing_links() ] def incoming_links(self): return [ l for conn in self.connectors.values() for l in conn.incoming_links() ] def hoverEnterEvent(self, event): """Sets a drop shadow effect to icon when mouse enters its boundaries. Args: event (QGraphicsSceneMouseEvent): Event """ self.prepareGeometryChange() self.graphicsEffect().setEnabled(True) event.accept() def hoverLeaveEvent(self, event): """Disables the drop shadow when mouse leaves icon boundaries. Args: event (QGraphicsSceneMouseEvent): Event """ self.prepareGeometryChange() self.graphicsEffect().setEnabled(False) event.accept() def mousePressEvent(self, event): super().mousePressEvent(event) self.icon_group = set(x for x in self.scene().selectedItems() if isinstance(x, ProjectItemIcon)) | {self} for icon in self.icon_group: icon._previous_pos = icon.scenePos() def mouseMoveEvent(self, event): """Moves icon(s) while the mouse button is pressed. Update links that are connected to selected icons. Args: event (QGraphicsSceneMouseEvent): Event """ super().mouseMoveEvent(event) self.update_links_geometry() def moveBy(self, dx, dy): super().moveBy(dx, dy) self.update_links_geometry() def update_links_geometry(self): """Updates geometry of connected links to reflect this item's most recent position.""" links = set(link for icon in self.icon_group for conn in icon.connectors.values() for link in conn.links) for link in links: link.update_geometry() def mouseReleaseEvent(self, event): for icon in self.icon_group: icon._current_pos = icon.scenePos() # pylint: disable=undefined-variable if (self._current_pos - self._previous_pos ).manhattanLength() > qApp.startDragDistance(): self._toolbox.undo_stack.push(MoveIconCommand(self)) super().mouseReleaseEvent(event) def notify_item_move(self): if self._moved_on_scene: self._moved_on_scene = False scene = self.scene() scene.item_move_finished.emit(self) def contextMenuEvent(self, event): """Show item context menu. Args: event (QGraphicsSceneMouseEvent): Mouse event """ self.scene().clearSelection() self.setSelected(True) self._toolbox.show_item_image_context_menu(event.screenPos(), self.name()) def keyPressEvent(self, event): """Handles deleting and rotating the selected item when dedicated keys are pressed. Args: event (QKeyEvent): Key event """ if event.key() == Qt.Key_Delete and self.isSelected(): self._project_item._project.remove_item(self.name()) event.accept() elif event.key() == Qt.Key_R and self.isSelected(): # TODO: # 1. Change name item text direction when rotating # 2. Save rotation into project file rect = self.mapToScene(self.boundingRect()).boundingRect() center = rect.center() t = QTransform() t.translate(center.x(), center.y()) t.rotate(90) t.translate(-center.x(), -center.y()) self.setPos(t.map(self.pos())) self.setRotation(self.rotation() + 90) links = set(lnk for conn in self.connectors.values() for lnk in conn.links) for link in links: link.update_geometry() event.accept() else: super().keyPressEvent(event) def itemChange(self, change, value): """ Reacts to item removal and position changes. In particular, destroys the drop shadow effect when the items is removed from a scene and keeps track of item's movements on the scene. Args: change (GraphicsItemChange): a flag signalling the type of the change value: a value related to the change Returns: Whatever super() does with the value parameter """ if change == QGraphicsItem.ItemScenePositionHasChanged: self._moved_on_scene = True elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None: self.prepareGeometryChange() self.setGraphicsEffect(None) return super().itemChange(change, value) def show_item_info(self): """Update GUI to show the details of the selected item.""" ind = self._toolbox.project_item_model.find_item(self.name()) self._toolbox.ui.treeView_project.setCurrentIndex(ind)
class ProjectItemIcon(QGraphicsRectItem): """Base class for project item icons drawn in Design View.""" ITEM_EXTENT = 64 def __init__(self, toolbox, icon_file, icon_color, background_color): """ Args: toolbox (ToolboxUI): QMainWindow instance icon_file (str): Path to icon resource icon_color (QColor): Icon's color background_color (QColor): Background color """ super().__init__() self._toolbox = toolbox self.icon_file = icon_file self._moved_on_scene = False self.previous_pos = QPointF() self.current_pos = QPointF() self.icon_group = {self} self.renderer = QSvgRenderer() self.svg_item = QGraphicsSvgItem(self) self.colorizer = QGraphicsColorizeEffect() self.setRect( QRectF(-self.ITEM_EXTENT / 2, -self.ITEM_EXTENT / 2, self.ITEM_EXTENT, self.ITEM_EXTENT)) self.text_font_size = 10 # point size # Make item name graphics item. self._name = "" self.name_item = QGraphicsSimpleTextItem(self._name, self) self.set_name_attributes() # Set font, size, position, etc. # Make connector buttons self.connectors = dict( bottom=ConnectorButton(self, toolbox, position="bottom"), left=ConnectorButton(self, toolbox, position="left"), right=ConnectorButton(self, toolbox, position="right"), ) # Make exclamation and rank icons self.exclamation_icon = ExclamationIcon(self) self.execution_icon = ExecutionIcon(self) self.rank_icon = RankIcon(self) brush = QBrush(background_color) self._setup(brush, icon_file, icon_color) shadow_effect = QGraphicsDropShadowEffect() shadow_effect.setOffset(1) shadow_effect.setEnabled(False) self.setGraphicsEffect(shadow_effect) def finalize(self, name, x, y): """ Names the icon and moves it by given amount. Args: name (str): icon's name x (int): horizontal offset y (int): vertical offset """ self.update_name_item(name) self.moveBy(x, y) def _setup(self, brush, svg, svg_color): """Setup item's attributes. Args: brush (QBrush): Used in filling the background rectangle svg (str): Path to SVG icon file svg_color (QColor): Color of SVG icon """ self.setPen(QPen(Qt.black, 1, Qt.SolidLine)) self.setBrush(brush) self.colorizer.setColor(svg_color) # Load SVG loading_ok = self.renderer.load(svg) if not loading_ok: self._toolbox.msg_error.emit( "Loading SVG icon from resource:{0} failed".format(svg)) return size = self.renderer.defaultSize() self.svg_item.setSharedRenderer(self.renderer) self.svg_item.setElementId( "") # guess empty string loads the whole file dim_max = max(size.width(), size.height()) rect_w = self.rect().width() # Parent rect width margin = 32 self.svg_item.setScale((rect_w - margin) / dim_max) self.svg_item.setPos(self.rect().center() - self.svg_item.sceneBoundingRect().center()) self.svg_item.setGraphicsEffect(self.colorizer) self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True) self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True) self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True) self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True) self.setAcceptHoverEvents(True) self.setCursor(Qt.PointingHandCursor) # Set exclamation, execution_log, and rank icons position self.exclamation_icon.setPos( self.rect().topRight() - self.exclamation_icon.sceneBoundingRect().topRight()) self.execution_icon.setPos( self.rect().bottomRight() - 0.5 * self.execution_icon.sceneBoundingRect().bottomRight()) self.rank_icon.setPos(self.rect().topLeft()) def name(self): """Returns name of the item that is represented by this icon. Returns: str: icon's name """ return self._name def update_name_item(self, new_name): """Set a new text to name item. Args: new_name (str): icon's name """ self._name = new_name self.name_item.setText(new_name) self.set_name_attributes() def set_name_attributes(self): """Set name QGraphicsSimpleTextItem attributes (font, size, position, etc.)""" # Set font size and style font = self.name_item.font() font.setPointSize(self.text_font_size) font.setBold(True) self.name_item.setFont(font) # Set name item position (centered on top of the master icon) name_width = self.name_item.boundingRect().width() name_height = self.name_item.boundingRect().height() self.name_item.setPos( self.rect().x() + self.rect().width() / 2 - name_width / 2, self.rect().y() - name_height - 4) def conn_button(self, position="left"): """Returns item's connector button. Args: position (str): "left", "right" or "bottom" Returns: QWidget: connector button """ return self.connectors.get(position, self.connectors["left"]) def outgoing_links(self): """Collects outgoing links. Returns: list of LinkBase: outgoing links """ return [ l for conn in self.connectors.values() for l in conn.outgoing_links() ] def incoming_links(self): """Collects incoming links. Returns: list of LinkBase: outgoing links """ return [ l for conn in self.connectors.values() for l in conn.incoming_links() ] def run_execution_leave_animation(self, skipped): """ Starts the animation associated with execution leaving the icon. Args: skipped (bool): True if project item was not actually executed. """ animation_group = QParallelAnimationGroup(self._toolbox) for link in self.outgoing_links(): animation_group.addAnimation( link.make_execution_animation(skipped)) animation_group.start() def hoverEnterEvent(self, event): """Sets a drop shadow effect to icon when mouse enters its boundaries. Args: event (QGraphicsSceneMouseEvent): Event """ self.prepareGeometryChange() self.graphicsEffect().setEnabled(True) event.accept() def hoverLeaveEvent(self, event): """Disables the drop shadow when mouse leaves icon boundaries. Args: event (QGraphicsSceneMouseEvent): Event """ self.prepareGeometryChange() self.graphicsEffect().setEnabled(False) event.accept() def mousePressEvent(self, event): super().mousePressEvent(event) self.icon_group = set(x for x in self.scene().selectedItems() if isinstance(x, ProjectItemIcon)) | {self} for icon in self.icon_group: icon.previous_pos = icon.scenePos() def mouseMoveEvent(self, event): """Moves icon(s) while the mouse button is pressed. Update links that are connected to selected icons. Args: event (QGraphicsSceneMouseEvent): Event """ super().mouseMoveEvent(event) self.update_links_geometry() def moveBy(self, dx, dy): super().moveBy(dx, dy) self.update_links_geometry() def update_links_geometry(self): """Updates geometry of connected links to reflect this item's most recent position.""" links = set(link for icon in self.icon_group for conn in icon.connectors.values() for link in conn.links) for link in links: link.update_geometry() def mouseReleaseEvent(self, event): for icon in self.icon_group: icon.current_pos = icon.scenePos() # pylint: disable=undefined-variable if (self.current_pos - self.previous_pos ).manhattanLength() > qApp.startDragDistance(): self._toolbox.undo_stack.push( MoveIconCommand(self, self._toolbox.project())) super().mouseReleaseEvent(event) def notify_item_move(self): if self._moved_on_scene: self._moved_on_scene = False scene = self.scene() scene.item_move_finished.emit(self) def contextMenuEvent(self, event): """Show item context menu. Args: event (QGraphicsSceneMouseEvent): Mouse event """ event.accept() self.scene().clearSelection() self.setSelected(True) ind = self._toolbox.project_item_model.find_item(self.name()) self._toolbox.show_project_item_context_menu(event.screenPos(), ind) def itemChange(self, change, value): """ Reacts to item removal and position changes. In particular, destroys the drop shadow effect when the items is removed from a scene and keeps track of item's movements on the scene. Args: change (GraphicsItemChange): a flag signalling the type of the change value: a value related to the change Returns: Whatever super() does with the value parameter """ if change == QGraphicsItem.ItemScenePositionHasChanged: self._moved_on_scene = True elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None: self.prepareGeometryChange() self.setGraphicsEffect(None) return super().itemChange(change, value) def select_item(self): """Update GUI to show the details of the selected item.""" ind = self._toolbox.project_item_model.find_item(self.name()) self._toolbox.ui.treeView_project.setCurrentIndex(ind)
class InteractiveChartView(QtCharts.QChartView): """ A ChartView which optionally supports value tooltip, mouse position tracker, zoom and pan """ def __init__(self, chart: QtCharts.QChart = None, parent: QWidget = None, setInWindow: bool = False): super().__init__(parent) self._setInWindow: bool = setInWindow self._coordX: QGraphicsItem = None self._coordY: QGraphicsItem = None # self.__callouts: List[Callout] = None # Disabled for now self._tooltip: Callout = None # Internal fields self._mousePressEventPos: QPointF = None self._panOn: bool = False self._chartIsSet: bool = False # True iff a valid (i.e. non empty) chart is set in this view # Option enable flags self._panEnabled: bool = True self._zoomEnabled: bool = True self._keySeqEnabled: bool = True self._calloutEnabled: bool = True self._positionTrackerEnabled: bool = True self._openInWindowDoubleClick: bool = True self.setDragMode(QGraphicsView.NoDrag) self.setRubberBand(QtCharts.QChartView.RectangleRubberBand) self.setMouseTracking(True) self.setInteractive(True) if chart: self.setChart(chart) def enablePan(self, value: bool) -> None: self._panEnabled = value def enableZoom(self, value: bool) -> None: self._zoomEnabled = value if value: self.setRubberBand(QtCharts.QChartView.RectangleRubberBand) else: self.setRubberBand(QtCharts.QChartView.NoRubberBand) def enableKeySequences(self, value: bool) -> None: self._keySeqEnabled = value def enableCallout(self, value: bool) -> None: self._calloutEnabled = value def enablePositionTracker(self, value: bool) -> None: self._positionTrackerEnabled = value def enableInWindow(self, value: bool) -> None: self._openInWindowDoubleClick = value def setChart(self, chart: QtCharts.QChart) -> None: """ Sets a new chart in the view. Doesn't delete the previous chart """ # Set New chart super().setChart(chart) # Update fields series: List[QtCharts.QAbstractSeries] = chart.series() if not series: # Empty chart self._chartIsSet = False else: self._chartIsSet = True # self.__callouts = list() self._tooltip = Callout(chart) chart.setAcceptHoverEvents(True) for s in series: # s.clicked.connect(self.keepCallout) s.hovered.connect(self.tooltip) if self._chartIsSet and self._positionTrackerEnabled: self._coordX = QGraphicsSimpleTextItem(chart) self._coordX.setText("X: ") self._coordY = QGraphicsSimpleTextItem(chart) self._coordY.setText("Y: ") self._updateMouseTrackerPosition( ) # Show them in the correct place @staticmethod def _updateAxisTickCount(chart: QtCharts.QChart, axis: QtCharts.QAbstractAxis, newSize: QSize) -> None: """ Given an axis and the size of the view, sets the number of ticks to the best value avoiding too many overlapping labels """ # Get one label as string and the current number of ticks/labels label: str ticks: int if axis.type() == QtCharts.QAbstractAxis.AxisTypeDateTime: ticks = axis.tickCount() # current number of dates shown label = axis.min().toString(axis.format()) elif axis.type() == QtCharts.QAbstractAxis.AxisTypeBarCategory: ticks = axis.count() # number of labels label = axis.at(0) if ticks else None elif axis.type() == QtCharts.QAbstractAxis.AxisTypeCategory: ticks = axis.count() # number of labels labels = axis.categoriesLabels() label = labels[0] if labels else None else: return # Axis type not supported if not label: # No labels set return # Decide which dimension is relevant for resizing margins = chart.margins() # layoutMargins: (left, top, right, bottom) layoutMargins: Tuple[float, ...] = chart.layout().getContentsMargins() if layoutMargins: layoutMargins = tuple( [i if i is not None else 0.0 for i in layoutMargins]) offset: int = 0 if axis.orientation() == Qt.Horizontal: if margins: offset += margins.left() + margins.right() if layoutMargins: offset += layoutMargins[0] + layoutMargins[2] # 'length' is the available space for displaying labels, without margins and the space # between every label length = newSize.width() - offset - (ticks * 10) else: if margins: offset += margins.top() + margins.bottom() if layoutMargins: offset += layoutMargins[1] + layoutMargins[3] length = newSize.height() - offset - (ticks * 10) # Compute the optimal width of the label (in pixel) metrics = QFontMetrics(axis.labelsFont()) optimalWidth: int = metrics.horizontalAdvance( label) * 1.9 # not precise, 1.9 is to fix it # Deal with every type separately if axis.type() == QtCharts.QAbstractAxis.AxisTypeDateTime: # Determine optimal number of ticks to avoid much overlapping newTicks = int(length / optimalWidth) - 1 axis.setTickCount(newTicks) elif axis.type() == QtCharts.QAbstractAxis.AxisTypeCategory or axis.type() == \ QtCharts.QAbstractAxis.AxisTypeBarCategory: labelSpace: float = length / (ticks * 2) if labelSpace < optimalWidth: deg = min([ 90, np.degrees(np.arccos(labelSpace / optimalWidth) * 1.1) ]) axis.setLabelsAngle(deg) else: axis.setLabelsAngle(0) def setBestTickCount(self, newSize: QSize) -> None: if self._chartIsSet: xAxis = self.chart().axisX() yAxis = self.chart().axisY() if xAxis: self._updateAxisTickCount(self.chart(), xAxis, newSize) if yAxis: self._updateAxisTickCount(self.chart(), yAxis, newSize) def _updateMouseTrackerPosition(self, xOffset: int = 50, yOffset: int = 20) -> None: if self._chartIsSet and self._positionTrackerEnabled: # Update coordinates tracker position self._coordX.setPos(self.chart().size().width() / 2 - xOffset, self.chart().size().height() - yOffset) self._coordY.setPos(self.chart().size().width() / 2 + xOffset, self.chart().size().height() - yOffset) def resizeEvent(self, event: QResizeEvent): if self.scene() and self._chartIsSet: self.scene().setSceneRect(QRectF(QPointF(0, 0), event.size())) self.chart().resize(event.size()) # Update axis self.setBestTickCount(event.size()) # Update coordinates tracker position (if tracker is active) self._updateMouseTrackerPosition() super().resizeEvent(event) def mousePressEvent(self, event: QMouseEvent) -> None: if self._chartIsSet and self._panEnabled and event.button( ) == Qt.MiddleButton: self._mousePressEventPos = event.pos() self._panOn = True QApplication.setOverrideCursor(QCursor(Qt.ClosedHandCursor)) super().mousePressEvent(event) def mouseMoveEvent(self, event: QMouseEvent) -> None: if self._panEnabled and self._panOn: offset = event.pos() - self._mousePressEventPos self.chart().scroll(-offset.x(), offset.y()) self._mousePressEventPos = event.pos() event.accept() elif self._chartIsSet and self._positionTrackerEnabled: metrics = QFontMetrics(self._coordX.font()) xVal = self.chart().mapToValue(event.pos()).x() yVal = self.chart().mapToValue(event.pos()).y() # if self.chart().axisX().type() == QtCharts.QAbstractAxis.AxisTypeDateTime: xText: str = 'X: {}'.format( computeAxisValue(self.chart().axisX(), xVal)) yText: str = 'Y: {}'.format( computeAxisValue(self.chart().axisY(), yVal)) xSize = metrics.width(xText, -1) ySize = metrics.width(yText, -1) totSize = xSize + ySize self._updateMouseTrackerPosition(xOffset=(totSize // 2)) self._coordX.setText(xText) self._coordY.setText(yText) super().mouseMoveEvent(event) def mouseReleaseEvent(self, event: QMouseEvent) -> None: if self._panEnabled and self._panOn: self._panOn = False QApplication.restoreOverrideCursor() super().mouseReleaseEvent(event) def keyPressEvent(self, event: QKeyEvent) -> None: if not self._chartIsSet: return if event.key() == Qt.Key_Left: self.chart().scroll(-10, 0) elif event.key() == Qt.Key_Right: self.chart().scroll(+10, 0) elif event.key() == Qt.Key_Up: self.chart().scroll(0, +10) elif event.key() == Qt.Key_Down: self.chart().scroll(0, -10) elif self._keySeqEnabled and event.key( ) == Qt.Key_R and event.modifiers() & Qt.ControlModifier: self.chart().zoomReset() else: super().keyPressEvent(event) def wheelEvent(self, event: QWheelEvent) -> None: if self._chartIsSet and self._zoomEnabled: delta: int = event.angleDelta().y() factor = pow(1.25, delta / 240.0) self.chart().zoom(factor) event.accept() # @Slot() # Disabled because it causes too many problems # def keepCallout(self): # if not self._calloutEnabled: # return # self.__callouts.append(self._tooltip) # self._tooltip = Callout(self.chart()) # @Slot() # Disabled because it causes too many problems # def clearCallouts(self) -> None: # for c in self.__callouts: # self.scene().removeItem(c) # self.__callouts = list() @Slot(QPointF, bool) def tooltip(self, point: QPointF, state: bool): if not self._calloutEnabled: return if not self._tooltip: self._tooltip = Callout(self.chart()) if state: self._tooltip.setText('X: {} \nY: {} '.format( computeAxisValue(self.chart().axisX(), point.x()), computeAxisValue(self.chart().axisY(), point.y()))) self._tooltip.setAnchor(point) self._tooltip.setZValue(11) self._tooltip.updateGeometry() self._tooltip.show() else: self._tooltip.hide() def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: if self._chartIsSet and self._openInWindowDoubleClick and not self._setInWindow and \ event.button() == Qt.LeftButton: chartWindow = InteractiveChartWindow( self) # needs a parent to be kept alive # Open widget with plot chart = copyChart(self.chart()) iView = InteractiveChartView(chart=chart, setInWindow=True) iView.enableKeySequences(False) iView.setRenderHints(self.renderHints()) chartWindow.setAttribute(Qt.WA_DeleteOnClose, True) chartWindow.setCentralWidget( iView) # window takes ownership of view chartWindow.resize(600, 500) chartWindow.show() event.accept() super().mouseDoubleClickEvent(event)