feat(StatusChatList): adding drag n drop functionality

Closes #3364
This commit is contained in:
Alexandra Betouni 2023-03-20 17:07:16 +02:00 committed by Jonathan Rainville
parent 812c0e9705
commit f507e33cba
7 changed files with 244 additions and 277 deletions

View File

@ -29,7 +29,12 @@ class CommunityScreenComponents(Enum):
CHAT_LOG = "chatView_log"
COMMUNITY_HEADER_BUTTON = "mainWindow_communityHeader_StatusChatInfoButton"
COMMUNITY_HEADER_NAME_TEXT= "community_ChatInfo_Name_Text"
COMMUNITY_CREATE_CHANNEL_OR_CAT_BUTTON = "mainWindow_createChannelOrCategoryBtn_StatusBaseText"
COMMUNITY_CREATE_CHANNEL_OR_CAT_BUTTON = "ma.
inWindow_createChannelOrCategoryBtn_StatusBaseText"
COMMUNITY_CREATE_CHANNEL_MENU_ITEM = "create_channel_StatusMenuItem"
COMMUNITY_CREATE_CATEGORY_MENU_ITEM = "create_category_StatusMenuItem"
COMMUNITY_EDIT_CATEGORY_MENU_ITEM = "edit_сategory_StatusMenuItem"
@ -114,7 +119,7 @@ class StatusCommunityScreen:
chat_or_cat_loader = chat_and_category_list.itemAtIndex(i)
if not chat_or_cat_loader or chat_or_cat_loader.item.objectName != "categoryItem":
continue
if str(chat_or_cat_loader.item.title).lower() == community_category_name.lower():
if str(chat_or_cat_loader.item.text).lower() == community_category_name.lower():
return True, chat_or_cat_loader.item
return False, None
@ -436,13 +441,13 @@ class StatusCommunityScreen:
found = False
verify(chat_lists.count > 0, "At least one chat exists")
for i in range(chat_lists.count):
chat = chat_lists.itemAtIndex(i)
chat_list_items = get_children_with_object_name(chat, "chatItem")
verify(len(chat_list_items) > 0, "StatusChatListItem exists")
if str(chat_list_items[0].name) == chatName:
right_click_obj(chat)
found = True
break
draggable_item = chat_lists.itemAtIndex(i)
chat = draggable_item.item
if chat != None:
if chat.text == chatName:
right_click_obj(draggable_item)
found = True
break
if not found:
test.fail("Chat is not loaded")

View File

