Drag and dropping identical QML components on top of each other - qt

I have a repeater that generates a bunch of rectangles. I want to be able to drag these rectangles around and generate an event when I drop one rectangle on another (think files and folders on an OS. I can drag folders & files, but I can drop a file on a folder and it stores it inside). I've tried the following but the dropArea gets confused because the object itself is draggable. OnDropped just never triggers. Is this possible?
Repeater {
model: 5
Item {
id: element
Item {
id: surfaceContainer
width: 150
height: 150
x: index * width
DropArea {
id: dropArea
anchors.fill: surfaceContainer
onContainsDragChanged: {
if (containsDrag && drag.source != draggableRectangle)
console.log("CHANGED")
}
onEntered: {
if (drag.source != draggableRectangle)
console.log("ENTERED")
}
onDropped: {
console.log("DROPPED")
}
onExited: {
if (drag.source != draggableRectangle && containsDrag)
drag.source.color = "yellow"
}
}
Rectangle {
id: draggableRectangle
x: width / 2
color: "blue"
height: 100
width: 100
Drag.active: dragArea.drag.active
MouseArea {
id: dragArea
anchors.fill: draggableRectangle
drag.target: draggableRectangle
onDoubleClicked: {
connectionId.visible = !connectionId.visible
}
}
}
}
}
}

[EDIT]
I made the following changes to your application:
Initialize the DropArea to match the Rectangle geometry
Everytime the Rectangle has completed dragging, to re-update the DropArea to match the Rectangle geometry
When an Rectangle is being dragged, only enable the DropArea that are valid destinations, i.e. it can only be dragged to a DropArea of a different delegate
import QtQuick
import QtQuick.Controls
Page {
property var sourceItem: null
property var destItem: null
property string statusText: ""
Repeater {
model: [ "A", "B", "C", "D", "E" ]
Item {
z: dragArea.drag.active ? 2 : 0
DropArea {
id: dropArea
x: index * 150 + 100
y: 100
width: 100
height: 100
enabled: sourceItem && sourceItem !== modelData
onContainsDragChanged: {
if (containsDrag) {
destItem = modelData;
} else {
destItem = null;
}
}
}
Rectangle {
id: dragRect
x: index * 150 + 100
y: 100
width: 100
height: 100
color: dropArea.containsDrag ? "green" : dragArea.drag.active ? "#c00" : "#f88"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
Label {
anchors.centerIn: parent
text: modelData
}
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
drag.onActiveChanged: {
if (drag.active) {
sourceItem = modelData;
} else {
if (sourceItem && destItem) {
statusText = "Dragged " + JSON.stringify(sourceItem) + " to " + JSON.stringify(destItem);
} else {
statusText = "";
}
sourceItem = null;
dropArea.x = dragRect.x;
dropArea.y = dragRect.y;
}
}
}
}
}
}
Frame {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 3 / 4
visible: statusText
Text {
text: statusText
}
}
}
You can Try it Online!

Related

QML List View Drag and Drop

