pythonpyside2qgraphicsitem

QGraphicsItem only visible when parent selected


I'm trying to mimick path editing similar to what you would see in photoshop, which interacts this way...

  1. You select the Path and it's Points become visible
  2. Users can click and drag any Point item of the Path, to adjust the Path
  3. When users click and drag the Path directly it moves the Path
  4. When the Path is deselected the Points become hidden again

Where I'm having issues are

  1. Making the Points hidden when the Path is deselected but not hidden when a Point of the selected spline is being Edited

Here is what i have:

enter image description here

Here is a reference to something I'm trying to match:

enter image description here

import sys
import math
import random

from PySide2 import QtWidgets, QtGui, QtCore

# SETTINGS
handle_size = 16
handle_color = QtGui.QColor(40,130,230)
handle_radius = 8


class AnnotationPointItem(QtWidgets.QGraphicsEllipseItem):
    def __init__(self, positionFlag=0, pos=QtCore.QPointF(), parent=None):
        super(AnnotationPointItem, self).__init__(-handle_radius, -handle_radius, 2*handle_radius, 2*handle_radius, parent)
        
        self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)    
        self.setPen(QtGui.QPen(handle_color, 4, QtCore.Qt.SolidLine))
        self.setBrush(QtGui.QBrush(QtGui.QColor('white')))
        self.positionFlag = positionFlag 


    def paint(self, painter, option, widget=None):
        # Remove the selection outline
        # if self.isSelected():
        #     option.state &= ~QtWidgets.QStyle.State_Selected

        super(AnnotationPointItem, self).paint(painter, option, widget)


    # def mousePressEvent(self, event):
    #     # Handle the event, but don't propagate to the parent
    #     # event.accept()
    #     print('clicked....')
    #     return super(AnnotationPointItem, self).mousePressEvent(event)


    def itemChange(self, change, value):
        # print(change, self.isSelected())
        if change == QtWidgets.QGraphicsItem.ItemPositionChange:
            # print('ItemPositionChange')
            pass
        elif change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
            # print('ItemPositionHasChanged')
            parent = self.parentItem()
            if parent:
                # Get the position of the cursor in the view's coordinates
                if self.positionFlag == 0:
                    parent.setPoints(start=self.pos())
                elif self.positionFlag == 1:
                    parent.setPoints(end=self.pos())
        elif change == QtWidgets.QGraphicsItem.ItemSelectedChange:
            pass
        return super(AnnotationPointItem, self).itemChange(change, value)


class AnnotationPathItem(QtWidgets.QGraphicsLineItem):
    def __init__(self, 
        start=QtCore.QPointF(), 
        end=QtCore.QPointF(), 
        color=QtCore.Qt.green,
        thickness=10,
        parent=None):
        super(AnnotationPathItem, self).__init__(start.x(), start.y(), end.x(), end.y(), parent)

        self._color = color
        self._thickness = thickness

        self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable)
        self.setPen(QtGui.QPen(self._color, self._thickness, QtCore.Qt.SolidLine))

        # child items
        self.startPointItem = AnnotationPointItem(positionFlag=0, parent=self)
        self.startPointItem.hide()
        self.startPointItem.setPos(self.line().p1())

        self.endPointItem = AnnotationPointItem(positionFlag=1, parent=self)
        self.endPointItem.hide()
        self.endPointItem.setPos(self.line().p2())


    def itemChange(self, change, value):
        if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
            self.selectionChanged(value)
        return super(AnnotationPathItem, self).itemChange(change, value)


    def selectionChanged(self, selected):
        # Implement what you want to do when the selection changes
        print(self.startPointItem.isSelected(), self.endPointItem.isSelected())
        if selected or self.startPointItem.isSelected() or self.endPointItem.isSelected():
            self.startPointItem.show()
            self.endPointItem.show()
        # else:
        #     self.startPointItem.hide()
        #     self.endPointItem.hide()


    def paint(self, painter, option, widget=None):
        # Remove the selection outline
        if self.isSelected():
            option.state &= ~QtWidgets.QStyle.State_Selected

        super(AnnotationPathItem, self).paint(painter, option, widget)


    def setPoints(self, start=None, end=None):
        currentLine = self.line()
        if start != None:
            currentLine.setP1(start)
        if end != None:
            currentLine.setP2(end)
        self.setLine(currentLine)


class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.resize(1200,1200)

        self.scene = QtWidgets.QGraphicsScene(self)
        self.scene.setBackgroundBrush(QtGui.QColor(40,40,40))

        self.view = QtWidgets.QGraphicsView(self)
        self.view.setSceneRect(-4000, -4000, 8000, 8000)
        self.view.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
        self.view.setMouseTracking(True)
        self.view.setScene(self.scene)

        self.addButton = QtWidgets.QPushButton("Add Annotation", self)
        self.addButton.clicked.connect(self.add_annotation)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.view)
        layout.addWidget(self.addButton)
        self.setLayout(layout)

        # samples
        item = AnnotationPathItem(QtCore.QPointF(-70, -150), QtCore.QPointF(150, -350))
        self.scene.addItem(item)


    def add_annotation(self):
        r = random.randint(0,255)
        g = random.randint(0,255)
        b = random.randint(0,255)
        color = QtGui.QColor(r,g,b)
        startPos = QtCore.QPointF(random.randint(-200,200), random.randint(-200,200))
        endPos = QtCore.QPointF(random.randint(-200,200), random.randint(-200,200))
        item = AnnotationPathItem(startPos, endPos, color)
        self.scene.addItem(item)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

Solution

  • You cannot achieve this by checking the selection changes of the item, because when an item gets selected upon mouse press, the view has already deselected all the other.

    Instead, you can use the selectionChanged signal of the scene, which is emitted when all selection changes are completed, and decide whether to show items or not. Since items are usually not created with a scene at first, you have to connect to the signal upon the ItemSceneChange item change:

        def itemChange(self, change, value):
            if change == QGraphicsItem.ItemSceneChange:
                if self.scene():
                    self.scene().selectionChanged.disconnect(
                        self.updateChildSelection)
                if value:
                    value.selectionChanged.connect(self.updateChildSelection)
            return super(AnnotationPathItem, self).itemChange(change, value)
    
        def updateChildSelection(self):
            selection = set(self.scene().selectedItems())
            showItems = bool(
                set((self, self.startPointItem, self.endPointItem)) & selection
            )
            self.startPointItem.setVisible(showItems)
            self.endPointItem.setVisible(showItems)