Drag and drop issue: widgets dragged out of container disappear

Hello, everybody! I read this article Drag and Drop widgets in PySide6 with this Drop-in Sortable Widget and i had a similar task in my project with moving buttons by drag-and-drop. I faced a problem: If I drop an item in an inaccessible area, it disappears from the list. How can this be prevented? How can I change dropEvent? Thanks for your answers in advance.

def dropEvent(self, e):
    widget = e.source()
    
    # Use drop target location for destination, then remove it.
    self._drag_target_indicator.hide()
    index = self.blayout.indexOf(self._drag_target_indicator)

    if index is not None:
        self.blayout.insertWidget(index, widget)
        self.orderChanged.emit(self.get_item_data())
        widget.show()
        self.blayout.activate()
    

    e.accept()
1 Like

Hi @Margo-tail welcome to the community!

In the code we hide the dragged widget while it’s being dragged, showing the drag target indicator instead. I think all you would need to do to handle the abort is to un-hide the widget with e.source().hide() or widget.show() even if index is None.

So something like:

def dropEvent(self, e):
    widget = e.source()
    
    # Use drop target location for destination, then remove it.
    self._drag_target_indicator.hide()
    index = self.blayout.indexOf(self._drag_target_indicator)

    if index is not None:
        self.blayout.insertWidget(index, widget)
        self.orderChanged.emit(self.get_item_data())
        self.blayout.activate()

    widget.show()    

    e.accept()

It should return to it’s original position (as it hasn’t been moved yet).

Unfortunately, this does not solve the problem. The video shows that objects thrown off the list disappear completely. https://drive.google.com/file/d/1SAa22ooB1DQSx4twIpKVeKTnq9xd8pYq/view
The reason is that index is not equal to None when the object is in an inaccessible zone. What condition must be fulfilled in order to track whether the object is in the right place when throwing?

def dragMoveEvent(self, e):
        # Find the correct location of the drop target, so we can move it there.
        index = self._find_drop_location(e)
        if index is not None:
            # Inserting moves the item if its alreaady in the layout.
            self.blayout.insertWidget(index, self._drag_target_indicator)
            # Hide the item being dragged.
            e.source().hide()
            # Show the target.
            self._drag_target_indicator.show()
        e.accept()

def dropEvent(self, e):
    widget = e.source()
    # Use drop target location for destination, then remove it.
    self._drag_target_indicator.hide()
    index = self.blayout.indexOf(self._drag_target_indicator)
    if index is not None:
        self.blayout.insertWidget(index, widget)
        self.orderChanged.emit(self.get_item_data())
        
        self.blayout.activate()
    widget.show()
    e.accept()

def _find_drop_location(self, e):
    pos = e.position()
    spacing = self.blayout.spacing() / 2

    for n in range(self.blayout.count()):
        # Get the widget at each index in turn.
        w = self.blayout.itemAt(n).widget()

        if self.orientation == Qt.Orientation.Vertical:
            # Drag drop vertically.
            drop_here = (
                pos.y() >= w.y() - spacing
                and pos.y() <= w.y() + w.size().height() + spacing
            )
        else:
            # Drag drop horizontally.
            drop_here = (
                pos.x() >= w.x() - spacing
                and pos.x() <= w.x() + w.size().width() + spacing
            )

        if drop_here:
            # Drop over this target.
            break

    return n

Oh, I see the issue @Margo-tail We need to re-show the widget in the mouse handler on the widget itself (after the exec). If the widget has moved, it will be shown anyway, but in the case where it drops outside we need to make it visible again.


class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)
            self.show() # Make the dragged widget visible again, if dragged outside.

I’ll update the code on the site, since this is a bug really. The widget looks like it’s being removed but it’s actually not.

1 Like