I would like to create two qml list views that can perform two functions:
Drag and drop items within one list to change order of items
Drag and drop items cross lists. Items will remove from one list and add to another
Based on the Drag and Drop example in Qt Documentation, I decided to create two list views that accessing the same list model. Each item in the list model has a key as a bool value. Using DelegateModel with a filter, I can display items with key equals to true in one list and with false in the second list. When dragging an item from first list to second list, the key value will be flipped.
Now, I am facing two problems:
onDropped is not executed
DelegateModel is not refreshed after key value changed
Can anybody help me solve these problems or maybe have a better solution?
import QtQuick 2.5
import QtQuick.Window 2.0
import QtQml.Models 2.2
import QtQuick.Controls 2.2
Window {
visible: true
width: 300
height: 400
title: qsTr("List View Test")
property bool dropAreaOneEntered: false
property bool dropAreaTwoEntered: false
property int listNumer: 0
Rectangle {
id: root
width: parent.width
height: parent.height
ListView {
id: displayListOne
width: 100; height: parent.height
model: displayListOneModel
property int dragItemIndex: -1
moveDisplaced: Transition {
NumberAnimation{
properties: "x,y"
duration: 200
}
}
}
ListView {
id: displayListTwo
width: 100; height: parent.height
anchors.right: parent.right
model: displayListTwoModel
property int dragItemIndex: -1
moveDisplaced: Transition {
NumberAnimation{
properties: "x,y"
duration: 200
}
}
}
// ..........................................................
ListModel {
id: itemsListModel
// #disable-check M16
ListElement { colore: "blue"; key: true }
// #disable-check M16
ListElement { colore: "orange"; key: false }
// #disable-check M16
ListElement { colore: "blue"; key: true }
// #disable-check M16
ListElement { colore: "orange"; key: false }
// #disable-check M16
ListElement { colore: "blue"; key: true }
// #disable-check M16
ListElement { colore: "orange"; key: false }
// #disable-check M16
ListElement { colore: "blue"; key: true }
// #disable-check M16
ListElement { colore: "orange"; key: false }
}
// ..........................................................
DelegateModel {
id: displayListOneModel
model: itemsListModel
delegate: MouseArea {
id: mouseOneArea
width: 100; height: 100
property bool held: false
drag.target: held ? itemOneBlock : undefined
drag.axis: Drag.XAndYAxis
property var draggedItem: null
onPressAndHold: {
held = true
listNumer = 1
displayListOne.dragItemIndex = index
}
onReleased: {
listNumer = 0
held = false
}
Rectangle {
id: itemOneBlock
anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
width: 100
height: 100
color: colore
opacity: mouseOneArea.held ? 0.8 : 1.0
Drag.active: mouseOneArea.held
Drag.source: mouseOneArea
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
states: State{
when: mouseOneArea.held
ParentChange { target: itemOneBlock; parent: root }
AnchorChanges {
target: itemOneBlock
anchors { horizontalCenter: undefined; verticalCenter: undefined }
}
}
}
DropArea {
id: listOneDropArea
anchors.fill: parent
property bool flag: false
onExited: dropAreaOneEntered = false
onEntered: {
dropAreaOneEntered = true
if (dropAreaOneEntered & ! dropAreaTwoEntered){
if (listNumer == 1){
displayListOneModel.items.move(drag.source.DelegateModel.itemsIndex,
mouseOneArea.DelegateModel.itemsIndex)
}
}
}
onDropped: {
console.log("onDropped has been executed!")
}
}
}
groups: [
DelegateModelGroup {
includeByDefault: true
name: "listOne"
}
]
filterOnGroup: "listOne"
Component.onCompleted: {
var rowCount = itemsListModel.count;
for( var i = 0; i < items.count; i++ ) {
var entry = items.get(i).model;
if((entry.key != true)) {
items.removeGroups(i,1,"listOne")
}
}
}
}
DelegateModel {
id: displayListTwoModel
model: itemsListModel
delegate: MouseArea {
id: mouseTwoArea
width: 100; height: 100
property bool held: false
drag.target: held ? itemTwoBlock : undefined
drag.axis: Drag.XAndYAxis
onPressAndHold: {
held = true
listNumer = 2
displayListTwo.dragItemIndex = index
console.log("current index is "+index)
}
onReleased: {
listNumer = 0
held = false
}
Rectangle {
id: itemTwoBlock
anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
width: 100
height: 100
color: colore
opacity: mouseTwoArea.held ? 0.8 : 1.0
Drag.active: mouseTwoArea.held
Drag.source: mouseTwoArea
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
states: State{
when: mouseTwoArea.held
ParentChange { target: itemTwoBlock; parent: root }
AnchorChanges {
target: itemTwoBlock
anchors { horizontalCenter: undefined; verticalCenter: undefined }
}
}
}
DropArea {
id: listTwoDropArea
anchors.fill: parent
onExited: dropAreaTwoEntered = false
onEntered: {
dropAreaTwoEntered = true
if (dropAreaTwoEntered & ! dropAreaOneEntered){
if (listNumer == 2){
displayListTwoModel.items.move(drag.source.DelegateModel.itemsIndex,
mouseTwoArea.DelegateModel.itemsIndex)
}
}
}
onDropped: {
console.log("Flip the key value here!")
}
}
}
groups: [
DelegateModelGroup {
includeByDefault: true
name: "listTwo"
}
]
filterOnGroup: "listTwo"
Component.onCompleted: {
var rowCount = itemsListModel.count;
for( var i = 0; i < items.count; i++ ) {
var entry = items.get(i).model;
if((entry.key != false)) {
items.removeGroups(i,1,"listTwo")
}
}
}
}
}
}

How to retrieve the new ordered list of ListModel inside ListView after drag and drop event

I have this simple qml code
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import QtQml.Models 2.2
ApplicationWindow {
visible: true
width: 300
height: 120
title: qsTr("Hello World")
Rectangle {
anchors.fill: parent;
ListView{
id: timeline
anchors.fill: parent
orientation: ListView.Horizontal
model: visualModel
delegate: timelineDelegate
moveDisplaced: Transition {
NumberAnimation{
properties: "x,y"
duration: 200
}
}
DelegateModel {
id: visualModel
model: timelineModel
delegate: timelineDelegate
}
Component {
id: timelineDelegate
MouseArea {
id: dragArea
width: 100; height: 100
property bool held: false
drag.target: held ? content : undefined
drag.axis: Drag.XAxis
onPressAndHold: held = true
onReleased: {
held = false
var listOnModel = "{";
for(var i = 0; i < timelineModel.count; i++){
listOnModel += timelineModel.get(i).colore + ", "
}
console.log("List: " + listOnModel + "}");
}
Rectangle {
id: content
anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
width: 100
height: 100
color: colore
opacity: dragArea.held ? 0.8 : 1.0
Text{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: index
font.pixelSize: 20
}
Drag.active: dragArea.held
Drag.source: dragArea
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
states: State{
when: dragArea.held
ParentChange { target: content; parent: timeline }
AnchorChanges {
target: content
anchors { horizontalCenter: undefined; verticalCenter: undefined }
}
}
}
DropArea {
anchors.fill: parent
onEntered: {
visualModel.items.move( drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
}
}
}
}
ListModel {
id: timelineModel
// #disable-check M16
ListElement { colore: "blue" }
// #disable-check M16
ListElement { colore: "orange" }
// #disable-check M16
ListElement { colore: "green" }
}
}
}
}
Here we have a simple list of colored draggable rectangles. In the center of every rectangle is shown the actual index, that this component has inside the model.
As you can see, after the drop event, the index for every item doesn't change, and the order of the items inside the model is still the same. Is there a way to retrieve the new order of the list after a drag and drop event occurred?
You don't reorder the ListModel, but the items of your DelegateModel.
So you need to use this code instead:
onReleased: {
held = false
var listOnModel = "{";
for(var i = 0; i < visualModel.items.count; i++){
listOnModel += visualModel.items.get(i).model.colore + ", "
}
console.log("List: " + listOnModel + "}");
}

QML: How to reject drop action

I have one DropArea and two elements. I want DropArea reject the drop event if the DropArea already got one element be dropped, the another element not allow drop into, unless the first one move out.
DropArea {
property bool dropped: false
onDropped: {
drop.accepted = !dropped;
dropped = true;
}
onExited: dropped = false
}
But looks like drop.accepted not work,
BTW anyway to get the objects was dropped in DropArea
You should control if the item must be dropped or not in onReleased, checking the dropped property.
Full example:
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window {
id: win
visible: true
width: 800
height: 600
title: qsTr("Hello World")
Repeater {
model: 10
Rectangle {
id: rect
width: 50
height: 50
z: mouseArea.drag.active || mouseArea.pressed ? 2 : 1
color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
x: Math.random() * (win.width / 2 - 100)
y: Math.random() * (win.height - 100)
property point beginDrag
property bool caught: false
border { width:2; color: "white" }
radius: 5
Drag.active: mouseArea.drag.active
Text {
anchors.centerIn: parent
text: index
color: "white"
}
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: parent
onPressed: {
rect.beginDrag = Qt.point(rect.x, rect.y);
}
onReleased: {
if(!rect.caught || dragTarget.dropped) {
backAnimX.from = rect.x;
backAnimX.to = beginDrag.x;
backAnimY.from = rect.y;
backAnimY.to = beginDrag.y;
backAnim.start()
}
parent.Drag.drop()
console.log("MouseArea - containsDrag " + dragTarget.dropped)
}
}
ParallelAnimation {
id: backAnim
SpringAnimation { id: backAnimX; target: rect; property: "x";
duration: 500; spring: 2; damping: 0.2 }
SpringAnimation { id: backAnimY; target: rect; property: "y";
duration: 500; spring: 2; damping: 0.2 }
}
}
}
Rectangle {
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
}
width: parent.width / 2
color: "gold"
DropArea {
id: dragTarget
anchors.fill: parent
property bool dropped: false
onEntered: {
console.log("onEntered " + containsDrag)
drag.source.caught = true;
}
onExited: {
console.log("onExited " + containsDrag)
dropped = false;
}
onDropped:
{
console.log("onDropped");
dropped = true;
}
}
}
}
Use drop.accept() instead. The above can be done as follows:
property bool containsItem: false
DropArea {
id: dropArea
anchors.fill: parent
onDropped: {
if(containsItem)
drop.accept(Qt.IgnoreAction)
else
drop.accept()
containsItem = true;
}
}
Also donot use dropped property as it is already an attached property inside onDropped event handler.
Edit:
If rect is the Item to be dragged and dropped then:
Rectangle {
id: rect
width: 40; height: 40
color: "red"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 20
Drag.hotSpot.y: 20
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
onReleased: if (rect.Drag.drop() !== Qt.IgnoreAction) {
console.log("Accepted!");
} else {
console.log("Rejected!");
}
}
}

How to access the property of a GridView - or ListView - element

