Commit 51f558b4 authored by Devin Lin's avatar Devin Lin 🎨
Browse files

homescreens/halcyon: Add ability to create folders by dragging

parent d3054b19
Pipeline #201871 passed with stage
in 1 minute and 15 seconds
......@@ -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)
}
}
}
......
......@@ -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);
}
}
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)
}
}
//onDropped: (drag) => {
//let from = modelIndex;
//let to = (drag.source as MobileShell.BaseItem).visualIndex
//Halcyon.PinnedModel.moveEntry(from, to);
//}
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
}
......
......@@ -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
......
......@@ -6,6 +6,8 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <KLocalizedString>
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) {
......
......@@ -37,12 +37,13 @@ public:
QHash<int, QByteArray> 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();
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment