mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
homescreens/halcyon: Add ability to create folders by dragging
This commit is contained in:
parent
d3054b1983
commit
51f558b4a3
5 changed files with 281 additions and 97 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue