Skip to content
This repository was archived by the owner on Sep 26, 2022. It is now read-only.

Commit c679854

Browse files
borismelnik0x-r4bbit
authored andcommitted
feat(StatusChatList): Add drag and drop support of list items
This implements drag and drop capabilities of chat items within a `StatusChatList`. The commit introduces a `DelegateModal` to visually reorder chat items when they're being dragged and dropped onto a `DropArea`. To persist the new order of chat items, various signals have been introduced to chat list related components: ```qml StatusChatList { onChatItemReordered: function (id, from, to) { // ... } } StatusChatListAndCategories { onChatItemReordered: function (categoryId, chatId, from, to) { // ... } } ``` There's no such API on the `StatusChatListCategory` type because that one already exposes its underlying `StatusChatList` via `chatList`, which makes the signal available. Dragging and dropping chat items is disabled by default and needs to be turned on using the `draggableItems` property: ```qml StatusChatList { draggableItems: true ... } ```
1 parent c6952a8 commit c679854

File tree

5 files changed

+247
-89
lines changed

5 files changed

+247
-89
lines changed

sandbox/DemoApp.qml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ Rectangle {
467467
width: leftPanel.width
468468
height: implicitHeight > (leftPanel.height - 64) ? implicitHeight + 8 : leftPanel.height - 64
469469

470+
draggableItems: true
470471
chatList.model: models.demoCommunityChatListItems
471472
categoryList.model: models.demoCommunityCategoryItems
472473

sandbox/Models.qml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ QtObject {
1313
unreadMessagesCount: 0
1414
mentionsCount: 0
1515
color: "blue"
16+
position: 0
1617
}
1718
ListElement {
1819
chatId: "1"
@@ -22,6 +23,7 @@ QtObject {
2223
color: "red"
2324
unreadMessagesCount: 1
2425
mentionsCount: 1
26+
position: 1
2527
}
2628
ListElement {
2729
chatId: "2"
@@ -32,6 +34,7 @@ QtObject {
3234
identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAlklEQVR4nOzW0QmDQBAG4SSkl7SUQlJGCrElq9F3QdjjVhh/5nv3cFhY9vUIYQiNITSG0Bh
3335
CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2ImYgiNITTlTdG1nUZ5a92VITQxITFiJmIIjSE0htAYQrMHAAD//+wwFVpz+yqXAAAAAElFTkSuQmCC"
3436
unreadMessagesCount: 0
37+
position: 2
3538
}
3639
ListElement {
3740
chatId: "3"
@@ -40,6 +43,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
4043
muted: false
4144
color: "purple"
4245
unreadMessagesCount: 0
46+
position: 3
4347
}
4448
ListElement {
4549
chatId: "4"
@@ -48,6 +52,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
4852
muted: true
4953
color: "Orange"
5054
unreadMessagesCount: 0
55+
position: 4
5156
}
5257
ListElement {
5358
chatId: "5"
@@ -56,6 +61,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
5661
muted: false
5762
color: "green"
5863
unreadMessagesCount: 0
64+
position: 5
5965
}
6066
}
6167

@@ -68,6 +74,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
6874
muted: false
6975
unreadMessagesCount: 0
7076
color: "orange"
77+
position: 0
7178
}
7279
ListElement {
7380
chatId: "1"
@@ -77,6 +84,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
7784
unreadMessagesCount: 0
7885
color: "orange"
7986
categoryId: "public"
87+
position: 0
8088
}
8189
ListElement {
8290
chatId: "2"
@@ -86,6 +94,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
8694
unreadMessagesCount: 0
8795
color: "orange"
8896
categoryId: "public"
97+
position: 1
8998
}
9099
ListElement {
91100
chatId: "3"
@@ -95,6 +104,7 @@ CExPynn1gWf9bx498P7/nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2I
95104
unreadMessagesCount: 0
96105
color: "orange"
97106
categoryId: "dev"
107+
position: 0
98108
}
99109
}
100110

src/StatusQ/Components/StatusChatList.qml

Lines changed: 181 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import QtQuick 2.13
2+
import QtQml.Models 2.14
3+
import QtQuick.Controls 2.13 as QC
24

