1

I have a MVC QAbstractItemModel that has drag and dropped enabled, like so:

class Model(QtCore.QAbstractItemModel):

    # ... other code ...

    def flags(self, index):
        flags = super(Model, self).flags(index)

        flags |= QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled

        return flags

This enables drag-and-drop but the drag-and-drop behavior is too permissive. In a tree like this:

- root
    - A
    - B
    - C
        - D
        - E

I only want to allow drag-and-drop if the dropped index keeps its original parent.

In other works, I can reorder A, B, or C underneath root and I can reorder D or E underneath C, but I cannot move A into C, or move D/E into A etc.

This is a "valid" drag-and-drop:

- root
    - B
    - C
        - D
        - E
    - A

because A was moved to the last row, but A still has the same parent, root.

But this is not a valid drag-and-drop

- root
    - B
        - A
    - C
        - D
        - E

A's parent was changed from root to B, which is not what I want.

First Attempt

I tried to achieve this "always keep the same parent" logic by subclassing QTreeView and adding dragMoveEvent to it, like this

class MyTreeSubClass(QtWidgets.QTreeView):

    # ... more code ...

    def dragMoveEvent(self, event):
        super(MyTreeSubClass, self).dragMoveEvent(event)

        widget = event.source()

        if widget != self:
            # Forbid dropping any external data to this widget
            event.ignore()

            return

        index = widget.indexAt(event.pos())

        for selected in widget.selectedIndexes():
            if index.parent() != selected.parent():
                event.ignore()

                return

        event.accept()

But this doesn't work I think because index = widget.indexAt(event.pos()) returns the same index , whether you're hovering directly over an index in the tree or between two indexes.

Is there a more reliable way to tell if a position is directly above an index (and if so, call event.reject())? If so, I may be able to use that to accept / reject the event.

If there's an easier way to achieve what I'm looking for, I would greatly appreciate the advice.

ColinKennedy
  • 828
  • 7
  • 24

1 Answers1

0

I managed to get this working with dragMoveEvent, after all.

The code became something like this:

  • on drag
    • Get the nearest index
    • If the cursor position (event.pos()) is above or below this index, event.accept() dropping.
    • Otherwise, event.ignore() instead
from PySide2 import QtCore, QtWidgets

IN_BETWEEN_POSITIONS = frozenset(
    (
        QtWidgets.QAbstractItemView.AboveItem, 
        QtWidgets.QAbstractItemView.BelowItem
    )
)

class MyTreeSubClass(QtWidgets.QTreeView):

    # ... more code ...

    def dragMoveEvent(self, event):
        widget = event.source()

        if widget != self:
            # Forbid dropping any external data to this widget
            event.ignore()

            return
        
        index = widget.indexAt(event.pos())
        position = _get_position(position, self.visualRect(index))

        if position in IN_BETWEEN_POSITIONS:
            super(MyTreeSubClass, self).dragMoveEvent(event)
            event.accept()

            return

        event.ignore()


def _get_position(position, bounds):
    output = QtWidgets.QAbstractItemView.OnViewport
    margin = 2

    if position.y() - bounds.top() < margin:
        return QtWidgets.QAbstractItemView.AboveItem

    if bounds.bottom() - position.y() < margin:
        return QtWidgets.QAbstractItemView.BelowItem

    if bounds.contains(position, True):
        return QtWidgets.QAbstractItemView.OnItem

    return output

I found _get_position's code from searching online

https://stackoverflow.com/a/26311179/3626104

https://github.com/jimmykuu/PyQt-PySide-Cookbook/blob/master/tree/drop_indicator.md

From what I understand, widget.indexAt always returns a proper index as long as the position is in a QTreeView, which isn't ideal if the position happens to be between more than one index. _get_position essentially clarifies whether the returned index is actually OnItem or somewhere else.

ColinKennedy
  • 828
  • 7
  • 24