Search code examples
listviewdrag-and-dropqml

QML Drag-and-drop ListView always on first element


I need to be able to move ListView rows, example: if I drag row 2 over item 4, it should insert before item 4. I followed multiple examples, the only thing I could make dragging work shows below, but the DropArea is always over the first element. This is my code, only with styling removed:

DragDropEx.qml

import QtQuick 2.4
import QtQuick.Window 2.2
import QtQml.Models 2.1

Window {
    width: 784; height: 250; visible: true;

    Flickable {
        anchors.fill: parent

        property var listOfStrings: ["Hello", "world", "I", "need", "help"]
        ListModel { id: listOfStringsModel }

        Component.onCompleted: {
            if (visible) {
                listOfStringsModel.clear();
                for (var i = 0; i < listOfStrings.length; i++) {
                    listOfStringsModel.append({ "textEntry": listOfStrings[i] });
                }
                textListView.currentIndex = listOfStrings.length - 1;
            }
        }

        Rectangle {
            anchors.fill: parent; color: "lightblue"

            ListView {
                id: textListView
                x: 10; y: 10; width: parent.width - 20; height: parent.height - 20
                spacing: 4
                model: DelegateModel {
                    id: visualModel
                    model: listOfStringsModel
                    delegate: TextListDelegate {
                        id: textListDelegateItem
                        height: 32; width: parent.width - 16
                        anchors { horizontalCenter: parent.horizontalCenter }
                    }
                }
            }
        }
    }
}

TextListDelegate.qml

import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQml.Models 2.1

MouseArea {
    id: delegateRoot

    property bool held: false
    property int visualIndex: DelegateModel.itemsIndex

    drag.target: held ? textEntryContainerRect : undefined
    drag.axis: Drag.YAxis
    drag.filterChildren: true   // this is because of the text field

    onPressAndHold: {
        held = true;
        textEntryContainerRect.opacity = 0.5;
    }
    onReleased: {
        if (held === true) {
            held = false;
            textEntryContainerRect.opacity = 1;
            textEntryContainerRect.Drag.drop();
        }
    }

    Rectangle {
        id: textEntryContainerRect
        anchors.fill: parent

        Drag.active: delegateRoot.drag.active
        Drag.source: delegateRoot
        Drag.hotSpot.x: textListDelegateItem.width / 2
        Drag.hotSpot.y: textListDelegateItem.height / 2

        states: [
            State {
                when: textEntryContainerRect.Drag.active

                ParentChange {
                    target: textEntryContainerRect
                    parent: textListView
                }

                AnchorChanges {
                    target: textEntryContainerRect
                    anchors.horizontalCenter: undefined
                    anchors.verticalCenter: undefined
                }
            }
        ]

        TextField {
            id: textListEntryField
            anchors { fill: parent; leftMargin: 8 }
            height: 30; text: textEntry
        }
    }

    DropArea {
        id: dropArea
        anchors { fill: parent; }
        onDropped: {
            var sourceIndex = drag.source.visualIndex;
            var targetIndex = delegateRoot.visualIndex;
            listOfStringsModel.move(sourceIndex, targetIndex, 1);
        }
        Rectangle {
            anchors.fill: parent
            color: "hotpink"
            visible: parent.containsDrag
        }
    }
}

I colored the drop area to show that it is stubbornly jumping to first element every time I press and hold, then try to move a row. How can I make it go to the row where the mouse is , similar to all examples on the web ?

Qt version: 5.12 - 5.17

Notes:

  • the DelegateModel was used in this Qt tutorial which is my most successful version.
  • press-and-hold needed for embedded
  • TextField is accompanied by other controls, and the model has other fields, that is why it may look more complicated than needed, solution needs to work with them.

Solution

  • I tried to create a minimal ListView / DelegateModel drag-n-drop example for you.

    I removed the Flickable, I wasn't sure what the purpose it had, and, you already had ListView and MouseArea with dragging so I didn't really want Flickable there confusing things.

    I kept your ListModel, but, I shortened the lines of code needed to populate it.

    I rewrote your delegate based on Qt's documentation https://doc.qt.io/qt-6/qml-qtquick-drag.html which showed a parent Item of which inside it you have your DropArea and the TextField. Inside the TextField you have a MouseArea. This arrangement is important because the parent Item is invisible an immovable. When you finish your drag action the TextField you want to snap back into place, i.e. reset the 'y' position, since, we're going to move it in the visual model, we want to undo the move from the drag.

    I remove all traces on DelegateModel. Correct usage of DelegateModel involves manipulation of a DelegateModelGroup. Seeing your example really manipulates the underlying ListModel I felt that there was no real need for DelegateModel here, and, a benefit of removing it is it reduces the code complexity of the example dramatically.

    I renamed visualIndex to just index / _index. Since this is the built-in index given to all delegates. Tracking this index will ultimately give us the record in the source ListModel which will enable us to call move correctly after the drag-drop is completed.

    After that, I wired it up with a minimal implementation on MouseArea's onReleased and the corresponding DropArea onDropped which, for you pretty much had right. I changed the sourceIndex and targetIndex from being a variable to being a property since, when I was debugging I found it useful to property bind to these values so I can see them (I've removed the debugging in the final result but I've kept those as properties since it made the code look leaner).

    I found in earlier versions that the delegates the z-order was stacked so that the dragged item wasn't always on top. In order to fix that problem I needed to elevate the z-order of the delegate corresponding to the current item currently being dragged.

    Lastly, it wasn't clear which version of Qt you were using, but, I've opted to use Qt6 syntax to shorten my imports.

    import QtQuick
    import QtQuick.Controls
    import QtQuick.Layouts
    Page {
        Rectangle { anchors.fill: parent; color: "lightblue" }
        property var listOfStrings: ["Hello", "world", "I", "need", "help"]
        ListModel { id: listOfStringsModel }
        Component.onCompleted: {
            for (let textEntry of listOfStrings) listOfStringsModel.append({textEntry});
        }
        ListView {
            anchors { fill: parent; margins: 10 }
            spacing: 4 
            model: listOfStringsModel
            delegate: TextListDelegate { }
        }
    }
    
    // TextListDelegate.qml
    import QtQuick
    import QtQuick.Controls
    import QtQml.Models
    Rectangle {
        height: 32
        width: ListView.view.width
        z: dragArea.drag.active ? 1 : 0
        border.color: "grey"
        DropArea {
            id: dropArea
            property int sourceIndex: drag.source._index
            property int targetIndex: index
            anchors { fill: parent; }
            onDropped: listOfStringsModel.move(sourceIndex, targetIndex, 1);
            Rectangle {
                anchors.fill: parent
                visible: parent.containsDrag
                color: "steelblue"
                opacity: 0.5
                Label { anchors.centerIn: parent; text: dropArea.sourceIndex + " -> " + dropArea.targetIndex }
            }
        }
        Label {
            id: label
            width: parent.width
            height: parent.height
            property int _index: index
            text: textEntry
            Drag.active: dragArea.drag.active
            Drag.hotSpot.x: 10
            Drag.hotSpot.y: 10
            MouseArea {
                id: dragArea
                anchors.fill: parent
                drag.target: parent
                drag.axis: Drag.YAxis
                onReleased: { parent.Drag.drop(); parent.y = 0; }
            }
        }
    }
    

    You can Try it Online!