35
import StatusQ.Core 0.1
46
import StatusQ.Core.Theme 0.1
@@ -13,7 +15,8 @@ Column {
1315

1416
property string categoryId: ""
1517
property string selectedChatId: ""
16-
property alias chatListItems: statusChatListItems
18+
property alias chatListItems: delegateModel
19+
property bool draggableItems: false
1720

1821
property Component popupMenu
1922

@@ -23,82 +26,209 @@ Column {
2326

2427
signal chatItemSelected(string id)
2528
signal chatItemUnmuted(string id)
29+
signal chatItemReordered(string id, int from, int to)
30+
31+
function getAbsolutePosition(node) {
32+
var returnPos = {};
33+
returnPos.x = 0;
34+
returnPos.y = 0;
35+
if (node !== undefined && node !== null) {
36+
var parentValue = getAbsolutePosition(node.parent);
37+
returnPos.x = parentValue.x + node.x;
38+
returnPos.y = parentValue.y + node.y;
39+
}
40+
return returnPos;
41+
}
2642

2743
onPopupMenuChanged: {
2844
if (!!popupMenu) {
2945
popupMenuSlot.sourceComponent = popupMenu
3046
}
3147
}
3248

33-
Repeater {
34-
id: statusChatListItems
35-
delegate: StatusChatListItem {
49+
DelegateModel {
50+
id: delegateModel
3651

37-
id: statusChatListItem
52+
delegate: Item {
53+
id: draggable
54+
width: statusChatListItem.width
55+
height: statusChatListItem.height
3856

39-
property string profileImage: ""
57+
property alias chatListItem: statusChatListItem
4058

41-
Component.onCompleted: {
42-
if (typeof statusChatList.profileImageFn === "function") {
43-
profileImage = statusChatList.profileImageFn(model.chatId || model.id) || ""
59+
visible: {
60+
if (!!statusChatList.filterFn) {
61+
return statusChatList.filterFn(model, statusChatList.categoryId)
4462
}
63+
return true
4564
}
4665

47-
chatId: model.chatId || model.id
48-
name: !!statusChatList.chatNameFn ? statusChatList.chatNameFn(model) : model.name
49-
type: model.chatType
50-
muted: !!model.muted
51-
hasUnreadMessages: !!model.hasUnreadMessages || model.unviewedMessagesCount > 0
52-
hasMention: model.mentionsCount > 0
53-
badge.value: model.chatType === StatusChatListItem.Type.OneToOneChat ?
54-
model.unviewedMessagesCount || 0 :
55-
model.mentionsCount || 0
56-
selected: (model.chatId || model.id) === statusChatList.selectedChatId
57-
58-
icon.color: model.color || ""
59-
image.isIdenticon: !!!profileImage && !!!model.identityImage && !!model.identicon
60-
image.source: profileImage || model.identityImage || model.identicon || ""
61-
62-
onClicked: {
63-
if (mouse.button === Qt.RightButton && !!statusChatList.popupMenu) {
64-
statusChatListItem.highlighted = true
65-
66-
let originalOpenHandler = popupMenuSlot.item.openHandler
67-
let originalCloseHandler = popupMenuSlot.item.closeHandler
68-
69-
popupMenuSlot.item.openHandler = function () {
70-
if (!!originalOpenHandler) {
71-
originalOpenHandler((model.chatId || model.id))
72-
}
66+
MouseArea {
67+
id: dragSensor
68+
69+
anchors.fill: parent
70+
cursorShape: active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
71+
hoverEnabled: true
72+
pressAndHoldInterval: 150
73+
enabled: statusChatList.draggableItems
74+
75+
property bool active: false
76+
property real startY: 0
77+
property real startX: 0
78+
79+
drag.target: draggedListItemLoader.item
80+
drag.threshold: 0.1
81+
drag.filterChildren: true
82+
83+
onPressed: {
84+
startY = mouseY
85+
startX = mouseX
86+
}
87+
onPressAndHold: active = true
88+
onReleased: {
89+
if (active) {
90+
statusChatList.chatItemReordered(statusChatListItem.chatId, statusChatListItem.originalOrder, statusChatListItem.originalOrder)
91+
}
92+
active = false
93+
}
94+
onMouseYChanged: {
95+
if ((Math.abs(startY - mouseY) > 1) && pressed) {
96+
active = true
97+
}
98+
}
99+
onMouseXChanged: {
100+
if ((Math.abs(startX - mouseX) > 1) && pressed) {
101+
active = true
73102
}
103+
}
104+
105+
StatusChatListItem {
74106

75-
popupMenuSlot.item.closeHandler = function () {
76-
if (statusChatListItem) {
77-
statusChatListItem.highlighted = false
107+
id: statusChatListItem
108+
109+
property string profileImage: ""
110+
111+
opacity: dragSensor.active ? 0.0 : 1.0
112+
Component.onCompleted: {
113+
if (typeof statusChatList.profileImageFn === "function") {
114+
profileImage = statusChatList.profileImageFn(model.chatId || model.id) || ""
78115
}
79-
if (!!originalCloseHandler) {
80-
originalCloseHandler()
116+
}
117+
originalOrder: model.position
118+
chatId: model.chatId || model.id
119+
categoryId: model.categoryId || ""
120+
name: !!statusChatList.chatNameFn ? statusChatList.chatNameFn(model) : model.name
121+
type: model.chatType
122+
muted: !!model.muted
123+
hasUnreadMessages: !!model.hasUnreadMessages || model.unviewedMessagesCount > 0
124+
hasMention: model.mentionsCount > 0
125+
badge.value: model.chatType === StatusChatListItem.Type.OneToOneChat ?
126+
model.unviewedMessagesCount || 0 :
127+
model.mentionsCount || 0
128+
selected: (model.chatId || model.id) === statusChatList.selectedChatId
129+
130+
icon.color: model.color || ""
131+
image.isIdenticon: !!!profileImage && !!!model.identityImage && !!model.identicon
132+
image.source: profileImage || model.identityImage || model.identicon || ""
133+
134+
sensor.cursorShape: dragSensor.cursorShape
135+
onClicked: {
136+
if (mouse.button === Qt.RightButton && !!statusChatList.popupMenu) {
137+
statusChatListItem.highlighted = true
138+
139+
let originalOpenHandler = popupMenuSlot.item.openHandler
140+
let originalCloseHandler = popupMenuSlot.item.closeHandler
141+
142+
popupMenuSlot.item.openHandler = function () {
143+
if (!!originalOpenHandler) {
144+
originalOpenHandler((model.chatId || model.id))
145+
}
146+
}
147+
148+
popupMenuSlot.item.closeHandler = function () {
149+
if (statusChatListItem) {
150+
statusChatListItem.highlighted = false
151+
}
152+
if (!!originalCloseHandler) {
153+
originalCloseHandler()
154+
}
155+
}
156+
157+
popupMenuSlot.item.popup(mouse.x + 4, statusChatListItem.y + mouse.y + 6)
158+
popupMenuSlot.item.openHandler = originalOpenHandler
159+
return
160+
}
161+
if (!statusChatListItem.selected) {
162+
statusChatList.chatItemSelected(model.chatId || model.id)
81163
}
82164
}
83-
84-
popupMenuSlot.item.popup(mouse.x + 4, statusChatListItem.y + mouse.y + 6)
85-
popupMenuSlot.item.openHandler = originalOpenHandler
86-
return
165+
onUnmute: statusChatList.chatItemUnmuted(model.chatId || model.id)
87166
}
88-
if (!statusChatListItem.selected) {
89-
statusChatList.chatItemSelected(model.chatId || model.id)
167+
}
168+
169+
DropArea {
170+
id: dropArea
171+
width: dragSensor.active ? 0 : parent.width
172+
height: dragSensor.active ? 0 : parent.height
173+
keys: ["chat-item-category-" + statusChatListItem.categoryId]
174+
175+
onEntered: reorderDelay.start()
176+
onDropped: statusChatList.chatItemReordered(statusChatListItem.chatId, drag.source.originalOrder, statusChatListItem.DelegateModel.itemsIndex)
177+
178+
Timer {
179+
id: reorderDelay
180+
interval: 100
181+
repeat: false
182+
onTriggered: {
183+
if (dropArea.containsDrag) {
184+
dropArea.drag.source.chatListItem.originalOrder = statusChatListItem.originalOrder
185+
delegateModel.items.move(dropArea.drag.source.DelegateModel.itemsIndex, draggable.DelegateModel.itemsIndex)
186+
}
187+
}
90188
}
91189
}
92-
onUnmute: statusChatList.chatItemUnmuted(model.chatId || model.id)
93-
visible: {
94-
if (!!statusChatList.filterFn) {
95-
return statusChatList.filterFn(model, statusChatList.categoryId)
190+
191+
Loader {
192+
id: draggedListItemLoader
193+
active: dragSensor.active
194+
sourceComponent: StatusChatListItem {
195+
property var globalPosition: statusChatList.getAbsolutePosition(draggable)
196+
parent: QC.Overlay.overlay
197+
sensor.cursorShape: dragSensor.cursorShape
198+
Drag.active: dragSensor.active
199+
Drag.hotSpot.x: width / 2
200+
Drag.hotSpot.y: height / 2
201+
Drag.keys: ["chat-item-category-" + categoryId]
202+
Drag.source: draggable
203+
204+
Component.onCompleted: {
205+
x = globalPosition.x
206+
y = globalPosition.y
207+
}
208+
chatId: draggable.chatListItem.chatId
209+
categoryId: draggable.chatListItem.categoryId
210+
name: draggable.chatListItem.name
211+
type: draggable.chatListItem.type
212+
muted: draggable.chatListItem.muted
213+
dragged: true
214+
hasUnreadMessages: draggable.chatListItem.hasUnreadMessages
215+
hasMention: draggable.chatListItem.hasMention
216+
badge.value: draggable.chatListItem.badge.value
217+
selected: draggable.chatListItem.selected
218+
219+
icon.color: draggable.chatListItem.icon.color
220+
image.isIdenticon: draggable.chatListItem.image.isIdenticon
221+
image.source: draggable.chatListItem.image.source
96222
}
97-
return true
98223
}
99224
}
100225
}
101226

227+
Repeater {
228+
id: statusChatListItems
229+
model: delegateModel
230+
}
231+
102232
Loader {
103233
id: popupMenuSlot
104234
active: !!statusChatList.popupMenu

0 commit comments

Comments
 (0)