0

I'm developing a GUI where you can connect nodes between them except in a few special cases. This implementation works perfectly fine most of the time, but after some testing i found that, when I connect one QGraphicsPixmapItem with another through a QGraphicsLineItem, and the user opens the contextual menu before completing the link, the line get stuck, and it cannot be deleted.

The process to link two elements is to first press the element, then keep pressing while moving the line and releasing when the pointer is over the other element. This is achieved using mousePressEvent, mouseMoveEvent and mouseReleaseEvent, respectively.

This code is an example:

#!/usr/bin/env python3

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

import sys


class Ellipse(QGraphicsEllipseItem):
    def __init__(self, x, y):
        super(Ellipse, self).__init__(x, y, 30, 30)

        self.setBrush(QBrush(Qt.darkBlue))
        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setZValue(100)

    def contextMenuEvent(self, event):
        menu = QMenu()

        first_action = QAction("First action")
        second_action = QAction("Second action")

        menu.addAction(first_action)
        menu.addAction(second_action)

        action = menu.exec(event.screenPos())


class Link(QGraphicsLineItem):
    def __init__(self, x, y):
        super(Link, self).__init__(x, y, x, y)

        self.pen_ = QPen()
        self.pen_.setWidth(2)
        self.pen_.setColor(Qt.red)

        self.setPen(self.pen_)

    def updateEndPoint(self, x2, y2):
        line = self.line()
        self.setLine(line.x1(), line.y1(), x2, y2)


class Scene(QGraphicsScene):
    def __init__(self):
        super(Scene, self).__init__()

        self.link = None
        self.link_original_node = None

        self.addItem(Ellipse(200, 400))
        self.addItem(Ellipse(400, 400))

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            item = self.itemAt(event.scenePos(), QTransform())
            if item is not None:
                self.link_original_node = item
                offset = item.boundingRect().center()
                self.link = Link(item.scenePos().x() + offset.x(), item.scenePos().y() + offset.y())
                self.addItem(self.link)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        if self.link is not None:
            self.link.updateEndPoint(event.scenePos().x(), event.scenePos().y())

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        if self.link is not None:
            item = self.itemAt(event.scenePos(), QTransform())
            if isinstance(item, (Ellipse, Link)):
                self.removeItem(self.link)
                self.link_original_node = None
                self.link = None


class MainWindow(QMainWindow):
    def __init__(self):
        super(QMainWindow, self).__init__()

        self.scene = Scene()
        self.canvas = QGraphicsView()

        self.canvas.setScene(self.scene)
        self.setCentralWidget(self.canvas)

        self.setGeometry(500, 200, 1000, 600)
        self.setContextMenuPolicy(Qt.NoContextMenu)


app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())

How can I get rid off the line before/after the context menu event? I tried to stop them, but I do not know how.

dpoloa
  • 15
  • 3
  • It seems that the link item is removed only when it's released on an ellipse item. Shouldn't it be removed in any case, especially if the mouse is *not* released on any item? – musicamante Nov 12 '21 at 18:52
  • @musicamante thanks for seeing it! I did a mistake in the code and now it is correct. The objective is to not link the elements between them in any case, so if the line lands out of any element, it must be removed, just as the updated code does. The error only occurs when the contextual menu is open. – dpoloa Nov 12 '21 at 19:00
  • Since that menu can only be opened with the mouse, I suppose the problem occurs when the user *also* right clicks *while* the left button is kept pressed, right? In any case, considering that there are situations for which the line item might *not* be *exactly* at the cursor position (this is due to the complex way mapping of position works), so you should always check `if self.link is not None` in the mouse release and eventually remove it based on that, and rely on `itemAt` only for successful linking. – musicamante Nov 12 '21 at 19:05
  • @musicamante that's right, the problem takes place when pressing right button while you keep pressing the left button. The problem to the solution you propose is that the mouseReleaseEvent does not activate when the user accesses the contextual menu. It is like the link gets into a "void". – dpoloa Nov 12 '21 at 19:49
  • That is *another* point: the mouse is implicitly released when a menu is opened as result of a context menu event. The fact remains, the item should always be removed on mouse release if it exists, not only after checking that the mouse is *over* it: since it's assumed that it's created on mouse press, the link item position is irrelevant. See my answer. Also note that while your approach "works", it has some issues: for instance, not calling the base `mousePressEvent` implementation *at all* might result in unexpected behavior (try double clicking). – musicamante Nov 12 '21 at 20:12

1 Answers1

0

Assuming that the menu is only triggered from a mouse button press, the solution is to remove any existing link item in the mouseButtonPress too.

    def mousePressEvent(self, event):
        if self.link is not None:
            self.removeItem(self.link)
            self.link_original_node = None
            self.link = None
        # ...

Note that itemAt for very small items is not always reliable, as the item's shape() might be slightly off the mapped mouse position. Since the link would be removed in any case, just do the same in the mouseReleaseEvent():

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        if self.link is not None:
            item = self.itemAt(event.scenePos(), QTransform())
            if isinstance(item, Ellipse):
                # do what you need with the linked ellipses

            # note the indentation level
            self.removeItem(self.link)
            self.link_original_node = None
            self.link = None
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • That was it! Just with the first piece of code, it worked! Thanks! – dpoloa Nov 12 '21 at 20:11
  • @dpoloa the second part is also important. I've been able to reproduce the issue more than once: at certain angles and distances, the `item` returned by `itemAt` is None even if the link is active. You *must* remove it in any case if it exists on mouse release, not only if it is under the mouse. – musicamante Nov 12 '21 at 20:14