@ -28,7 +28,8 @@ Item {
signal chatItemSelected(string categoryId, string id)
signal chatItemUnmuted(string id)
signal chatItemReordered(string id, int from, int to)
signal categoryReordered(string categoryId, int to)
signal chatItemReordered(string categoryId, string chatId, int to)
signal categoryAddButtonClicked(string id)
StatusListView {
@ -40,238 +41,170 @@ Item {
spacing: 0
interactive: height !== contentHeight
delegate: Loader {
id: chatLoader
delegate: DropArea {
id: chatListDelegate
objectName: model.name
width: model.isCategory ? statusChatListCategoryItem.width : statusChatListItem.width
height: model.isCategory ? statusChatListCategoryItem.height : statusChatListItem.height
keys: ["x-status-draggable-chat-list-item-and-categories"]
sourceComponent: {
if (model.isCategory) {
return categoryItemComponent
}
return channelItemComponent
property int visualIndex: index
property string chatId: model.itemId
property string categoryId: model.categoryId
property string isCategory: model.isCategory
property Item item: isCategory ? draggableItem.actions[0] : draggableItem.actions[1]
onEntered: function(drag) {
drag.accept();
statusChatListCategoryItem.highlighted = true;
statusChatListItem.highlighted = true;
}
Component {
id: categoryItemComponent
StatusChatListCategoryItem {
id: statusChatListCategoryItem
objectName: "categoryItem"
function setupPopup() {
categoryPopupMenuSlot.item.categoryItem = model
onExited: {
statusChatListCategoryItem.highlighted = false;
statusChatListItem.highlighted = false;
}
onDropped: function(drop) {
const from = drop.source.visualIndex;
const to = chatListDelegate.visualIndex;
if (to === from)
return;
if (!model.isCategory) {
root.chatItemReordered(statusChatListItems.itemAtIndex(from).categoryId, statusChatListItems.itemAtIndex(from).chatId, to);
} else {
root.categoryReordered(statusChatListItems.itemAtIndex(from).categoryId, to);
}
}
StatusDraggableListItem {
id: draggableItem
width: parent.width
height: visible ? implicitHeight : 0
dragParent: root.draggableItems ? statusChatListItems : null
visualIndex: chatListDelegate.visualIndex
draggable: (root.draggableItems && (statusChatListItems.count > 1))
horizontalPadding: 0
verticalPadding: 0
icon.width: 0
icon.height: 0
spacing: 0
topInset: 0
bottomInset: 0
customizable: true
Drag.keys: chatListDelegate.keys
onClicked: {
if (model.isCategory) {
statusChatListCategoryItem.clicked(mouse);
} else {
statusChatListItem.clicked(mouse);
}
}
Connections {
enabled: categoryPopupMenuSlot.active && statusChatListCategoryItem.highlighted
target: categoryPopupMenuSlot.item
function onClosed() {
statusChatListCategoryItem.highlighted = false
statusChatListCategoryItem.menuButton.highlighted = false
actions: [
StatusChatListCategoryItem {
id: statusChatListCategoryItem
objectName: "categoryItem"
visible: model.isCategory
function setupPopup() {
categoryPopupMenuSlot.item.categoryItem = model
}
}
title: model.name
opened: model.categoryOpened
sensor.pressAndHoldInterval: 150
propagateTitleClicks: true // title click is handled as a normal click (fallthru)
showAddButton: showCategoryActionButtons
showMenuButton: !!root.onPopupMenuChanged
highlighted: false//statusChatListCategory.dragged // FIXME DND
hasUnreadMessages: model.hasUnreadMessages
onClicked: {
if (!sensor.enabled) {
return
Connections {
enabled: categoryPopupMenuSlot.active && statusChatListCategoryItem.highlighted
target: categoryPopupMenuSlot.item
function onClosed() {
statusChatListCategoryItem.highlighted = false
statusChatListCategoryItem.menuButton.highlighted = false
}
}
if (mouse.button === Qt.RightButton && showCategoryActionButtons && !!root.categoryPopupMenu) {
text: model.name
opened: model.categoryOpened
highlighted: draggableItem.dragActive
showAddButton: showCategoryActionButtons
showMenuButton: !!root.onPopupMenuChanged
hasUnreadMessages: model.hasUnreadMessages
onClicked: {
if (mouse.button === Qt.RightButton && showCategoryActionButtons && !!root.categoryPopupMenu) {
statusChatListCategoryItem.setupPopup()
highlighted = true;
categoryPopupMenuSlot.item.popup()
} else if (mouse.button === Qt.LeftButton) {
root.model.sourceModel.changeCategoryOpened(model.categoryId, !statusChatListCategoryItem.opened)
}
}
onToggleButtonClicked: root.model.sourceModel.changeCategoryOpened(model.categoryId, !statusChatListCategoryItem.opened)
onMenuButtonClicked: {
statusChatListCategoryItem.setupPopup()
highlighted = true;
highlighted = true
menuButton.highlighted = true
categoryPopupMenuSlot.item.popup()
} else if (mouse.button === Qt.LeftButton) {
root.model.sourceModel.changeCategoryOpened(model.categoryId, !statusChatListCategoryItem.opened)
}
}
onToggleButtonClicked: root.model.sourceModel.changeCategoryOpened(model.categoryId, !statusChatListCategoryItem.opened)
onMenuButtonClicked: {
statusChatListCategoryItem.setupPopup()
highlighted = true
menuButton.highlighted = true
categoryPopupMenuSlot.item.popup()
}
onAddButtonClicked: {
root.categoryAddButtonClicked(categoryId)
}
}
}
Component {
id: channelItemComponent
QC.Control {
id: draggable
objectName: model.name
width: root.width
height: model.categoryOpened ? statusChatListItem.height + 4 /*spacing between non-collapsed items*/ : 0
visible: height
verticalPadding: 2
property alias chatListItem: statusChatListItem
contentItem: MouseArea {
id: dragSensor
anchors.fill: parent
cursorShape: active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
hoverEnabled: true
enabled: root.draggableItems
property bool active: false
property real startY: 0
property real startX: 0
drag.target: draggedListItemLoader.item
drag.filterChildren: true
onPressed: {
startY = mouseY
startX = mouseX
onAddButtonClicked: {
root.categoryAddButtonClicked(categoryId)
}
onPressAndHold: active = true
onReleased: {
if (active && d.destinationPosition !== -1 && statusChatListItem.originalOrder !== d.destinationPosition) {
root.chatItemReordered(statusChatListItem.chatId, statusChatListItem.originalOrder, d.destinationPosition)
}
active = false
}
onMouseYChanged: {
if ((Math.abs(startY - mouseY) > 1) && pressed) {
active = true
}
}
onMouseXChanged: {
if ((Math.abs(startX - mouseX) > 1) && pressed) {
active = true
}
}
onActiveChanged: d.destinationPosition = -1
},
StatusChatListItem {
id: statusChatListItem
objectName: model.name
width: root.width
height: visible ? (statusChatListItem.implicitHeight + 4) /*spacing between non-collapsed items*/ : 0
visible: (!model.isCategory && model.categoryOpened)
originalOrder: model.position
chatId: model.itemId
categoryId: model.categoryId
name: model.name
type: model.type ?? StatusChatListItem.Type.CommunityChat
muted: model.muted
hasUnreadMessages: model.hasUnreadMessages
notificationsCount: model.notificationsCount
highlightWhenCreated: !!model.highlight
selected: (model.active && root.highlightItem)
asset.emoji: !!model.emoji ? model.emoji : ""
asset.color: !!model.color ? model.color : Theme.palette.userCustomizationColors[model.colorId]
asset.isImage: model.icon.includes("data")
asset.name: model.icon
ringSettings.ringSpecModel: type === StatusChatListItem.Type.OneToOneChat && root.isEnsVerified(chatId) ? undefined : model.colorHash
onlineStatus: !!model.onlineStatus ? model.onlineStatus : StatusChatListItem.OnlineStatus.Inactive
sensor.enabled: draggableItem.dragActive
dragged: draggableItem.dragActive
onClicked: {
highlightWhenCreated = false
StatusChatListItem {
id: statusChatListItem
if (mouse.button === Qt.RightButton && !!root.popupMenu) {
statusChatListItem.highlighted = true
width: parent.width
opacity: dragSensor.active ? 0.0 : 1.0
originalOrder: model.position
chatId: model.itemId
categoryId: model.categoryId
name: model.name
type: model.type ?? StatusChatListItem.Type.CommunityChat
muted: model.muted
hasUnreadMessages: model.hasUnreadMessages
notificationsCount: model.notificationsCount
highlightWhenCreated: !!model.highlight
selected: (model.active && root.highlightItem)
asset.emoji: !!model.emoji ? model.emoji : ""
asset.color: !!model.color ? model.color : Theme.palette.userCustomizationColors[model.colorId]
asset.isImage: model.icon.includes("data")
asset.name: model.icon
ringSettings.ringSpecModel: type === StatusChatListItem.Type.OneToOneChat && root.isEnsVerified(chatId) ? undefined : model.colorHash
onlineStatus: !!model.onlineStatus ? model.onlineStatus : StatusChatListItem.OnlineStatus.Inactive
const originalOpenHandler = popupMenuSlot.item.openHandler
const originalCloseHandler = popupMenuSlot.item.closeHandler
sensor.cursorShape: dragSensor.cursorShape
onClicked: {
highlightWhenCreated = false
if (mouse.button === Qt.RightButton && !!root.popupMenu) {
statusChatListItem.highlighted = true
const originalOpenHandler = popupMenuSlot.item.openHandler
const originalCloseHandler = popupMenuSlot.item.closeHandler
popupMenuSlot.item.openHandler = function () {
if (!!originalOpenHandler) {
originalOpenHandler(statusChatListItem.chatId)
}
popupMenuSlot.item.openHandler = function () {
if (!!originalOpenHandler) {
originalOpenHandler(statusChatListItem.chatId)
}
}
popupMenuSlot.item.closeHandler = function () {
if (statusChatListItem) {
statusChatListItem.highlighted = false
}
if (!!originalCloseHandler) {
originalCloseHandler()
}
popupMenuSlot.item.closeHandler = function () {
if (statusChatListItem) {
statusChatListItem.highlighted = false
}
if (!!originalCloseHandler) {
originalCloseHandler()
}
const p = statusChatListItem.mapToItem(root, mouse.x, mouse.y)
popupMenuSlot.item.popup(p.x + 4, p.y + 6)
popupMenuSlot.item.openHandler = originalOpenHandler
return
}
if (!statusChatListItem.selected) {
root.chatItemSelected(statusChatListItem.categoryId, statusChatListItem.chatId)
}
const p = statusChatListItem.mapToItem(root, mouse.x, mouse.y)
popupMenuSlot.item.popup(p.x + 4, p.y + 6)
popupMenuSlot.item.openHandler = originalOpenHandler
return
}
onUnmute: root.chatItemUnmuted(statusChatListItem.chatId)
}
}
DropArea {
id: dropArea
width: dragSensor.active ? 0 : parent.width
height: dragSensor.active ? 0 : parent.height
keys: ["chat-item-category-" + statusChatListItem.categoryId]
onEntered: reorderDelay.start()
Timer {
id: reorderDelay
interval: 100
repeat: false
onTriggered: {
if (dropArea.containsDrag) {
d.destinationPosition = index;
}
if (!statusChatListItem.selected) {
root.chatItemSelected(statusChatListItem.categoryId, statusChatListItem.chatId)
}
}
onUnmute: root.chatItemUnmuted(statusChatListItem.chatId)
}
Loader {
id: draggedListItemLoader
active: dragSensor.active
sourceComponent: StatusChatListItem {
property var globalPosition: Utils.getAbsolutePosition(draggable)
parent: QC.Overlay.overlay
sensor.cursorShape: dragSensor.cursorShape
Drag.active: dragSensor.active
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: ["chat-item-category-" + categoryId]
Drag.source: draggable
chatId: draggable.chatListItem.chatId
categoryId: draggable.chatListItem.categoryId
name: draggable.chatListItem.name
type: draggable.chatListItem.type
muted: draggable.chatListItem.muted
dragged: true
hasUnreadMessages: model.hasUnreadMessages
notificationsCount: model.notificationsCount
selected: draggable.chatListItem.selected
asset.color: draggable.chatListItem.asset.color
asset.imgIsIdenticon: draggable.chatListItem.asset.imgIsIdenticon
asset.name: draggable.chatListItem.asset.name
Component.onCompleted: {
x = globalPosition.x
y = globalPosition.y
}
}
}
}
]
}
}
}
@ -290,7 +223,6 @@ Item {
QtObject {
id: d
property int destinationPosition: -1
}

View File

@ -37,8 +37,8 @@ Item {
signal chatItemSelected(string categoryId, string id)
signal chatItemUnmuted(string id)
signal chatItemReordered(string categoryId, string chatId, int from, int to)
signal chatListCategoryReordered(string categoryId, int from, int to)
signal chatItemReordered(string categoryId, string chatId, int to)
signal chatListCategoryReordered(string categoryId, int to)
signal categoryAddButtonClicked(string id)
onPopupMenuChanged: {
@ -73,7 +73,10 @@ Item {
visible: statusChatList.model.count > 0
onChatItemSelected: root.chatItemSelected(categoryId, id)
onChatItemUnmuted: root.chatItemUnmuted(id)
onChatItemReordered: root.chatItemReordered(categoryId, id, from, to)
onChatItemReordered: {
root.chatItemReordered(categoryId, chatId, to)
}
onCategoryReordered: root.chatListCategoryReordered(categoryId, to)
draggableItems: root.draggableItems
showCategoryActionButtons: root.showCategoryActionButtons
onCategoryAddButtonClicked: {

View File

@ -1,19 +1,21 @@
import QtQuick 2.13
import QtQuick.Controls 2.12
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
StatusListItem {
id: statusChatListCategoryItem
Control {
id: root
implicitWidth: 288
implicitHeight: 28
implicitWidth: visible ? 288 : 0
implicitHeight: visible ? 28 : 0
leftPadding: 8
rightPadding: 8
property string text
property bool opened: true
property bool highlighted: false
property bool showActionButtons: false
@ -24,45 +26,57 @@ StatusListItem {
property alias menuButton: menuButton
property alias toggleButton: toggleButton
signal clicked(var mouse)
signal addButtonClicked(var mouse)
signal menuButtonClicked(var mouse)
signal toggleButtonClicked(var mouse)
color: sensor.containsMouse || highlighted ? Theme.palette.baseColor2 : "transparent"
statusListItemTitle.color: Theme.palette.directColor4
statusListItemTitle.font.weight: hasUnreadMessages ? Font.Bold : Font.Medium
statusListItemComponentsSlot.spacing: 1
components: [
StatusChatListCategoryItemButton {
id: addButton
icon.name: "add"
icon.width: 20
visible: statusChatListCategoryItem.showAddButton &&
(statusChatListCategoryItem.highlighted ||
statusChatListCategoryItem.sensor.containsMouse)
onClicked: statusChatListCategoryItem.addButtonClicked(mouse)
tooltip.text: qsTr("Add channel inside category")
},
StatusChatListCategoryItemButton {
id: menuButton
icon.name: "more"
icon.width: 21
visible: statusChatListCategoryItem.showMenuButton &&
(statusChatListCategoryItem.highlighted ||
statusChatListCategoryItem.sensor.containsMouse)
onClicked: statusChatListCategoryItem.menuButtonClicked(mouse)
tooltip.text: qsTr("More")
},
StatusChatListCategoryItemButton {
id: toggleButton
icon.name: "chevron-down"
icon.width: 18
icon.rotation: statusChatListCategoryItem.opened ? 0 : 270
onClicked: statusChatListCategoryItem.toggleButtonClicked(mouse)
background: Rectangle {
HoverHandler {
id: hoverHandler
}
]
color: (hoverHandler.hovered || root.highlighted) ? Theme.palette.baseColor2 : "transparent"
radius: 8
}
contentItem: Item {
StatusBaseText {
width: Math.min(implicitWidth, parent.width)
anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium
font.pixelSize: 15
elide: Text.ElideRight
color: Theme.palette.directColor4
text: root.text
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 1
StatusChatListCategoryItemButton {
id: addButton
icon.name: "add"
icon.width: 20
visible: (root.showAddButton && (hoverHandler.hovered || root.highlighted))
onClicked: root.addButtonClicked(mouse)
tooltip.text: qsTr("Add channel inside category")
}
StatusChatListCategoryItemButton {
id: menuButton
icon.name: "more"
icon.width: 21
visible: (root.showMenuButton && (hoverHandler.hovered || root.highlighted))
onClicked: root.menuButtonClicked(mouse)
tooltip.text: qsTr("More")
}
StatusChatListCategoryItemButton {
id: toggleButton
icon.name: "chevron-down"
icon.width: 18
icon.rotation: root.opened ? 0 : 270
onClicked: root.toggleButtonClicked(mouse)
}
}
}
}

View File

@ -144,6 +144,16 @@ ItemDelegate {
This property holds whether this item can be dragged (and whether the drag handle is displayed)
*/
property bool draggable
/*!
\qmlproperty bool StatusDraggableListItem::customizable
This property holds whether this item can be customized
*/
property bool customizable: false
/*!
\qmlsignal
This signal is emitted when the StatusDraggableListItem is clicked.
*/
signal clicked(var mouse)
/*!
\qmlproperty int StatusDraggableListItem::dragAxis
@ -219,19 +229,23 @@ ItemDelegate {
]
background: Rectangle {
color: root.dragActive ? Theme.palette.indirectColor2 : "transparent"
border.width: 1
color: root.dragActive && !root.customizable ? Theme.palette.indirectColor2 : "transparent"
border.width: root.customizable ? 0 : 1
border.color: Theme.palette.baseColor2
radius: 8
radius: customizable ? 0 : 8
MouseArea {
id: dragHandler
anchors.fill: parent
drag.target: root
drag.target: root.draggable ? root : null
drag.axis: root.dragAxis
preventStealing: true // otherwise DND is broken inside a Flickable/ScrollView
hoverEnabled: true
cursorShape: root.dragActive ? Qt.ClosedHandCursor : Qt.OpenHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.clicked(mouse);
}
}
}
@ -253,7 +267,7 @@ ItemDelegate {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
icon: "justify"
visible: root.draggable
visible: root.draggable && !root.customizable
}
Loader {

View File

@ -216,5 +216,6 @@
<file>StatusQ/Controls/StatusLinkText.qml</file>
<file>StatusQ/Core/Utils/ModelChangeGuard.qml</file>
<file>StatusQ/Core/Utils/StackViewStates.qml</file>
<file>StatusQ/Components/StatusDraggableListItem.qml</file>
</qresource>
</RCC>

View File

@ -38,15 +38,13 @@ Item {
signal infoButtonClicked
signal manageButtonClicked
MouseArea {
TapHandler {
enabled: communityData.amISectionAdmin
anchors.fill: parent
z: 0
acceptedButtons: Qt.RightButton
onClicked: {
onTapped: {
adminPopupMenu.showInviteButton = true
adminPopupMenu.x = mouse.x + 4
adminPopupMenu.y = mouse.y + 4
adminPopupMenu.x = eventPoint.position.x + 4
adminPopupMenu.y = eventPoint.position.y + 4
adminPopupMenu.open()
}
}
@ -219,8 +217,8 @@ Item {
showPopupMenu: communityData.amISectionAdmin && communityData.canManageUsers
onChatItemUnmuted: root.communitySectionModule.unmuteChat(id)
onChatItemReordered: function(categoryId, chatId, from, to){
root.store.reorderCommunityChat(categoryId, chatId, to)
onChatItemReordered: function(categoryId, chatId, to) {
root.store.reorderCommunityChat(categoryId, chatId, to);
}
onChatListCategoryReordered: root.store.reorderCommunityCategories(categoryId, to)