homescreens/halcyon: Add ability to create folders by dragging

This commit is contained in:
Devin Lin 2022-07-09 12:05:58 -04:00
parent d3054b1983
commit 51f558b4a3
5 changed files with 281 additions and 97 deletions

View file

@ -24,6 +24,8 @@ Item {
property real leftPadding property real leftPadding
property real rightPadding property real rightPadding
property real dragFolderAnimationProgress: 0
// whether this delegate is a folder // whether this delegate is a folder
property bool isFolder property bool isFolder
@ -45,6 +47,18 @@ Item {
Drag.hotSpot.x: delegate.width / 2 Drag.hotSpot.x: delegate.width / 2
Drag.hotSpot.y: delegate.height / 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() { function openContextMenu() {
dialogLoader.active = true; dialogLoader.active = true;
dialogLoader.item.open(); dialogLoader.item.open();
@ -88,7 +102,7 @@ Item {
icon.name: "emblem-favorite" icon.name: "emblem-favorite"
text: i18n("Remove from favourites") text: i18n("Remove from favourites")
onClicked: { onClicked: {
Halcyon.PinnedModel.removeApp(model.index); Halcyon.PinnedModel.removeEntry(model.index);
} }
} }
onClosed: dialogLoader.active = false onClosed: dialogLoader.active = false
@ -107,7 +121,7 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse.button === Qt.RightButton) ? openContextMenu() : launch(); onClicked: (mouse.button === Qt.RightButton) ? openContextMenu() : launch();
onReleased: { onReleased: {
delegate.parent.Drag.drop(); delegate.Drag.drop();
inDrag = false; inDrag = false;
} }
onPressAndHold: { inDrag = true; openContextMenu() } onPressAndHold: { inDrag = true; openContextMenu() }
@ -198,28 +212,47 @@ Item {
Component { Component {
id: appIconComponent id: appIconComponent
PlasmaCore.IconItem { Item {
usesPlasmaTheme: false
source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon
Rectangle { Rectangle {
anchors { anchors.fill: parent
horizontalCenter: parent.horizontalCenter anchors.margins: PlasmaCore.Units.smallSpacing
bottom: parent.bottom color: Qt.rgba(255, 255, 255, 0.2)
} radius: PlasmaCore.Units.smallSpacing
visible: application ? application.running : false opacity: delegate.dragFolderAnimationProgress
radius: width
width: PlasmaCore.Units.smallSpacing
height: width
color: PlasmaCore.Theme.highlightColor
} }
layer.enabled: true PlasmaCore.IconItem {
layer.effect: DropShadow { id: icon
verticalOffset: 1 anchors.fill: parent
radius: 4 usesPlasmaTheme: false
samples: 6 source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon
color: Qt.rgba(0, 0, 0, 0.5)
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 { Item {
Rectangle { Rectangle {
id: rect
anchors.fill: parent anchors.fill: parent
anchors.margins: PlasmaCore.Units.smallSpacing anchors.margins: PlasmaCore.Units.smallSpacing
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
radius: PlasmaCore.Units.smallSpacing radius: PlasmaCore.Units.smallSpacing
Grid { transform: Scale {
id: grid origin.x: rect.width / 2
anchors.fill: parent origin.y: rect.height / 2
anchors.margins: PlasmaCore.Units.smallSpacing xScale: 1 + delegate.dragFolderAnimationProgress * 0.5
columns: 2 yScale: 1 + delegate.dragFolderAnimationProgress * 0.5
spacing: PlasmaCore.Units.smallSpacing }
}
property var previews: model.folder.appPreviews
Grid {
Repeater { id: grid
model: grid.previews anchors.fill: parent
delegate: Kirigami.Icon { anchors.margins: PlasmaCore.Units.smallSpacing * 2
implicitWidth: (grid.width - PlasmaCore.Units.smallSpacing) / 2 columns: 2
implicitHeight: (grid.width - PlasmaCore.Units.smallSpacing) / 2 spacing: PlasmaCore.Units.smallSpacing
source: modelData.icon
property var previews: model.folder.appPreviews
layer.enabled: true
layer.effect: DropShadow { Repeater {
verticalOffset: 1 model: grid.previews
radius: 4 delegate: Kirigami.Icon {
samples: 3 implicitWidth: (grid.width - PlasmaCore.Units.smallSpacing) / 2
color: Qt.rgba(0, 0, 0, 0.5) 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)
} }
} }
} }

View file

@ -20,17 +20,18 @@ MobileShell.GridView {
id: root id: root
required property var searchWidget required property var searchWidget
signal openConfigureRequested()
signal requestOpenFolder(Halcyon.ApplicationFolder folder)
// don't set anchors.margins since we want everywhere to be draggable // don't set anchors.margins since we want everywhere to be draggable
required property real leftMargin required property real leftMargin
required property real rightMargin required property real rightMargin
required property bool twoColumn required property bool twoColumn
signal openConfigureRequested()
signal requestOpenFolder(Halcyon.ApplicationFolder folder)
// search widget open gesture // search widget open gesture
property bool openingSearchWidget: false property bool openingSearchWidget: false
property real oldVerticalOvershoot: verticalOvershoot property real oldVerticalOvershoot: verticalOvershoot
onVerticalOvershootChanged: { onVerticalOvershootChanged: {
if (dragging && verticalOvershoot < 0) { if (dragging && verticalOvershoot < 0) {
if (!openingSearchWidget) { if (!openingSearchWidget) {
@ -75,27 +76,134 @@ MobileShell.GridView {
id: visualModel id: visualModel
model: Halcyon.PinnedModel model: Halcyon.PinnedModel
delegate: DropArea { delegate: Item {
id: delegateRoot id: delegateRoot
property int modelIndex
property int visualIndex: DelegateModel.itemsIndex property int visualIndex: DelegateModel.itemsIndex
width: root.cellWidth width: root.cellWidth
height: root.cellHeight height: root.cellHeight
onEntered: (drag) => { function moveDragToCurrentPos(from, to) {
let from = (drag.source as MobileShell.BaseItem).visualIndex; if (from !== to) {
let to = appDelegate.visualIndex; console.log(from + ' ' + to)
visualModel.items.move(from, to); visualModel.items.move(from, to);
Halcyon.PinnedModel.moveEntry(from, to); Halcyon.PinnedModel.moveEntry(from, to);
}
} }
//onDropped: (drag) => { function topDragEnter(drag) {
//let from = modelIndex; if (transitionAnim.running || appDelegate.drag.active) return; // don't do anything when reordering
//let to = (drag.source as MobileShell.BaseItem).visualIndex
//Halcyon.PinnedModel.moveEntry(from, to); 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 { FavoritesAppDelegate {
id: appDelegate id: appDelegate
visualIndex: delegateRoot.visualIndex visualIndex: delegateRoot.visualIndex
@ -139,6 +247,7 @@ MobileShell.GridView {
// animations // animations
displaced: Transition { displaced: Transition {
NumberAnimation { NumberAnimation {
id: transitionAnim
properties: "x,y" properties: "x,y"
easing.type: Easing.OutQuad easing.type: Easing.OutQuad
} }

View file

@ -98,28 +98,23 @@ MobileShell.GridView {
id: visualModel id: visualModel
model: root.folderModel model: root.folderModel
delegate: DropArea { delegate: Item {
id: delegateRoot id: delegateRoot
property var application: model.application
property int modelIndex
property int visualIndex: DelegateModel.itemsIndex
width: root.cellWidth width: root.cellWidth
height: root.cellHeight height: root.cellHeight
onEntered: (drag) => { property var application: model.application
let from = (drag.source as MobileShell.BaseItem).visualIndex; property int visualIndex: DelegateModel.itemsIndex
let to = appDelegate.visualIndex;
visualModel.items.move(from, to);
root.folder.moveEntry(from, to);
}
//onDropped: (drag) => { DropArea {
//let from = modelIndex; anchors.fill: parent
//let to = (drag.source as MobileShell.BaseItem).visualIndex onEntered: (drag) => {
//Halcyon.PinnedModel.moveEntry(from, to); let from = drag.source.visualIndex;
//} let to = appDelegate.visualIndex;
visualModel.items.move(from, to);
root.folder.moveEntry(from, to);
}
}
FavoritesAppDelegate { FavoritesAppDelegate {
id: appDelegate id: appDelegate

View file

@ -6,6 +6,8 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <KLocalizedString>
PinnedModel::PinnedModel(QObject *parent, Plasma::Applet *applet) PinnedModel::PinnedModel(QObject *parent, Plasma::Applet *applet)
: QAbstractListModel{parent} : QAbstractListModel{parent}
, m_applet{applet} , 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) void PinnedModel::addFolder(QString name, int row)
{ {
if (row < 0 || row > m_applications.size()) { if (row < 0 || row > m_applications.size()) {
@ -92,13 +79,19 @@ void PinnedModel::addFolder(QString name, int row)
save(); save();
} }
void PinnedModel::removeFolder(int row) void PinnedModel::removeEntry(int row)
{ {
if (row < 0 || row >= m_applications.size()) { if (row < 0 || row >= m_applications.size()) {
return; return;
} }
beginRemoveRows(QModelIndex(), row, row); 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_applications.removeAt(row);
m_folders.removeAt(row); m_folders.removeAt(row);
endRemoveRows(); endRemoveRows();
@ -140,6 +133,51 @@ void PinnedModel::moveEntry(int fromRow, int toRow)
m_applet->config().sync(); 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() void PinnedModel::load()
{ {
if (!m_applet) { if (!m_applet) {

View file

@ -37,12 +37,13 @@ public:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addApp(const QString &storageId, int row); 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 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 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(); Q_INVOKABLE void load();
void save(); void save();