From 51f558b4a3d4bba78e4322979a510bcca28daf74 Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Sat, 9 Jul 2022 12:05:58 -0400 Subject: [PATCH] homescreens/halcyon: Add ability to create folders by dragging --- .../contents/ui/FavoritesAppDelegate.qml | 129 ++++++++++------ .../package/contents/ui/FavoritesGrid.qml | 141 ++++++++++++++++-- .../package/contents/ui/FolderGrid.qml | 31 ++-- .../homescreens/halcyon/pinnedmodel.cpp | 70 +++++++-- .../homescreens/halcyon/pinnedmodel.h | 7 +- 5 files changed, 281 insertions(+), 97 deletions(-) diff --git a/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml b/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml index ea71a88c..756681c7 100644 --- a/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml +++ b/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml @@ -24,6 +24,8 @@ Item { property real leftPadding property real rightPadding + property real dragFolderAnimationProgress: 0 + // whether this delegate is a folder property bool isFolder @@ -45,6 +47,18 @@ Item { Drag.hotSpot.x: delegate.width / 2 Drag.hotSpot.y: delegate.height / 2 + // close context menu if drag move + onXChanged: { + if (dialogLoader.item) { + dialogLoader.item.close() + } + } + onYChanged: { + if (dialogLoader.item) { + dialogLoader.item.close() + } + } + function openContextMenu() { dialogLoader.active = true; dialogLoader.item.open(); @@ -88,7 +102,7 @@ Item { icon.name: "emblem-favorite" text: i18n("Remove from favourites") onClicked: { - Halcyon.PinnedModel.removeApp(model.index); + Halcyon.PinnedModel.removeEntry(model.index); } } onClosed: dialogLoader.active = false @@ -107,7 +121,7 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: (mouse.button === Qt.RightButton) ? openContextMenu() : launch(); onReleased: { - delegate.parent.Drag.drop(); + delegate.Drag.drop(); inDrag = false; } onPressAndHold: { inDrag = true; openContextMenu() } @@ -198,28 +212,47 @@ Item { Component { id: appIconComponent - PlasmaCore.IconItem { - usesPlasmaTheme: false - source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon - + Item { Rectangle { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - } - visible: application ? application.running : false - radius: width - width: PlasmaCore.Units.smallSpacing - height: width - color: PlasmaCore.Theme.highlightColor + anchors.fill: parent + anchors.margins: PlasmaCore.Units.smallSpacing + color: Qt.rgba(255, 255, 255, 0.2) + radius: PlasmaCore.Units.smallSpacing + opacity: delegate.dragFolderAnimationProgress } - layer.enabled: true - layer.effect: DropShadow { - verticalOffset: 1 - radius: 4 - samples: 6 - color: Qt.rgba(0, 0, 0, 0.5) + PlasmaCore.IconItem { + id: icon + anchors.fill: parent + usesPlasmaTheme: false + source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon + + transform: Scale { + origin.x: icon.width / 2 + origin.y: icon.height / 2 + xScale: 1 - delegate.dragFolderAnimationProgress * 0.5 + yScale: 1 - delegate.dragFolderAnimationProgress * 0.5 + } + + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + } + visible: application ? application.running : false + radius: width + width: PlasmaCore.Units.smallSpacing + height: width + color: PlasmaCore.Theme.highlightColor + } + + layer.enabled: true + layer.effect: DropShadow { + verticalOffset: 1 + radius: 4 + samples: 6 + color: Qt.rgba(0, 0, 0, 0.5) + } } } } @@ -229,34 +262,42 @@ Item { Item { Rectangle { + id: rect anchors.fill: parent anchors.margins: PlasmaCore.Units.smallSpacing color: Qt.rgba(255, 255, 255, 0.2) radius: PlasmaCore.Units.smallSpacing - Grid { - id: grid - anchors.fill: parent - anchors.margins: PlasmaCore.Units.smallSpacing - columns: 2 - spacing: PlasmaCore.Units.smallSpacing - - property var previews: model.folder.appPreviews - - Repeater { - model: grid.previews - delegate: Kirigami.Icon { - implicitWidth: (grid.width - PlasmaCore.Units.smallSpacing) / 2 - implicitHeight: (grid.width - PlasmaCore.Units.smallSpacing) / 2 - source: modelData.icon - - layer.enabled: true - layer.effect: DropShadow { - verticalOffset: 1 - radius: 4 - samples: 3 - color: Qt.rgba(0, 0, 0, 0.5) - } + transform: Scale { + origin.x: rect.width / 2 + origin.y: rect.height / 2 + xScale: 1 + delegate.dragFolderAnimationProgress * 0.5 + yScale: 1 + delegate.dragFolderAnimationProgress * 0.5 + } + } + + Grid { + id: grid + anchors.fill: parent + anchors.margins: PlasmaCore.Units.smallSpacing * 2 + columns: 2 + spacing: PlasmaCore.Units.smallSpacing + + property var previews: model.folder.appPreviews + + Repeater { + model: grid.previews + delegate: Kirigami.Icon { + implicitWidth: (grid.width - PlasmaCore.Units.smallSpacing) / 2 + implicitHeight: (grid.width - PlasmaCore.Units.smallSpacing) / 2 + source: modelData.icon + + layer.enabled: true + layer.effect: DropShadow { + verticalOffset: 1 + radius: 4 + samples: 3 + color: Qt.rgba(0, 0, 0, 0.5) } } } diff --git a/containments/homescreens/halcyon/package/contents/ui/FavoritesGrid.qml b/containments/homescreens/halcyon/package/contents/ui/FavoritesGrid.qml index 4fdf5496..f4b204fc 100644 --- a/containments/homescreens/halcyon/package/contents/ui/FavoritesGrid.qml +++ b/containments/homescreens/halcyon/package/contents/ui/FavoritesGrid.qml @@ -20,17 +20,18 @@ MobileShell.GridView { id: root required property var searchWidget - signal openConfigureRequested() - signal requestOpenFolder(Halcyon.ApplicationFolder folder) - // don't set anchors.margins since we want everywhere to be draggable required property real leftMargin required property real rightMargin required property bool twoColumn - + + signal openConfigureRequested() + signal requestOpenFolder(Halcyon.ApplicationFolder folder) + // search widget open gesture property bool openingSearchWidget: false property real oldVerticalOvershoot: verticalOvershoot + onVerticalOvershootChanged: { if (dragging && verticalOvershoot < 0) { if (!openingSearchWidget) { @@ -75,27 +76,134 @@ MobileShell.GridView { id: visualModel model: Halcyon.PinnedModel - delegate: DropArea { + delegate: Item { id: delegateRoot - property int modelIndex property int visualIndex: DelegateModel.itemsIndex width: root.cellWidth height: root.cellHeight - onEntered: (drag) => { - let from = (drag.source as MobileShell.BaseItem).visualIndex; - let to = appDelegate.visualIndex; - visualModel.items.move(from, to); - Halcyon.PinnedModel.moveEntry(from, to); + function moveDragToCurrentPos(from, to) { + if (from !== to) { + console.log(from + ' ' + to) + visualModel.items.move(from, to); + Halcyon.PinnedModel.moveEntry(from, to); + } } - //onDropped: (drag) => { - //let from = modelIndex; - //let to = (drag.source as MobileShell.BaseItem).visualIndex - //Halcyon.PinnedModel.moveEntry(from, to); - //} + function topDragEnter(drag) { + if (transitionAnim.running || appDelegate.drag.active) return; // don't do anything when reordering + + let fromIndex = drag.source.visualIndex; + let delegateVisualIndex = appDelegate.visualIndex; + let reorderIndex = -1; + + if (fromIndex < delegateVisualIndex) { // dragged item from above + // move to spot above + reorderIndex = delegateVisualIndex - (root.twoColumn ? 2 : 1); + } else { // dragged item from below + // move to current spot + reorderIndex = delegateVisualIndex; + } + + if (reorderIndex >= 0 && reorderIndex < root.count) { + delegateRoot.moveDragToCurrentPos(fromIndex, reorderIndex) + } + } + function bottomDragEnter(drag) { + if (transitionAnim.running || appDelegate.drag.active) return; // don't do anything when reordering + + let fromIndex = drag.source.visualIndex; + let delegateVisualIndex = appDelegate.visualIndex; + let reorderIndex = -1; + + if (fromIndex < delegateVisualIndex) { // dragged item from above + // move to current spot + reorderIndex = delegateVisualIndex; + } else { // dragged item from below + // move to spot below + reorderIndex = delegateVisualIndex + (root.twoColumn ? 2 : 1); + } + + if (reorderIndex >= 0 && reorderIndex < root.count) { + delegateRoot.moveDragToCurrentPos(fromIndex, reorderIndex); + } + } + + // top drop area + DropArea { + id: topDropArea + anchors.top: parent.top + anchors.left: leftDropArea.right + anchors.right: rightDropArea.left + height: delegateRoot.height * 0.2 + onEntered: (drag) => delegateRoot.topDragEnter(drag) + } + + // bottom drop area + DropArea { + id: bottomDropArea + anchors.bottom: parent.bottom + anchors.left: leftDropArea.right + anchors.right: rightDropArea.left + height: delegateRoot.height * 0.2 + onEntered: (drag) => delegateRoot.bottomDragEnter(drag) + } + + // left drop area + DropArea { + id: leftDropArea + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.left: parent.left + width: root.twoColumn ? Math.max(appDelegate.leftPadding, delegateRoot.width * 0.1) : 0 + onEntered: (drag) => delegateRoot.topDragEnter(drag) + } + + // right drop area + DropArea { + id: rightDropArea + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.right: parent.right + width: root.twoColumn ? Math.max(appDelegate.rightPadding, delegateRoot.width * 0.1) : 0 + onEntered: (drag) => delegateRoot.bottomDragEnter(drag) + } + + // folder drop area + DropArea { + anchors.top: topDropArea.bottom + anchors.bottom: bottomDropArea.top + anchors.left: leftDropArea.right + anchors.right: rightDropArea.left + onEntered: (drag) => { + if (transitionAnim.running || appDelegate.drag.active) return; // don't do anything when reordering + folderAnim.to = 1; + folderAnim.restart(); + } + onExited: () => { + folderAnim.to = 0; + folderAnim.restart(); + } + onDropped: (drop) => { + if (transitionAnim.running || appDelegate.drag.active) return; // don't do anything when reordering + if (appDelegate.isFolder) { + Halcyon.PinnedModel.addAppToFolder(drop.source.visualIndex, appDelegate.visualIndex); + } else { + Halcyon.PinnedModel.createFolderFromApps(drop.source.visualIndex, appDelegate.visualIndex); + } + } + + NumberAnimation { + id: folderAnim + target: appDelegate + properties: "dragFolderAnimationProgress" + duration: 100 + } + } + + // actual visual delegate FavoritesAppDelegate { id: appDelegate visualIndex: delegateRoot.visualIndex @@ -139,6 +247,7 @@ MobileShell.GridView { // animations displaced: Transition { NumberAnimation { + id: transitionAnim properties: "x,y" easing.type: Easing.OutQuad } diff --git a/containments/homescreens/halcyon/package/contents/ui/FolderGrid.qml b/containments/homescreens/halcyon/package/contents/ui/FolderGrid.qml index 1ea4201e..4a74b84d 100644 --- a/containments/homescreens/halcyon/package/contents/ui/FolderGrid.qml +++ b/containments/homescreens/halcyon/package/contents/ui/FolderGrid.qml @@ -98,28 +98,23 @@ MobileShell.GridView { id: visualModel model: root.folderModel - delegate: DropArea { - id: delegateRoot - property var application: model.application - - property int modelIndex - property int visualIndex: DelegateModel.itemsIndex - + delegate: Item { + id: delegateRoot width: root.cellWidth height: root.cellHeight - onEntered: (drag) => { - let from = (drag.source as MobileShell.BaseItem).visualIndex; - let to = appDelegate.visualIndex; - visualModel.items.move(from, to); - root.folder.moveEntry(from, to); - } + property var application: model.application + property int visualIndex: DelegateModel.itemsIndex - //onDropped: (drag) => { - //let from = modelIndex; - //let to = (drag.source as MobileShell.BaseItem).visualIndex - //Halcyon.PinnedModel.moveEntry(from, to); - //} + DropArea { + anchors.fill: parent + onEntered: (drag) => { + let from = drag.source.visualIndex; + let to = appDelegate.visualIndex; + visualModel.items.move(from, to); + root.folder.moveEntry(from, to); + } + } FavoritesAppDelegate { id: appDelegate diff --git a/containments/homescreens/halcyon/pinnedmodel.cpp b/containments/homescreens/halcyon/pinnedmodel.cpp index b799b2cb..01198d22 100644 --- a/containments/homescreens/halcyon/pinnedmodel.cpp +++ b/containments/homescreens/halcyon/pinnedmodel.cpp @@ -6,6 +6,8 @@ #include #include +#include + PinnedModel::PinnedModel(QObject *parent, Plasma::Applet *applet) : QAbstractListModel{parent} , m_applet{applet} @@ -60,21 +62,6 @@ void PinnedModel::addApp(const QString &storageId, int row) } } -void PinnedModel::removeApp(int row) -{ - if (row < 0 || row >= m_applications.size()) { - return; - } - - beginRemoveRows(QModelIndex(), row, row); - m_applications[row]->deleteLater(); - m_applications.removeAt(row); - m_folders.removeAt(row); // maintain indicies - endRemoveRows(); - - save(); -} - void PinnedModel::addFolder(QString name, int row) { if (row < 0 || row > m_applications.size()) { @@ -92,13 +79,19 @@ void PinnedModel::addFolder(QString name, int row) save(); } -void PinnedModel::removeFolder(int row) +void PinnedModel::removeEntry(int row) { if (row < 0 || row >= m_applications.size()) { return; } beginRemoveRows(QModelIndex(), row, row); + if (m_folders[row]) { + m_folders[row]->deleteLater(); + } + if (m_applications[row]) { + m_applications[row]->deleteLater(); + } m_applications.removeAt(row); m_folders.removeAt(row); endRemoveRows(); @@ -140,6 +133,51 @@ void PinnedModel::moveEntry(int fromRow, int toRow) m_applet->config().sync(); } +void PinnedModel::createFolderFromApps(int sourceAppRow, int draggedAppRow) +{ + if (sourceAppRow < 0 || sourceAppRow >= m_applications.size() || draggedAppRow < 0 || draggedAppRow >= m_applications.size()) { + return; + } + + if (sourceAppRow == draggedAppRow || !m_applications[sourceAppRow] || !m_applications[draggedAppRow]) { + return; + } + + // replace source app with folder containing both apps + ApplicationFolder *folder = new ApplicationFolder(this, i18nc("Default application folder name.", "Folder")); + connect(folder, &ApplicationFolder::saveRequested, this, &PinnedModel::save); + + folder->addApp(m_applications[sourceAppRow]->storageId(), 0); + folder->addApp(m_applications[draggedAppRow]->storageId(), 0); + + m_applications[sourceAppRow]->deleteLater(); + m_applications[sourceAppRow] = nullptr; + m_folders[sourceAppRow] = folder; + + Q_EMIT dataChanged(index(sourceAppRow, 0), index(sourceAppRow, 0), {IsFolderRole, ApplicationRole, FolderRole}); + save(); + + // remove dragged app after + removeEntry(draggedAppRow); +} + +void PinnedModel::addAppToFolder(int appRow, int folderRow) +{ + if (appRow < 0 || appRow >= m_applications.size() || folderRow < 0 || folderRow >= m_applications.size()) { + return; + } + + if (!m_applications[appRow] || !m_folders[folderRow]) { + return; + } + + ApplicationFolder *folder = m_folders[folderRow]; + Application *app = m_applications[appRow]; + folder->addApp(app->storageId(), folder->applications().count()); + + removeEntry(appRow); +} + void PinnedModel::load() { if (!m_applet) { diff --git a/containments/homescreens/halcyon/pinnedmodel.h b/containments/homescreens/halcyon/pinnedmodel.h index e415f7bc..f9e7adec 100644 --- a/containments/homescreens/halcyon/pinnedmodel.h +++ b/containments/homescreens/halcyon/pinnedmodel.h @@ -37,12 +37,13 @@ public: QHash roleNames() const override; Q_INVOKABLE void addApp(const QString &storageId, int row); - Q_INVOKABLE void removeApp(int row); Q_INVOKABLE void addFolder(QString name, int row); - Q_INVOKABLE void removeFolder(int row); - + Q_INVOKABLE void removeEntry(int row); Q_INVOKABLE void moveEntry(int fromRow, int toRow); + Q_INVOKABLE void createFolderFromApps(int sourceAppRow, int draggedAppRow); + Q_INVOKABLE void addAppToFolder(int appRow, int folderRow); + Q_INVOKABLE void load(); void save();