Here is the code, I create 4 buttons. When one is clicked I wanna that its color changes to red and the color of all the others change to black.
But looks like I could not access the color property.
Rectangle {
id: root
width: 200; height: 100
DelegateModel {
id: visualModel
model: ListModel {
ListElement { my_color: "red" }
ListElement { my_color: "black" }
ListElement { my_color: "black" }
ListElement { my_color: "black" }
}
groups: [
DelegateModelGroup { name: "selected" }
]
delegate: Rectangle {
id: item
height: 25
width: 200
color:my_color
MouseArea {
anchors.fill: parent
onClicked: {
console.log(visualModel.items.get(index).color)
for (var i = 0; i < root.count; i++){
if(index == i)
visualModel.items.get(i).color = "red";
else
visualModel.items.get(i).color = "black";
}
}
}
}
}
ListView {
anchors.fill: parent
model: visualModel
}
}
I advice you to use ExclusiveGroup from QML controls. Usually it is used for Action but it's possible to use it for any other Item. From the Qt docs:
It is possible to add support for ExclusiveGroup for an object or
control. It should have a checked property, and either a
checkedChanged, toggled(), or toggled(bool) signal.
So all we need is to add suitable property. Small example:
import QtQuick 2.5
import QtQuick.Window 2.0
import QtQuick.Controls 1.4
Window {
width: 200
height: 400
ExclusiveGroup { id: exclusiveGroup }
ListView {
anchors.fill: parent
anchors.margins: 5
spacing: 2
model: 10
delegate: Rectangle {
id: myItem
property bool checked: false // <-- this is necessary
height: 30
width: parent.width
color: myItem.checked ? "lightblue" : "#DEDEDE"
border { width: 1; color: "#999" }
radius: 5
Text { text: "item" + (index + 1); anchors.centerIn: parent}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: myItem.checked = !myItem.checked;
}
Component.onCompleted: {
exclusiveGroup.bindCheckable(myItem);
}
}
}
}

How to move/animate components created by repeater in QML?

I have a component named Tile in Tile.qml, which I want to create by a Repeater. Tile.qml is as follows:
import QtQuick 2.0
Rectangle {
id: tile
property string tileLabel: label.text
property int tileSize: height
width: 50
height: tileSize
color: "green"
border.color: Qt.lighter(color)
anchors.bottom: parent.bottom
Text {
id: label
color: "white";
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
text: tileLabel
}
}
And my main.qml is as follows:
import QtQuick 2.0
Rectangle {
id: root
width: 552; height: 300
color: "#3C3C3C"
border.color: Qt.lighter(color)
Row {
id: tilesRow
anchors.margins: 8
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Repeater {
id: repeater
model: 10
delegate: Tile {
tileSize: Math.random() * 100 + 1
tileLabel: tileSize
}
}
}
Rectangle {
id: button
width: 100
height: 30
color: "gray"
focus: true
Text {
anchors.centerIn: parent
color: "white"
text: "Button"
}
MouseArea {
hoverEnabled: true
anchors.fill: parent
onEntered: { button.color = Qt.lighter("blue")}
onExited: { button.color = "gray" }
onPressed: { button.color = "blue" }
onReleased: { button.color = Qt.lighter("blue") }
onClicked: func()
}
}
}
I need to sort the tiles when the button is clicked so that the tiles are in ascending order by their labels. I can access the labels of the tiles using repeater.itemAt(i).tileSize. How can I animate the movement of tiles as they are moved/swapped?
Small example:
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
visible: true
width: 800
height: 600
Row {
anchors.centerIn: parent
property var word: ['H','e','l','l','o','!']
id: row
Repeater {
id: repeater
model: row.word.length
delegate: Rectangle {
id: delegate;
width: 100
height: 100
property int pos
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1);
Text {
anchors.centerIn: parent
font.pixelSize: 36
color: "white"
text: row.word[index]
}
Behavior on x {
ParallelAnimation {
PropertyAnimation {
duration: 500
easing.type: Easing.InOutBack
}
SequentialAnimation {
PropertyAnimation {
target: delegate
property: "y"
from: 0
to: delegate.pos == 1 ? 20 : -20
duration: 250
}
PropertyAnimation {
target: delegate
property: "y"
from: delegate.pos == 1 ? 20 : -20
to: 0
duration: 250
}
}
}
}
Behavior on rotation {
RotationAnimation {
direction: RotationAnimation.Clockwise
duration: 300
}
}
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
var element1 = repeater.itemAt(Math.round(Math.random() * (repeater.count - 1)));
var element2 = repeater.itemAt(Math.round(Math.random() * (repeater.count - 1)));
if(element1 === element2) {
element1.rotation = element1.rotation + 90;
} else {
element1.pos = 1;
element2.pos = 2;
var temp = element1.x;
element1.x = element2.x;
element2.x = temp;
}
}
}
}

Resources