folio: Refactor and implement support for proper drag & drop

This refactors the homescreen state object to isolate drag & drop from
swipe states, allowing for using proper system-level drag & drop for delegate
movement. This then ports the new applet list to use it.
This commit is contained in:
Devin Lin 2025-12-12 00:33:25 -05:00
parent 1250180c45
commit 4c76f55b5c
16 changed files with 901 additions and 787 deletions

View file

@ -9,6 +9,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
qml/AppDrawerGrid.qml qml/AppDrawerGrid.qml
qml/AppDrawerHeader.qml qml/AppDrawerHeader.qml
qml/DelegateDragItem.qml qml/DelegateDragItem.qml
qml/DelegateDropArea.qml
qml/FavouritesBar.qml qml/FavouritesBar.qml
qml/FolderView.qml qml/FolderView.qml
qml/FolderViewTitle.qml qml/FolderViewTitle.qml
@ -65,6 +66,7 @@ ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
) )
ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
qml/settings/AppletListDelegate.qml
qml/settings/AppletListViewer.qml qml/settings/AppletListViewer.qml
qml/settings/SettingsButton.qml qml/settings/SettingsButton.qml
qml/settings/SettingsComponent.qml qml/settings/SettingsComponent.qml

View file

@ -175,12 +175,9 @@ DragState::DragState(HomeScreenState *state, HomeScreen *parent)
connect(m_state, &HomeScreenState::delegateDragFromFavouritesStarted, this, &DragState::onDelegateDragFromFavouritesStarted); connect(m_state, &HomeScreenState::delegateDragFromFavouritesStarted, this, &DragState::onDelegateDragFromFavouritesStarted);
connect(m_state, &HomeScreenState::delegateDragFromFolderStarted, this, &DragState::onDelegateDragFromFolderStarted); connect(m_state, &HomeScreenState::delegateDragFromFolderStarted, this, &DragState::onDelegateDragFromFolderStarted);
connect(m_state, &HomeScreenState::delegateDragFromWidgetListStarted, this, &DragState::onDelegateDragFromWidgetListStarted); connect(m_state, &HomeScreenState::delegateDragFromWidgetListStarted, this, &DragState::onDelegateDragFromWidgetListStarted);
connect(m_state, &HomeScreenState::swipeStateChanged, this, [this]() { connect(m_state, &HomeScreenState::delegateDragStarted, this, &DragState::onDelegateDraggingStarted);
if (m_state->swipeState() == HomeScreenState::DraggingDelegate) { connect(m_state, &HomeScreenState::delegateDragDropped, this, &DragState::onDelegateDropped);
onDelegateDraggingStarted(); connect(m_state, &HomeScreenState::delegateDragCancelled, this, &DragState::onDelegateDraggingCancelled);
}
});
connect(m_state, &HomeScreenState::delegateDragEnded, this, &DragState::onDelegateDropped);
connect(m_state, &HomeScreenState::pageNumChanged, this, [this]() { connect(m_state, &HomeScreenState::pageNumChanged, this, [this]() {
m_candidateDropPosition->setPageRow(m_state->currentPage()); m_candidateDropPosition->setPageRow(m_state->currentPage());
@ -546,19 +543,8 @@ void DragState::onDelegateDropped()
// add dropped delegate // add dropped delegate
bool success = createDropPositionDelegate(); bool success = createDropPositionDelegate();
// delete empty pages at the end if they exist // Cleanup timers and state
// (it can be created if user drags app to new page, but doesn't place it there) dragStopCleanup();
m_homeScreen->pageListModel()->deleteEmptyPagesAtEnd();
// clear ghost position if there is one
m_homeScreen->favouritesModel()->deleteGhostEntry();
// reset timers
m_folderInsertBetweenTimer->stop();
m_changeFolderPageTimer->stop();
m_leaveFolderTimer->stop();
m_changePageTimer->stop();
m_favouritesInsertBetweenTimer->stop();
// emit corresponding signal // emit corresponding signal
// -> if we couldn't drop a new delegate at a spot, emit newDelegateDropAbandoned() // -> if we couldn't drop a new delegate at a spot, emit newDelegateDropAbandoned()
@ -570,6 +556,41 @@ void DragState::onDelegateDropped()
} }
} }
void DragState::onDelegateDraggingCancelled()
{
if (!m_dropDelegate) {
return;
}
// Cleanup timers and state
dragStopCleanup();
if (m_startPosition->location() == DelegateDragPosition::WidgetList || m_startPosition->location() == DelegateDragPosition::AppDrawer) {
// If this is a new delegate, it's abandoned
Q_EMIT newDelegateDropAbandoned();
} else {
// If it's an existing delegate, it simply goes back to its original position
Q_EMIT delegateDroppedAndPlaced();
}
}
void DragState::dragStopCleanup()
{
// Delete empty pages at the end if they exist
// (it can be created if user drags app to new page, but doesn't place it there)
m_homeScreen->pageListModel()->deleteEmptyPagesAtEnd();
// Clear ghost position if there is one
m_homeScreen->favouritesModel()->deleteGhostEntry();
// Reset timers
m_folderInsertBetweenTimer->stop();
m_changeFolderPageTimer->stop();
m_leaveFolderTimer->stop();
m_changePageTimer->stop();
m_favouritesInsertBetweenTimer->stop();
}
void DragState::onLeaveCurrentFolder() void DragState::onLeaveCurrentFolder()
{ {
if (!m_state) { if (!m_state) {
@ -589,7 +610,7 @@ void DragState::onLeaveCurrentFolder()
void DragState::onChangePageTimerFinished() void DragState::onChangePageTimerFinished()
{ {
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate)) { if (!m_state || !m_state->isDraggingDelegate()) {
return; return;
} }
@ -624,7 +645,7 @@ void DragState::onChangePageTimerFinished()
void DragState::onOpenFolderTimerFinished() void DragState::onOpenFolderTimerFinished()
{ {
if (!m_state || m_state->swipeState() != HomeScreenState::DraggingDelegate || m_state->viewState() != HomeScreenState::PageView if (!m_state || !m_state->isDraggingDelegate() || m_state->viewState() != HomeScreenState::PageView
|| (m_candidateDropPosition->location() != DelegateDragPosition::Pages && m_candidateDropPosition->location() != DelegateDragPosition::Favourites)) { || (m_candidateDropPosition->location() != DelegateDragPosition::Pages && m_candidateDropPosition->location() != DelegateDragPosition::Favourites)) {
return; return;
} }
@ -672,7 +693,7 @@ void DragState::onOpenFolderTimerFinished()
void DragState::onLeaveFolderTimerFinished() void DragState::onLeaveFolderTimerFinished()
{ {
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) { if (!m_state || !m_state->isDraggingDelegate() || !m_state->currentFolder()) {
return; return;
} }
@ -684,7 +705,7 @@ void DragState::onLeaveFolderTimerFinished()
void DragState::onChangeFolderPageTimerFinished() void DragState::onChangeFolderPageTimerFinished()
{ {
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) { if (!m_state || !m_state->isDraggingDelegate() || !m_state->currentFolder()) {
return; return;
} }
@ -720,7 +741,7 @@ void DragState::onChangeFolderPageTimerFinished()
void DragState::onFolderInsertBetweenTimerFinished() void DragState::onFolderInsertBetweenTimerFinished()
{ {
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) { if (!m_state || !m_state->isDraggingDelegate() || !m_state->currentFolder()) {
return; return;
} }

View file

@ -118,6 +118,7 @@ private Q_SLOTS:
void onDelegateDragFromFolderStarted(FolioApplicationFolder *folder, int position); void onDelegateDragFromFolderStarted(FolioApplicationFolder *folder, int position);
void onDelegateDragFromWidgetListStarted(QString appletPluginId); void onDelegateDragFromWidgetListStarted(QString appletPluginId);
void onDelegateDropped(); void onDelegateDropped();
void onDelegateDraggingCancelled();
void onLeaveCurrentFolder(); void onLeaveCurrentFolder();
@ -129,6 +130,9 @@ private Q_SLOTS:
void onFavouritesInsertBetweenTimerFinished(); void onFavouritesInsertBetweenTimerFinished();
private: private:
// Cleanup after a drag event (stop timers etc.)
void dragStopCleanup();
// deletes the delegate at m_startPosition // deletes the delegate at m_startPosition
void deleteStartPositionDelegate(); void deleteStartPositionDelegate();

View file

@ -195,6 +195,7 @@ void HomeScreenState::setSwipeState(SwipeState swipeState)
if (swipeState != m_swipeState) { if (swipeState != m_swipeState) {
m_swipeState = swipeState; m_swipeState = swipeState;
Q_EMIT swipeStateChanged(); Q_EMIT swipeStateChanged();
Q_EMIT isDraggingDelegateChanged();
} }
} }
@ -692,6 +693,11 @@ int HomeScreenState::currentFolderPage()
return m_folderPageNum; return m_folderPageNum;
} }
bool HomeScreenState::isDraggingDelegate()
{
return m_dragDropActive || m_swipeState == SwipeState::DraggingDelegate;
}
FolioDelegate *HomeScreenState::getPageDelegateAt(int page, int row, int column) FolioDelegate *HomeScreenState::getPageDelegateAt(int page, int row, int column)
{ {
PageModel *pageModel = m_homeScreen->pageListModel()->getPage(page); PageModel *pageModel = m_homeScreen->pageListModel()->getPage(page);
@ -905,6 +911,7 @@ void HomeScreenState::startDelegateAppDrawerDrag(qreal startX, qreal startY, qre
// we start dragging the delegate immediately from the app drawer, because we don't have a context menu to deal with! // we start dragging the delegate immediately from the app drawer, because we don't have a context menu to deal with!
// NOTE: this has to happen after delegateDragFromAppDrawerStarted, because slots for that expect SwipeState::AwaitingDraggingDelegate // NOTE: this has to happen after delegateDragFromAppDrawerStarted, because slots for that expect SwipeState::AwaitingDraggingDelegate
setSwipeState(SwipeState::DraggingDelegate); setSwipeState(SwipeState::DraggingDelegate);
Q_EMIT delegateDragStarted();
} }
void HomeScreenState::startDelegateFolderDrag(qreal startX, void HomeScreenState::startDelegateFolderDrag(qreal startX,
@ -922,10 +929,6 @@ void HomeScreenState::startDelegateWidgetListDrag(qreal startX, qreal startY, qr
{ {
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY); startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
Q_EMIT delegateDragFromWidgetListStarted(appletPluginId); Q_EMIT delegateDragFromWidgetListStarted(appletPluginId);
// we start dragging the delegate immediately from the app drawer, because we don't have a context menu to deal with!
// NOTE: this has to happen after delegateDragFromAppDrawerStarted, because slots for that expect SwipeState::AwaitingDraggingDelegate
setSwipeState(SwipeState::DraggingDelegate);
} }
void HomeScreenState::cancelDelegateDrag() void HomeScreenState::cancelDelegateDrag()
@ -991,7 +994,7 @@ void HomeScreenState::swipeEnded()
break; break;
} }
case SwipeState::DraggingDelegate: case SwipeState::DraggingDelegate:
Q_EMIT delegateDragEnded(); Q_EMIT delegateDragDropped();
break; break;
case SwipeState::AwaitingDraggingDelegate: case SwipeState::AwaitingDraggingDelegate:
case SwipeState::DeterminingSwipeType: case SwipeState::DeterminingSwipeType:
@ -1003,6 +1006,45 @@ void HomeScreenState::swipeEnded()
setSwipeState(SwipeState::None); setSwipeState(SwipeState::None);
} }
void HomeScreenState::dragStart()
{
// Cancel AwaitingDraggingDelegate
swipeEnded();
m_dragDropActive = true;
Q_EMIT delegateDragStarted();
Q_EMIT isDraggingDelegateChanged();
}
void HomeScreenState::dragMove(qreal deltaX, qreal deltaY)
{
if (!m_dragDropActive) {
return;
}
setDelegateDragX(m_delegateDragX + deltaX);
setDelegateDragY(m_delegateDragY + deltaY);
}
void HomeScreenState::dragDrop()
{
if (!m_dragDropActive) {
return;
}
m_dragDropActive = false;
Q_EMIT delegateDragDropped();
Q_EMIT isDraggingDelegateChanged();
}
void HomeScreenState::dragCancel()
{
if (!m_dragDropActive) {
return;
}
m_dragDropActive = false;
Q_EMIT delegateDragCancelled();
Q_EMIT isDraggingDelegateChanged();
}
void HomeScreenState::swipeCancelled() void HomeScreenState::swipeCancelled()
{ {
setSwipeState(SwipeState::None); setSwipeState(SwipeState::None);
@ -1038,6 +1080,7 @@ void HomeScreenState::swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal del
break; break;
case SwipeState::AwaitingDraggingDelegate: case SwipeState::AwaitingDraggingDelegate:
setSwipeState(SwipeState::DraggingDelegate); setSwipeState(SwipeState::DraggingDelegate);
Q_EMIT delegateDragStarted();
break; break;
case SwipeState::DraggingDelegate: case SwipeState::DraggingDelegate:
setDelegateDragX(m_delegateDragX + deltaX); setDelegateDragX(m_delegateDragX + deltaX);

View file

@ -80,6 +80,8 @@ class HomeScreenState : public QObject
Q_PROPERTY(int currentPage READ currentPage NOTIFY pageNumChanged) Q_PROPERTY(int currentPage READ currentPage NOTIFY pageNumChanged)
Q_PROPERTY(int currentFolderPage READ currentFolderPage NOTIFY folderPageNumChanged) Q_PROPERTY(int currentFolderPage READ currentFolderPage NOTIFY folderPageNumChanged)
Q_PROPERTY(bool isDraggingDelegate READ isDraggingDelegate NOTIFY isDraggingDelegateChanged)
public: public:
enum SwipeState { enum SwipeState {
None, None,
@ -258,6 +260,9 @@ public:
int currentFolderPage(); int currentFolderPage();
// Whether something is being dragged (either from SwipeArea or Drag & Drop)
bool isDraggingDelegate();
// QML helpers // QML helpers
Q_INVOKABLE FolioDelegate *getPageDelegateAt(int page, int row, int column); Q_INVOKABLE FolioDelegate *getPageDelegateAt(int page, int row, int column);
Q_INVOKABLE FolioDelegate *getFavouritesDelegateAt(int position); Q_INVOKABLE FolioDelegate *getFavouritesDelegateAt(int position);
@ -305,7 +310,9 @@ Q_SIGNALS:
void searchWidgetYChanged(); void searchWidgetYChanged();
void delegateDragXChanged(); void delegateDragXChanged();
void delegateDragYChanged(); void delegateDragYChanged();
void delegateDragEnded(); void delegateDragStarted();
void delegateDragDropped();
void delegateDragCancelled();
void delegateDragFromPageStarted(int page, int row, int column); void delegateDragFromPageStarted(int page, int row, int column);
void delegateDragFromFavouritesStarted(int position); void delegateDragFromFavouritesStarted(int position);
void delegateDragFromAppDrawerStarted(QString storageId); void delegateDragFromAppDrawerStarted(QString storageId);
@ -313,6 +320,7 @@ Q_SIGNALS:
void delegateDragFromWidgetListStarted(QString appletPluginId); void delegateDragFromWidgetListStarted(QString appletPluginId);
void pageNumChanged(); void pageNumChanged();
void folderPageNumChanged(); void folderPageNumChanged();
void isDraggingDelegateChanged();
void leftCurrentFolder(); void leftCurrentFolder();
void folderAboutToOpen(qreal x, qreal y); // the position on the screen where the delegate is at, for animations void folderAboutToOpen(qreal x, qreal y); // the position on the screen where the delegate is at, for animations
@ -347,6 +355,12 @@ public Q_SLOTS:
void swipeCancelled(); void swipeCancelled();
void swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal deltaX, qreal deltaY); void swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal deltaX, qreal deltaY);
// from DropArea
void dragStart();
void dragMove(qreal deltaX, qreal deltaY);
void dragDrop();
void dragCancel();
private: private:
void setViewState(ViewState viewState); void setViewState(ViewState viewState);
void setSwipeState(SwipeState swipeState); void setSwipeState(SwipeState swipeState);
@ -410,6 +424,7 @@ private:
qreal m_delegateDragY{0}; qreal m_delegateDragY{0};
qreal m_delegateDragPointerOffsetX{0}; qreal m_delegateDragPointerOffsetX{0};
qreal m_delegateDragPointerOffsetY{0}; qreal m_delegateDragPointerOffsetY{0};
bool m_dragDropActive{false};
int m_pageNum{0}; int m_pageNum{0};
int m_folderPageNum{0}; int m_folderPageNum{0};

View file

@ -10,6 +10,8 @@ import org.kde.plasma.private.mobileshell as MobileShell
import "./delegate" import "./delegate"
// Placeholder item that the user sees as they drag app/folder delegates around.
// See WidgetDragItem for the equivalent for widgets.
Item { Item {
id: root id: root
property Folio.HomeScreen folio property Folio.HomeScreen folio
@ -75,15 +77,16 @@ Item {
property var delegateDroppedOn: null property var delegateDroppedOn: null
// reset and show drag item // reset and show drag item
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && !isWidgetDrag) { if (isWidgetDrag) {
return;
}
root.scale = 1.0; root.scale = 1.0;
root.visible = true; root.visible = true;
} }
}
// save the existing delegate at the spot (this is called before the delegate is dropped) // save the existing delegate at the spot (this is called before the delegate is dropped)
function onDelegateDragEnded() { function onDelegateDragDropped() {
if (root.isWidgetDrag) { if (root.isWidgetDrag) {
return; return;
} }
@ -103,6 +106,7 @@ Item {
break; break;
} }
} }
} }
Connections { Connections {

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
DropArea {
id: root
property Folio.HomeScreen folio
property Folio.HomeScreenState homeScreenState: folio.HomeScreenState
keys: ["text/x-plasmoidservicename"]
property real prevX
property real prevY
onEntered: (drag) => {
drag.accept();
const widthOffset = folio.HomeScreenState.pageCellWidth / 2;
const heightOffset = folio.HomeScreenState.pageCellHeight / 2;
homeScreenState.startDelegateWidgetListDrag(
drag.x - widthOffset,
drag.y - heightOffset,
widthOffset,
heightOffset,
drag.getDataAsString("text/x-plasmoidservicename")
);
homeScreenState.dragStart();
prevX = drag.x;
prevY = drag.y;
}
onDropped: (drop) => {
drop.accept();
dropWaitTimer.restart();
}
onExited: {
homeScreenState.dragCancel();
}
onPositionChanged: (drag) => {
drag.accept();
homeScreenState.dragMove(drag.x - prevX, drag.y - prevY);
prevX = drag.x;
prevY = drag.y;
}
// HACK: Seems to crash otherwise, Qt bug?
Timer {
id: dropWaitTimer
interval: 10
onTriggered: {
homeScreenState.dragDrop();
}
}
}

View file

@ -66,7 +66,7 @@ MouseArea {
readonly property var dragState: folio.HomeScreenState.dragState readonly property var dragState: folio.HomeScreenState.dragState
readonly property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Favourites && readonly property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Favourites &&
dragState.candidateDropPosition.favouritesPosition === delegate.index dragState.candidateDropPosition.favouritesPosition === delegate.index
readonly property bool isAppHoveredOver: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && readonly property bool isAppHoveredOver: folio.HomeScreenState.isDraggingDelegate &&
dragState.dropDelegate && dragState.dropDelegate &&
dragState.dropDelegate.type === Folio.FolioDelegate.Application && dragState.dropDelegate.type === Folio.FolioDelegate.Application &&
isDropPositionThis isDropPositionThis
@ -178,7 +178,7 @@ MouseArea {
shadow: true shadow: true
turnToFolder: delegate.isAppHoveredOver turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
// do not show if the drop animation is running to this delegate // do not show if the drop animation is running to this delegate
visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis) visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis)
@ -221,12 +221,10 @@ MouseArea {
Connections { Connections {
target: folio.HomeScreenState target: folio.HomeScreenState
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close(); contextMenu.close();
} }
} }
}
actions: [ actions: [
Kirigami.Action { Kirigami.Action {
@ -298,12 +296,10 @@ MouseArea {
Connections { Connections {
target: folio.HomeScreenState target: folio.HomeScreenState
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close(); contextMenu.close();
} }
} }
}
actions: [ actions: [
Kirigami.Action { Kirigami.Action {

View file

@ -326,7 +326,7 @@ Folio.DelegateTouchArea {
anchors.fill: parent anchors.fill: parent
sourceComponent: { sourceComponent: {
if (delegate.delegateModel.type === Folio.FolioDelegate.Application) { if (delegate.delegateModel && delegate.delegateModel.type === Folio.FolioDelegate.Application) {
return appComponent; return appComponent;
} else { } else {
return noneComponent; return noneComponent;
@ -391,12 +391,10 @@ Folio.DelegateTouchArea {
Connections { Connections {
target: folio.HomeScreenState target: folio.HomeScreenState
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close(); contextMenu.close();
} }
} }
}
actions: [ actions: [
Kirigami.Action { Kirigami.Action {

View file

@ -117,13 +117,20 @@ Item {
} }
} }
// area that can be swiped // Drag and drop area
DelegateDropArea {
id: dropArea
folio: root.folio
anchors.fill: parent
// Area that can be swiped
MobileShell.SwipeArea { MobileShell.SwipeArea {
id: swipeArea id: swipeArea
anchors.fill: parent anchors.fill: parent
interactive: root.interactive && interactive: root.interactive &&
settingsLoader.homeScreenInteractive && settings.homeScreenInteractive &&
!dropArea.containsDrag &&
(appDrawer.flickable.atYBeginning || // there are cases where contentY > 0 but atYBeginning is true (appDrawer.flickable.atYBeginning || // there are cases where contentY > 0 but atYBeginning is true
appDrawer.flickable.contentY <= 10 || appDrawer.flickable.contentY <= 10 ||
// disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop // disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop
@ -160,35 +167,23 @@ Item {
} }
} }
Loader { SettingsComponent {
id: settingsLoader id: settings
asynchronous: true folio: root.folio
active: true homeScreen: root
settingsModeHomeScreenScale: root.settingsModeHomeScreenScale
// Don't anchor, since we set y // Don't anchor, since we set y
width: parent.width width: parent.width
height: parent.height height: parent.height
opacity: folio.HomeScreenState.settingsOpenProgress opacity: folio.HomeScreenState.settingsOpenProgress
visible: opacity > 0
// move the settings out of the way if it is not visible
// NOTE: we do this instead of setting visible to false, because
// it doesn't mess with widget drag and drop
y: (opacity > 0) ? 0 : parent.height
z: 1 z: 1
readonly property bool homeScreenInteractive: item ? item.homeScreenInteractive : true
sourceComponent: SettingsComponent {
id: settings
folio: root.folio
bottomMargin: root.bottomMargin bottomMargin: root.bottomMargin
leftMargin: root.leftMargin leftMargin: root.leftMargin
rightMargin: root.rightMargin rightMargin: root.rightMargin
settingsModeHomeScreenScale: root.settingsModeHomeScreenScale
homeScreen: root
}
} }
Item { Item {
@ -610,4 +605,5 @@ Item {
anchors.rightMargin: root.rightMargin anchors.rightMargin: root.rightMargin
} }
} }
}
} }

View file

@ -124,7 +124,7 @@ Item {
property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget
// only show if it is an empty spot on this page // only show if it is an empty spot on this page
visible: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && visible: folio.HomeScreenState.isDraggingDelegate &&
dropPosition.location === Folio.DelegateDragPosition.Pages && dropPosition.location === Folio.DelegateDragPosition.Pages &&
dropPosition.page === root.pageNum && dropPosition.page === root.pageNum &&
!dropDelegateIsWidget && !dropDelegateIsWidget &&
@ -145,7 +145,7 @@ Item {
property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget
// only show if the widget can be placed here // only show if the widget can be placed here
visible: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && visible: folio.HomeScreenState.isDraggingDelegate &&
dropPosition.location === Folio.DelegateDragPosition.Pages && dropPosition.location === Folio.DelegateDragPosition.Pages &&
dropPosition.page === root.pageNum && dropPosition.page === root.pageNum &&
dropDelegateIsWidget && dropDelegateIsWidget &&
@ -180,7 +180,7 @@ Item {
dragState.candidateDropPosition.pageRow === delegate.pageDelegate.row && dragState.candidateDropPosition.pageRow === delegate.pageDelegate.row &&
dragState.candidateDropPosition.pageColumn === delegate.pageDelegate.column dragState.candidateDropPosition.pageColumn === delegate.pageDelegate.column
property bool isAppHoveredOver: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && property bool isAppHoveredOver: folio.HomeScreenState.isDraggingDelegate &&
dragState.dropDelegate && dragState.dropDelegate &&
dragState.dropDelegate.type === Folio.FolioDelegate.Application && dragState.dropDelegate.type === Folio.FolioDelegate.Application &&
isDropPositionThis isDropPositionThis
@ -274,7 +274,7 @@ Item {
name: folio.FolioSettings.showPagesAppLabels ? delegate.pageDelegate.application.name : "" name: folio.FolioSettings.showPagesAppLabels ? delegate.pageDelegate.application.name : ""
application: delegate.pageDelegate.application application: delegate.pageDelegate.application
turnToFolder: delegate.isAppHoveredOver turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
implicitWidth: folio.HomeScreenState.pageCellWidth implicitWidth: folio.HomeScreenState.pageCellWidth
implicitHeight: folio.HomeScreenState.pageCellHeight implicitHeight: folio.HomeScreenState.pageCellHeight
@ -323,12 +323,10 @@ Item {
Connections { Connections {
target: folio.HomeScreenState target: folio.HomeScreenState
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close(); contextMenu.close();
} }
} }
}
actions: [ actions: [
Kirigami.Action { Kirigami.Action {
@ -409,12 +407,10 @@ Item {
Connections { Connections {
target: folio.HomeScreenState target: folio.HomeScreenState
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close(); contextMenu.close();
} }
} }
}
actions: [ actions: [
Kirigami.Action { Kirigami.Action {

View file

@ -16,6 +16,8 @@ import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import './delegate' import './delegate'
import './private' import './private'
// Placeholder item that the user sees as they drag widgets around.
// See DelegateDragItem for the equivalent for app delegates.
Item { Item {
id: root id: root
property Folio.HomeScreen folio property Folio.HomeScreen folio
@ -25,7 +27,9 @@ Item {
property Folio.FolioWidget widget property Folio.FolioWidget widget
readonly property bool isWidgetDelegate: folio.HomeScreenState.dragState.dropDelegate && folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Widget readonly property bool isWidgetDelegate: folio.HomeScreenState.dragState.dropDelegate
&& folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Widget
&& folio.HomeScreenState.dragState.dropDelegate.widget.visualApplet
readonly property bool dropAnimationRunning: dragXAnim.running || dragYAnim.running readonly property bool dropAnimationRunning: dragXAnim.running || dragYAnim.running
visible: false visible: false
@ -74,15 +78,14 @@ Item {
id: stateWatcher id: stateWatcher
target: folio.HomeScreenState target: folio.HomeScreenState
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && if (!root.isWidgetDelegate) {
folio.HomeScreenState.dragState.dropDelegate && return;
folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Widget) { }
root.startDrag(folio.HomeScreenState.dragState.dropDelegate.widget); root.startDrag(folio.HomeScreenState.dragState.dropDelegate.widget);
} }
} }
}
Connections { Connections {
target: folio.HomeScreenState.dragState target: folio.HomeScreenState.dragState

View file

@ -91,13 +91,11 @@ Item {
target: folio.HomeScreenState target: folio.HomeScreenState
// if we are starting drag-and-drop, close the menu immediately // if we are starting drag-and-drop, close the menu immediately
function onSwipeStateChanged() { function onDelegateDragStarted() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
configOverlay.animClose(); configOverlay.animClose();
root.closed(); root.closed();
} }
} }
}
// the config overlay // the config overlay
FastBlur { FastBlur {

View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2023-2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.private.shell 2.0
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell as MobileShell
Item {
id: delegate
property Folio.HomeScreen folio
readonly property string pluginName: model.pluginName
property real zoomScale: (model.isSupported && mouseArea.pressed) ? 0.8 : 1
transform: Scale {
origin.x: delegate.width / 2;
origin.y: delegate.height / 2;
xScale: delegate.zoomScale
yScale: delegate.zoomScale
}
Behavior on zoomScale { NumberAnimation { duration: 80 } }
// Placeholder item used for implement drag & drop
Item {
id: draggable
anchors.fill: parent
Drag.hotSpot.x: iconWidget.width / 2
Drag.hotSpot.y: iconWidget.height / 2
Drag.mimeData: { "text/x-plasmoidservicename": pluginName }
Drag.dragType: Drag.Automatic
Drag.onDragFinished: {
root.requestClose();
}
}
MobileShell.HapticsEffect {
id: haptics
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: model.isSupported ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onPressAndHold: {
if (!model.isSupported) {
return
}
haptics.buttonVibrate();
iconWidget.grabToImage(function(result) {
// Start drag & drop
folio.HomeScreenState.closeSettingsView();
draggable.Drag.imageSource = result.url;
draggable.Drag.active = true;
root.requestClose();
})
}
}
Rectangle {
id: background
color: Qt.rgba(255, 255, 255, 0.3)
visible: model.isSupported && mouseArea.containsMouse
radius: Kirigami.Units.cornerRadius
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
Item {
id: iconWidget
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.preferredHeight: Kirigami.Units.iconSizes.large
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.alignment: Qt.AlignBottom
Kirigami.Icon {
anchors.centerIn: parent
source: model.decoration
visible: model.screenshot == ""
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
}
Image {
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: model.screenshot
width: Kirigami.Units.iconSizes.large
height: Kirigami.Units.iconSizes.large
}
}
PC3.Label {
id: heading
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.alignment: Qt.AlignCenter
text: model.name
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
horizontalAlignment: Text.AlignHCenter
font.weight: Font.Bold
}
PC3.Label {
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.alignment: Qt.AlignTop
// otherwise causes binding loop due to the way the Plasma sets the height
height: implicitHeight
text: model.isSupported ? model.description : model.unsupportedMessage
font.pointSize: Kirigami.Theme.smallFont.pointSize
wrapMode: Text.Wrap
elide: Text.ElideRight
maximumLineCount: heading.lineCount === 1 ? 3 : 2
horizontalAlignment: Text.AlignHCenter
}
}
}

View file

@ -19,28 +19,27 @@ import org.kde.plasma.private.mobileshell as MobileShell
import '../delegate' import '../delegate'
import '../private' import '../private'
MouseArea { Loader {
id: root id: root
property Folio.HomeScreen folio property Folio.HomeScreen folio
property var homeScreen property var homeScreen
signal requestClose() active: false
onClicked: root.requestClose()
function requestClose() {
active = false;
}
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
MobileShell.HapticsEffect { sourceComponent: Item {
id: haptics
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.7) color: Qt.rgba(0, 0, 0, 0.7)
} }
PlasmaExtras.ModelContextMenu { PlasmaExtras.ModelContextMenu {
id: getWidgetsDialog id: getWidgetsDialog
visualParent: getWidgetsButton visualParent: getWidgetsButton
@ -109,7 +108,6 @@ MouseArea {
model: widgetExplorer.widgetsModel model: widgetExplorer.widgetsModel
readonly property real maxCellWidth: Kirigami.Units.gridUnit * 20
readonly property real intendedCellWidth: Kirigami.Units.gridUnit * 8 readonly property real intendedCellWidth: Kirigami.Units.gridUnit * 8
readonly property int columns: Math.min(5, (width - leftMargin - rightMargin) / intendedCellWidth) readonly property int columns: Math.min(5, (width - leftMargin - rightMargin) / intendedCellWidth)
@ -120,117 +118,10 @@ MouseArea {
leftMargin: horizontalMargin leftMargin: horizontalMargin
rightMargin: horizontalMargin rightMargin: horizontalMargin
MouseArea { delegate: AppletListDelegate {
z: -1 folio: root.folio
anchors.fill: parent
onClicked: root.requestClose()
}
delegate: MouseArea {
id: delegate
width: gridView.cellWidth width: gridView.cellWidth
height: gridView.cellHeight height: gridView.cellHeight
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property real zoomScale: pressed ? 0.8 : 1
transform: Scale {
origin.x: delegate.width / 2;
origin.y: delegate.height / 2;
xScale: delegate.zoomScale
yScale: delegate.zoomScale
}
Behavior on zoomScale { NumberAnimation { duration: 80 } }
readonly property string pluginName: model.pluginName
onPressAndHold: {
if (!model.isSupported) {
return
}
root.requestClose();
folio.HomeScreenState.closeSettingsView();
haptics.buttonVibrate();
let mappedCoords = root.homeScreen.prepareStartDelegateDrag(null, delegate, true);
const widthOffset = folio.HomeScreenState.pageCellWidth / 2;
const heightOffset = folio.HomeScreenState.pageCellHeight / 2;
folio.HomeScreenState.startDelegateWidgetListDrag(
mappedCoords.x + mouseX - widthOffset,
mappedCoords.y + mouseY - heightOffset,
widthOffset,
heightOffset,
pluginName
);
}
Rectangle {
id: background
color: Qt.rgba(255, 255, 255, 0.3)
visible: delegate.containsMouse
radius: Kirigami.Units.cornerRadius
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
Item {
id: iconWidget
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.preferredHeight: Kirigami.Units.iconSizes.large
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.alignment: Qt.AlignBottom
Kirigami.Icon {
anchors.centerIn: parent
source: model.decoration
visible: model.screenshot == ""
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
}
Image {
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: model.screenshot
width: Kirigami.Units.iconSizes.large
height: Kirigami.Units.iconSizes.large
}
}
PC3.Label {
id: heading
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.alignment: Qt.AlignCenter
text: model.name
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
horizontalAlignment: Text.AlignHCenter
font.weight: Font.Bold
}
PC3.Label {
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.alignment: Qt.AlignTop
// otherwise causes binding loop due to the way the Plasma sets the height
height: implicitHeight
text: model.isSupported ? model.description : model.unsupportedMessage
font.pointSize: Kirigami.Theme.smallFont.pointSize
wrapMode: Text.Wrap
elide: Text.ElideRight
maximumLineCount: heading.lineCount === 1 ? 3 : 2
horizontalAlignment: Text.AlignHCenter
}
}
} }
} }
@ -246,4 +137,5 @@ MouseArea {
onShouldClose: root.requestClose() onShouldClose: root.requestClose()
} }
}
} }

View file

@ -21,23 +21,12 @@ Item {
property var homeScreen property var homeScreen
property real settingsModeHomeScreenScale property real settingsModeHomeScreenScale
readonly property bool homeScreenInteractive: !appletListViewerLoader.active readonly property bool homeScreenInteractive: !appletListViewer.active
property real bottomMargin: 0 property real bottomMargin: 0
property real leftMargin: 0 property real leftMargin: 0
property real rightMargin: 0 property real rightMargin: 0
Connections {
target: folio.HomeScreenState
// Close applet viewer when settings view closes
function onViewStateChanged() {
if (folio.HomeScreenState.viewState !== Folio.HomeScreenState.SettingsView) {
appletListViewerLoader.requestClose();
}
}
}
MouseArea { MouseArea {
id: closeSettings id: closeSettings
@ -84,7 +73,7 @@ Item {
iconName: 'widget-alternatives' iconName: 'widget-alternatives'
textLabel: i18n("Widgets") textLabel: i18n("Widgets")
onClicked: { onClicked: {
appletListViewerLoader.active = true; appletListViewer.active = true;
} }
} }
} }
@ -157,49 +146,13 @@ Item {
} }
] ]
Loader { AppletListViewer {
id: appletListViewerLoader
asynchronous: true
active: false
signal requestClose()
onRequestClose: item?.requestClose()
width: parent.width
height: parent.height
opacity: status == Loader.Ready ? 1 : 0
// move the settings out of the way if it is not visible
// NOTE: we do this instead of setting visible to false, because
// it doesn't mess with widget drag and drop
y: (opacity > 0) ? 0 : parent.height
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration }
}
sourceComponent: AppletListViewer {
id: appletListViewer id: appletListViewer
folio: root.folio
width: parent.width width: parent.width
height: parent.height height: parent.height
onRequestClose: parent.active = false folio: root.folio
homeScreen: root.homeScreen homeScreen: root.homeScreen
} }
}
PC3.BusyIndicator {
id: appletListLoadingIndicator
anchors.centerIn: parent
visible: appletListViewerLoader.status === Loader.Loading
implicitHeight: Kirigami.Units.iconSizes.huge
implicitWidth: Kirigami.Units.iconSizes.huge
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
}
} }