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:
DelegateModel
was used in this Qt tutorial which is my most successful version.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.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!