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/AppDrawerHeader.qml
qml/DelegateDragItem.qml
qml/DelegateDropArea.qml
qml/FavouritesBar.qml
qml/FolderView.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
qml/settings/AppletListDelegate.qml
qml/settings/AppletListViewer.qml
qml/settings/SettingsButton.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::delegateDragFromFolderStarted, this, &DragState::onDelegateDragFromFolderStarted);
connect(m_state, &HomeScreenState::delegateDragFromWidgetListStarted, this, &DragState::onDelegateDragFromWidgetListStarted);
connect(m_state, &HomeScreenState::swipeStateChanged, this, [this]() {
if (m_state->swipeState() == HomeScreenState::DraggingDelegate) {
onDelegateDraggingStarted();
}
});
connect(m_state, &HomeScreenState::delegateDragEnded, this, &DragState::onDelegateDropped);
connect(m_state, &HomeScreenState::delegateDragStarted, this, &DragState::onDelegateDraggingStarted);
connect(m_state, &HomeScreenState::delegateDragDropped, this, &DragState::onDelegateDropped);
connect(m_state, &HomeScreenState::delegateDragCancelled, this, &DragState::onDelegateDraggingCancelled);
connect(m_state, &HomeScreenState::pageNumChanged, this, [this]() {
m_candidateDropPosition->setPageRow(m_state->currentPage());
@ -546,19 +543,8 @@ void DragState::onDelegateDropped()
// add dropped delegate
bool success = createDropPositionDelegate();
// 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();
// Cleanup timers and state
dragStopCleanup();
// emit corresponding signal
// -> 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()
{
if (!m_state) {
@ -589,7 +610,7 @@ void DragState::onLeaveCurrentFolder()
void DragState::onChangePageTimerFinished()
{
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate)) {
if (!m_state || !m_state->isDraggingDelegate()) {
return;
}
@ -624,7 +645,7 @@ void DragState::onChangePageTimerFinished()
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)) {
return;
}
@ -672,7 +693,7 @@ void DragState::onOpenFolderTimerFinished()
void DragState::onLeaveFolderTimerFinished()
{
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) {
if (!m_state || !m_state->isDraggingDelegate() || !m_state->currentFolder()) {
return;
}
@ -684,7 +705,7 @@ void DragState::onLeaveFolderTimerFinished()
void DragState::onChangeFolderPageTimerFinished()
{
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) {
if (!m_state || !m_state->isDraggingDelegate() || !m_state->currentFolder()) {
return;
}
@ -720,7 +741,7 @@ void DragState::onChangeFolderPageTimerFinished()
void DragState::onFolderInsertBetweenTimerFinished()
{
if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) {
if (!m_state || !m_state->isDraggingDelegate() || !m_state->currentFolder()) {
return;
}

View file

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

View file

@ -195,6 +195,7 @@ void HomeScreenState::setSwipeState(SwipeState swipeState)
if (swipeState != m_swipeState) {
m_swipeState = swipeState;
Q_EMIT swipeStateChanged();
Q_EMIT isDraggingDelegateChanged();
}
}
@ -692,6 +693,11 @@ int HomeScreenState::currentFolderPage()
return m_folderPageNum;
}
bool HomeScreenState::isDraggingDelegate()
{
return m_dragDropActive || m_swipeState == SwipeState::DraggingDelegate;
}
FolioDelegate *HomeScreenState::getPageDelegateAt(int page, int row, int column)
{
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!
// NOTE: this has to happen after delegateDragFromAppDrawerStarted, because slots for that expect SwipeState::AwaitingDraggingDelegate
setSwipeState(SwipeState::DraggingDelegate);
Q_EMIT delegateDragStarted();
}
void HomeScreenState::startDelegateFolderDrag(qreal startX,
@ -922,10 +929,6 @@ void HomeScreenState::startDelegateWidgetListDrag(qreal startX, qreal startY, qr
{
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
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()
@ -991,7 +994,7 @@ void HomeScreenState::swipeEnded()
break;
}
case SwipeState::DraggingDelegate:
Q_EMIT delegateDragEnded();
Q_EMIT delegateDragDropped();
break;
case SwipeState::AwaitingDraggingDelegate:
case SwipeState::DeterminingSwipeType:
@ -1003,6 +1006,45 @@ void HomeScreenState::swipeEnded()
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()
{
setSwipeState(SwipeState::None);
@ -1038,6 +1080,7 @@ void HomeScreenState::swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal del
break;
case SwipeState::AwaitingDraggingDelegate:
setSwipeState(SwipeState::DraggingDelegate);
Q_EMIT delegateDragStarted();
break;
case SwipeState::DraggingDelegate:
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 currentFolderPage READ currentFolderPage NOTIFY folderPageNumChanged)
Q_PROPERTY(bool isDraggingDelegate READ isDraggingDelegate NOTIFY isDraggingDelegateChanged)
public:
enum SwipeState {
None,
@ -258,6 +260,9 @@ public:
int currentFolderPage();
// Whether something is being dragged (either from SwipeArea or Drag & Drop)
bool isDraggingDelegate();
// QML helpers
Q_INVOKABLE FolioDelegate *getPageDelegateAt(int page, int row, int column);
Q_INVOKABLE FolioDelegate *getFavouritesDelegateAt(int position);
@ -305,7 +310,9 @@ Q_SIGNALS:
void searchWidgetYChanged();
void delegateDragXChanged();
void delegateDragYChanged();
void delegateDragEnded();
void delegateDragStarted();
void delegateDragDropped();
void delegateDragCancelled();
void delegateDragFromPageStarted(int page, int row, int column);
void delegateDragFromFavouritesStarted(int position);
void delegateDragFromAppDrawerStarted(QString storageId);
@ -313,6 +320,7 @@ Q_SIGNALS:
void delegateDragFromWidgetListStarted(QString appletPluginId);
void pageNumChanged();
void folderPageNumChanged();
void isDraggingDelegateChanged();
void leftCurrentFolder();
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 swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal deltaX, qreal deltaY);
// from DropArea
void dragStart();
void dragMove(qreal deltaX, qreal deltaY);
void dragDrop();
void dragCancel();
private:
void setViewState(ViewState viewState);
void setSwipeState(SwipeState swipeState);
@ -410,6 +424,7 @@ private:
qreal m_delegateDragY{0};
qreal m_delegateDragPointerOffsetX{0};
qreal m_delegateDragPointerOffsetY{0};
bool m_dragDropActive{false};
int m_pageNum{0};
int m_folderPageNum{0};

View file

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

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 bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Favourites &&
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.type === Folio.FolioDelegate.Application &&
isDropPositionThis
@ -178,7 +178,7 @@ MouseArea {
shadow: true
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
visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis)
@ -221,10 +221,8 @@ MouseArea {
Connections {
target: folio.HomeScreenState
function onSwipeStateChanged() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close();
}
function onDelegateDragStarted() {
contextMenu.close();
}
}
@ -298,10 +296,8 @@ MouseArea {
Connections {
target: folio.HomeScreenState
function onSwipeStateChanged() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close();
}
function onDelegateDragStarted() {
contextMenu.close();
}
}

View file

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

View file

@ -117,497 +117,493 @@ Item {
}
}
// area that can be swiped
MobileShell.SwipeArea {
id: swipeArea
// Drag and drop area
DelegateDropArea {
id: dropArea
folio: root.folio
anchors.fill: parent
interactive: root.interactive &&
settingsLoader.homeScreenInteractive &&
(appDrawer.flickable.atYBeginning || // there are cases where contentY > 0 but atYBeginning is true
appDrawer.flickable.contentY <= 10 ||
// disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop
folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate ||
folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate ||
folio.HomeScreenState.swipeState === Folio.HomeScreenState.SwipingAppDrawerGrid ||
folio.HomeScreenState.viewState !== Folio.HomeScreenState.AppDrawerView)
// Area that can be swiped
MobileShell.SwipeArea {
id: swipeArea
anchors.fill: parent
onSwipeStarted: (currentPos, startPos) => {
const deltaX = currentPos.x - startPos.x;
const deltaY = currentPos.y - startPos.y;
homeScreenState.swipeStarted(deltaX, deltaY);
}
onSwipeEnded: {
homeScreenState.swipeEnded();
}
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => {
// cancel swipe when settings component is opening to prevent conflicts
if (folio.HomeScreenState.settingsOpenProgress && folio.HomeScreenState.viewState !== Folio.HomeScreenState.SettingsView) {
homeScreenState.swipeCancelled();
return;
interactive: root.interactive &&
settings.homeScreenInteractive &&
!dropArea.containsDrag &&
(appDrawer.flickable.atYBeginning || // there are cases where contentY > 0 but atYBeginning is true
appDrawer.flickable.contentY <= 10 ||
// disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop
folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate ||
folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate ||
folio.HomeScreenState.swipeState === Folio.HomeScreenState.SwipingAppDrawerGrid ||
folio.HomeScreenState.viewState !== Folio.HomeScreenState.AppDrawerView)
onSwipeStarted: (currentPos, startPos) => {
const deltaX = currentPos.x - startPos.x;
const deltaY = currentPos.y - startPos.y;
homeScreenState.swipeStarted(deltaX, deltaY);
}
homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
}
onTouchpadScrollStarted: homeScreenState.swipeStarted(0, 0);
onTouchpadScrollEnded: homeScreenState.swipeEnded();
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
onPressedChanged: {
if (pressed) {
// ensures that components like the widget settings overlay close when swiping
noFocus.forceActiveFocus();
onSwipeEnded: {
homeScreenState.swipeEnded();
}
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => {
// cancel swipe when settings component is opening to prevent conflicts
if (folio.HomeScreenState.settingsOpenProgress && folio.HomeScreenState.viewState !== Folio.HomeScreenState.SettingsView) {
homeScreenState.swipeCancelled();
return;
}
homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
}
}
Loader {
id: settingsLoader
asynchronous: true
active: true
onTouchpadScrollStarted: homeScreenState.swipeStarted(0, 0);
onTouchpadScrollEnded: homeScreenState.swipeEnded();
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
// Don't anchor, since we set y
width: parent.width
height: parent.height
opacity: folio.HomeScreenState.settingsOpenProgress
onPressedChanged: {
if (pressed) {
// ensures that components like the widget settings overlay close when swiping
noFocus.forceActiveFocus();
}
}
// 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
readonly property bool homeScreenInteractive: item ? item.homeScreenInteractive : true
sourceComponent: SettingsComponent {
SettingsComponent {
id: settings
folio: root.folio
homeScreen: root
settingsModeHomeScreenScale: root.settingsModeHomeScreenScale
// Don't anchor, since we set y
width: parent.width
height: parent.height
opacity: folio.HomeScreenState.settingsOpenProgress
visible: opacity > 0
z: 1
bottomMargin: root.bottomMargin
leftMargin: root.leftMargin
rightMargin: root.rightMargin
settingsModeHomeScreenScale: root.settingsModeHomeScreenScale
homeScreen: root
}
}
Item {
id: mainHomeScreen
anchors.fill: parent
Item {
id: mainHomeScreen
anchors.fill: parent
// we stop showing halfway through the animation
opacity: 1 - Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress, homeScreenState.folderOpenProgress) * 2
visible: opacity > 0 // prevent handlers from picking up events
transform: [
Scale {
property real scaleFactor: Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress)
origin.x: mainHomeScreen.width / 2
origin.y: mainHomeScreen.height / 2
yScale: 1 - (scaleFactor * 2) * 0.1
xScale: 1 - (scaleFactor * 2) * 0.1
}
]
HomeScreenPages {
id: homeScreenPages
folio: root.folio
maskManager: root.maskManager
homeScreen: root
anchors.topMargin: root.topMargin
anchors.leftMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : root.leftMargin
anchors.rightMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : root.rightMargin
anchors.bottomMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : root.bottomMargin
// update the model with page dimensions
onWidthChanged: {
homeScreenState.pageWidth = homeScreenPages.width;
}
onHeightChanged: {
homeScreenState.pageHeight = homeScreenPages.height;
}
// Keyboard navigation from pages
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Up:
// Open search widget when going up
folio.HomeScreenState.openSearchWidget();
event.accepted = true;
break;
case Qt.Key_Down:
// Focus on favorites bar or app drawer, depending on its physical location
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom) {
favouritesBar.forceActiveFocus();
} else {
folio.HomeScreenState.openAppDrawer();
}
event.accepted = true;
break;
case Qt.Key_Left:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left) {
// If favorites bar is on the left, navigate to it
favouritesBar.forceActiveFocus();
} else {
// Otherwise go to page on the left
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage - 1, false);
homeScreenPages.focusCurrentPageForKeyboardNav();
}
event.accepted = true;
break;
case Qt.Key_Right:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right) {
// If favorites bar is on the right, navigate to it
favouritesBar.forceActiveFocus();
} else {
// Otherwise go to page on the right
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage + 1, false);
homeScreenPages.focusCurrentPageForKeyboardNav();
}
event.accepted = true;
break;
default:
break;
}
}
// we stop showing halfway through the animation
opacity: 1 - Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress, homeScreenState.folderOpenProgress) * 2
visible: opacity > 0 // prevent handlers from picking up events
transform: [
Scale {
// animation when settings opens
property real scaleFactor: 1 - folio.HomeScreenState.settingsOpenProgress * (1 - settingsModeHomeScreenScale)
origin.x: root.leftMargin + (root.width - root.rightMargin - root.leftMargin) / 2
origin.y: root.height * settingsModeHomeScreenScale / 2
xScale: scaleFactor
yScale: scaleFactor
property real scaleFactor: Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress)
origin.x: mainHomeScreen.width / 2
origin.y: mainHomeScreen.height / 2
yScale: 1 - (scaleFactor * 2) * 0.1
xScale: 1 - (scaleFactor * 2) * 0.1
}
]
states: [
State {
name: "bottom"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
AnchorChanges {
target: homeScreenPages
anchors.top: parent.top
anchors.bottom: favouritesBar.top
anchors.left: parent.left
anchors.right: parent.right
}
}, State {
name: "left"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left
AnchorChanges {
target: homeScreenPages
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: favouritesBar.right
anchors.right: parent.right
}
}, State {
name: "right"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right
AnchorChanges {
target: homeScreenPages
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: favouritesBar.left
HomeScreenPages {
id: homeScreenPages
folio: root.folio
maskManager: root.maskManager
homeScreen: root
anchors.topMargin: root.topMargin
anchors.leftMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : root.leftMargin
anchors.rightMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : root.rightMargin
anchors.bottomMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : root.bottomMargin
// update the model with page dimensions
onWidthChanged: {
homeScreenState.pageWidth = homeScreenPages.width;
}
onHeightChanged: {
homeScreenState.pageHeight = homeScreenPages.height;
}
// Keyboard navigation from pages
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Up:
// Open search widget when going up
folio.HomeScreenState.openSearchWidget();
event.accepted = true;
break;
case Qt.Key_Down:
// Focus on favorites bar or app drawer, depending on its physical location
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom) {
favouritesBar.forceActiveFocus();
} else {
folio.HomeScreenState.openAppDrawer();
}
event.accepted = true;
break;
case Qt.Key_Left:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left) {
// If favorites bar is on the left, navigate to it
favouritesBar.forceActiveFocus();
} else {
// Otherwise go to page on the left
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage - 1, false);
homeScreenPages.focusCurrentPageForKeyboardNav();
}
event.accepted = true;
break;
case Qt.Key_Right:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right) {
// If favorites bar is on the right, navigate to it
favouritesBar.forceActiveFocus();
} else {
// Otherwise go to page on the right
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage + 1, false);
homeScreenPages.focusCurrentPageForKeyboardNav();
}
event.accepted = true;
break;
default:
break;
}
}
]
transform: [
Scale {
// animation when settings opens
property real scaleFactor: 1 - folio.HomeScreenState.settingsOpenProgress * (1 - settingsModeHomeScreenScale)
origin.x: root.leftMargin + (root.width - root.rightMargin - root.leftMargin) / 2
origin.y: root.height * settingsModeHomeScreenScale / 2
xScale: scaleFactor
yScale: scaleFactor
}
]
states: [
State {
name: "bottom"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
AnchorChanges {
target: homeScreenPages
anchors.top: parent.top
anchors.bottom: favouritesBar.top
anchors.left: parent.left
anchors.right: parent.right
}
}, State {
name: "left"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left
AnchorChanges {
target: homeScreenPages
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: favouritesBar.right
anchors.right: parent.right
}
}, State {
name: "right"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right
AnchorChanges {
target: homeScreenPages
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: favouritesBar.left
}
}
]
}
Rectangle {
id: favouritesBarScrim
color: Qt.rgba(255, 255, 255, 0.2)
Component.onCompleted: maskManager.assignToMask(this)
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
visible: folio.FolioSettings.showFavouritesBarBackground
anchors.top: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? favouritesBar.top : parent.top
anchors.bottom: parent.bottom
anchors.left: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? favouritesBar.left : parent.left
anchors.right: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? favouritesBar.right : parent.right
// because of the scale animation, we need to extend the panel out a bit
anchors.topMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : -Kirigami.Units.gridUnit * 5
anchors.bottomMargin: -Kirigami.Units.gridUnit * 5
anchors.leftMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : -Kirigami.Units.gridUnit * 5
anchors.rightMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : -Kirigami.Units.gridUnit * 5
}
FavouritesBar {
id: favouritesBar
folio: root.folio
maskManager: root.maskManager
homeScreen: root
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
visible: opacity > 0
// one is ignored as anchors are set
height: Kirigami.Units.gridUnit * 6
width: Kirigami.Units.gridUnit * 6
anchors.topMargin: root.topMargin
anchors.bottomMargin: root.bottomMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
// Keyboard navigation on favorites bar
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Up:
// Focus on homescreen pages or search widget depending on physical position
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom) {
homeScreenPages.forceActiveFocus();
} else {
folio.HomeScreenState.openSearchWidget();
}
event.accepted = true;
break;
case Qt.Key_Down:
// Open app drawer
folio.HomeScreenState.openAppDrawer();
event.accepted = true;
break;
case Qt.Key_Left:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left) {
// Go to left page if mounted on the left
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage - 1, false);
event.accepted = true;
}
break;
case Qt.Key_Right:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right) {
// Go to right page if mounted on the right
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage + 1, false);
event.accepted = true;
}
break;
default:
break;
}
}
states: [
State {
name: "bottom"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
AnchorChanges {
target: favouritesBar
anchors.top: undefined
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
PropertyChanges {
target: favouritesBar
height: Kirigami.Units.gridUnit * 6
}
}, State {
name: "left"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left
AnchorChanges {
target: favouritesBar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: undefined
}
PropertyChanges {
target: favouritesBar
width: Kirigami.Units.gridUnit * 6
}
}, State {
name: "right"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right
AnchorChanges {
target: favouritesBar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: undefined
anchors.right: parent.right
}
PropertyChanges {
target: favouritesBar
width: Kirigami.Units.gridUnit * 6
}
}
]
}
Item {
id: pageIndicatorWrapper
property bool favouritesBarAtBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: favouritesBarAtBottom ? favouritesBar.top : parent.bottom
anchors.topMargin: root.topMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: favouritesBarAtBottom ? 0 : (root.bottomMargin + Kirigami.Units.largeSpacing)
// show page indicator if there are multiple pages
QQC2.PageIndicator {
visible: count > 1
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
currentIndex: folio.HomeScreenState.currentPage
count: folio.PageListModel.length
TapHandler {
onTapped: folio.HomeScreenState.openAppDrawer()
}
}
// show arrow to open app drawer when there is 1 page
Kirigami.Icon {
source: 'arrow-up'
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
visible: folio.PageListModel.length <= 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.smallSpacing
TapHandler {
onTapped: folio.HomeScreenState.openAppDrawer()
}
}
}
}
Rectangle {
id: favouritesBarScrim
color: Qt.rgba(255, 255, 255, 0.2)
Component.onCompleted: maskManager.assignToMask(this)
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
visible: folio.FolioSettings.showFavouritesBarBackground
anchors.top: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? favouritesBar.top : parent.top
anchors.bottom: parent.bottom
anchors.left: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? favouritesBar.left : parent.left
anchors.right: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? favouritesBar.right : parent.right
// because of the scale animation, we need to extend the panel out a bit
anchors.topMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : -Kirigami.Units.gridUnit * 5
anchors.bottomMargin: -Kirigami.Units.gridUnit * 5
anchors.leftMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : -Kirigami.Units.gridUnit * 5
anchors.rightMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : -Kirigami.Units.gridUnit * 5
}
FavouritesBar {
id: favouritesBar
// folder view
FolderView {
id: folderView
folio: root.folio
maskManager: root.maskManager
anchors.fill: parent
anchors.topMargin: root.topMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: root.bottomMargin
homeScreen: root
opacity: homeScreenState.folderOpenProgress
transform: Translate { y: folderView.opacity > 0 ? 0 : folderView.height }
}
// bottom app drawer
AppDrawer {
id: appDrawer
folio: root.folio
width: parent.width
height: parent.height
homeScreen: root
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
visible: opacity > 0
// we only start showing it halfway through
opacity: homeScreenState.appDrawerOpenProgress < 0.5 ? 0 : (homeScreenState.appDrawerOpenProgress - 0.5) * 2
// one is ignored as anchors are set
height: Kirigami.Units.gridUnit * 6
width: Kirigami.Units.gridUnit * 6
// position for animation
property real animationY: (1 - homeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2)
// move the app drawer 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 app drag and drop from the app drawer
y: (opacity > 0) ? animationY : parent.height
headerHeight: Math.round(Kirigami.Units.gridUnit * 4)
headerItem: AppDrawerHeader {
id: appDrawerHeader
folio: root.folio
onReleaseFocusRequested: appDrawer.forceActiveFocus()
}
// Account for panels
topPadding: root.topMargin
bottomPadding: root.bottomMargin
leftPadding: root.leftMargin
rightPadding: root.rightMargin
// Forward keyboard text to the search bar
Keys.onPressed: (event) => {
if (event.text.trim().length > 0) {
appDrawerHeader.addSearchText(event.text);
appDrawerHeader.forceActiveFocus();
event.accepted = true;
} else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
appDrawerHeader.forceActiveFocus();
event.accepted = true;
}
}
Connections {
target: folio.HomeScreenState
function onAppDrawerOpened() {
appDrawer.forceActiveFocus();
}
function onAppDrawerClosed() {
// reset app drawer position when closed
appDrawer.flickable.contentY = 0;
}
}
}
// search component
MobileShell.KRunnerScreen {
id: searchWidget
anchors.fill: parent
opacity: homeScreenState.searchWidgetOpenProgress
visible: opacity > 0
transform: Translate { y: (1 - homeScreenState.searchWidgetOpenProgress) * (-Kirigami.Units.gridUnit * 2) }
onVisibleChanged: {
if (!visible) {
// clear search bar when closed
searchWidget.clearField();
}
}
// focus the search bar if it opens
Connections {
target: folio.HomeScreenState
function onSearchWidgetOpenProgressChanged() {
if (homeScreenState.searchWidgetOpenProgress === 1.0) {
searchWidget.requestFocus();
}
}
}
onRequestedClose: (triggeredByKeyEvent) => {
homeScreenState.closeSearchWidget();
}
anchors.topMargin: root.topMargin
anchors.bottomMargin: root.bottomMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
// Keyboard navigation on favorites bar
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Up:
// Focus on homescreen pages or search widget depending on physical position
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom) {
homeScreenPages.forceActiveFocus();
} else {
folio.HomeScreenState.openSearchWidget();
}
event.accepted = true;
break;
case Qt.Key_Down:
// Open app drawer
folio.HomeScreenState.openAppDrawer();
event.accepted = true;
break;
case Qt.Key_Left:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left) {
// Go to left page if mounted on the left
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage - 1, false);
event.accepted = true;
}
break;
case Qt.Key_Right:
if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right) {
// Go to right page if mounted on the right
folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage + 1, false);
event.accepted = true;
}
break;
default:
break;
}
}
states: [
State {
name: "bottom"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
AnchorChanges {
target: favouritesBar
anchors.top: undefined
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
PropertyChanges {
target: favouritesBar
height: Kirigami.Units.gridUnit * 6
}
}, State {
name: "left"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left
AnchorChanges {
target: favouritesBar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: undefined
}
PropertyChanges {
target: favouritesBar
width: Kirigami.Units.gridUnit * 6
}
}, State {
name: "right"
when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right
AnchorChanges {
target: favouritesBar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: undefined
anchors.right: parent.right
}
PropertyChanges {
target: favouritesBar
width: Kirigami.Units.gridUnit * 6
}
}
]
}
Item {
id: pageIndicatorWrapper
property bool favouritesBarAtBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: favouritesBarAtBottom ? favouritesBar.top : parent.bottom
anchors.topMargin: root.topMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: favouritesBarAtBottom ? 0 : (root.bottomMargin + Kirigami.Units.largeSpacing)
// show page indicator if there are multiple pages
QQC2.PageIndicator {
visible: count > 1
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
currentIndex: folio.HomeScreenState.currentPage
count: folio.PageListModel.length
TapHandler {
onTapped: folio.HomeScreenState.openAppDrawer()
}
}
// show arrow to open app drawer when there is 1 page
Kirigami.Icon {
source: 'arrow-up'
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
visible: folio.PageListModel.length <= 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.smallSpacing
TapHandler {
onTapped: folio.HomeScreenState.openAppDrawer()
}
}
}
}
// folder view
FolderView {
id: folderView
folio: root.folio
anchors.fill: parent
anchors.topMargin: root.topMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: root.bottomMargin
homeScreen: root
opacity: homeScreenState.folderOpenProgress
transform: Translate { y: folderView.opacity > 0 ? 0 : folderView.height }
}
// bottom app drawer
AppDrawer {
id: appDrawer
folio: root.folio
width: parent.width
height: parent.height
homeScreen: root
// we only start showing it halfway through
opacity: homeScreenState.appDrawerOpenProgress < 0.5 ? 0 : (homeScreenState.appDrawerOpenProgress - 0.5) * 2
// position for animation
property real animationY: (1 - homeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2)
// move the app drawer 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 app drag and drop from the app drawer
y: (opacity > 0) ? animationY : parent.height
headerHeight: Math.round(Kirigami.Units.gridUnit * 4)
headerItem: AppDrawerHeader {
id: appDrawerHeader
folio: root.folio
onReleaseFocusRequested: appDrawer.forceActiveFocus()
}
// Account for panels
topPadding: root.topMargin
bottomPadding: root.bottomMargin
leftPadding: root.leftMargin
rightPadding: root.rightMargin
// Forward keyboard text to the search bar
Keys.onPressed: (event) => {
if (event.text.trim().length > 0) {
appDrawerHeader.addSearchText(event.text);
appDrawerHeader.forceActiveFocus();
event.accepted = true;
} else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
appDrawerHeader.forceActiveFocus();
event.accepted = true;
}
}
Connections {
target: folio.HomeScreenState
function onAppDrawerOpened() {
appDrawer.forceActiveFocus();
}
function onAppDrawerClosed() {
// reset app drawer position when closed
appDrawer.flickable.contentY = 0;
}
}
}
// search component
MobileShell.KRunnerScreen {
id: searchWidget
anchors.fill: parent
opacity: homeScreenState.searchWidgetOpenProgress
visible: opacity > 0
transform: Translate { y: (1 - homeScreenState.searchWidgetOpenProgress) * (-Kirigami.Units.gridUnit * 2) }
onVisibleChanged: {
if (!visible) {
// clear search bar when closed
searchWidget.clearField();
}
}
// focus the search bar if it opens
Connections {
target: folio.HomeScreenState
function onSearchWidgetOpenProgressChanged() {
if (homeScreenState.searchWidgetOpenProgress === 1.0) {
searchWidget.requestFocus();
}
}
}
onRequestedClose: (triggeredByKeyEvent) => {
homeScreenState.closeSearchWidget();
}
anchors.topMargin: root.topMargin
anchors.bottomMargin: root.bottomMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
}
}
}

View file

@ -124,11 +124,11 @@ Item {
property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget
// only show if it is an empty spot on this page
visible: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate &&
dropPosition.location === Folio.DelegateDragPosition.Pages &&
dropPosition.page === root.pageNum &&
!dropDelegateIsWidget &&
folio.HomeScreenState.getPageDelegateAt(root.pageNum, dropPosition.pageRow, dropPosition.pageColumn) === null
visible: folio.HomeScreenState.isDraggingDelegate &&
dropPosition.location === Folio.DelegateDragPosition.Pages &&
dropPosition.page === root.pageNum &&
!dropDelegateIsWidget &&
folio.HomeScreenState.getPageDelegateAt(root.pageNum, dropPosition.pageRow, dropPosition.pageColumn) === null
x: dropPosition.pageColumn * folio.HomeScreenState.pageCellWidth
y: dropPosition.pageRow * folio.HomeScreenState.pageCellHeight
@ -145,7 +145,7 @@ Item {
property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget
// 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.page === root.pageNum &&
dropDelegateIsWidget &&
@ -180,7 +180,7 @@ Item {
dragState.candidateDropPosition.pageRow === delegate.pageDelegate.row &&
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.type === Folio.FolioDelegate.Application &&
isDropPositionThis
@ -274,7 +274,7 @@ Item {
name: folio.FolioSettings.showPagesAppLabels ? delegate.pageDelegate.application.name : ""
application: delegate.pageDelegate.application
turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
implicitWidth: folio.HomeScreenState.pageCellWidth
implicitHeight: folio.HomeScreenState.pageCellHeight
@ -323,10 +323,8 @@ Item {
Connections {
target: folio.HomeScreenState
function onSwipeStateChanged() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close();
}
function onDelegateDragStarted() {
contextMenu.close();
}
}
@ -409,10 +407,8 @@ Item {
Connections {
target: folio.HomeScreenState
function onSwipeStateChanged() {
if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
contextMenu.close();
}
function onDelegateDragStarted() {
contextMenu.close();
}
}

View file

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

View file

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

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,231 +19,123 @@ import org.kde.plasma.private.mobileshell as MobileShell
import '../delegate'
import '../private'
MouseArea {
Loader {
id: root
property Folio.HomeScreen folio
property var homeScreen
signal requestClose()
onClicked: root.requestClose()
active: false
function requestClose() {
active = false;
}
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
MobileShell.HapticsEffect {
id: haptics
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.7)
}
PlasmaExtras.ModelContextMenu {
id: getWidgetsDialog
visualParent: getWidgetsButton
placement: PlasmaExtras.Menu.TopPosedLeftAlignedPopup
// model set on first invocation
onClicked: model.trigger()
}
RowLayout {
id: header
spacing: Kirigami.Units.largeSpacing
anchors.left: parent.left
anchors.leftMargin: Kirigami.Units.gridUnit
anchors.top: parent.top
anchors.topMargin: Kirigami.Units.gridUnit * 3 + root.homeScreen.topMargin
anchors.right: parent.right
anchors.rightMargin: Kirigami.Units.gridUnit
PC3.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: 'go-previous'
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 2
padding: Kirigami.Units.smallSpacing
onClicked: root.requestClose()
}
PC3.Label {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
text: i18n("Widgets")
wrapMode: Text.Wrap
font.weight: Font.Bold
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5
Layout.fillWidth: true
}
PC3.ToolButton {
id: getWidgetsButton
icon.name: "get-hot-new-stuff"
text: i18ndc("plasma_shell_org.kde.plasma.mobile", "@action:button The word 'new' refers to widgets", "Get New Widgets…")
Accessible.name: i18ndc("plasma_shell_org.kde.plasma.mobile", "@action:button", "Get New Widgets…")
onClicked: {
getWidgetsDialog.model = widgetExplorer.widgetsMenuActions
getWidgetsDialog.openRelative()
}
}
}
GridView {
id: gridView
clip: true
reuseItems: true
opacity: 0 // we display with the opacity gradient below
anchors.top: header.bottom
anchors.topMargin: Kirigami.Units.gridUnit
anchors.left: parent.left
anchors.leftMargin: root.homeScreen.leftMargin
anchors.right: parent.right
anchors.rightMargin: root.homeScreen.rightMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: root.homeScreen.bottomMargin
model: widgetExplorer.widgetsModel
readonly property real maxCellWidth: Kirigami.Units.gridUnit * 20
readonly property real intendedCellWidth: Kirigami.Units.gridUnit * 8
readonly property int columns: Math.min(5, (width - leftMargin - rightMargin) / intendedCellWidth)
cellWidth: (width - leftMargin - rightMargin) / columns
cellHeight: cellWidth + Kirigami.Units.gridUnit * 3
readonly property real horizontalMargin: Math.round(width * 0.05)
leftMargin: horizontalMargin
rightMargin: horizontalMargin
MouseArea {
z: -1
sourceComponent: Item {
Rectangle {
anchors.fill: parent
onClicked: root.requestClose()
color: Qt.rgba(0, 0, 0, 0.7)
}
delegate: MouseArea {
id: delegate
width: gridView.cellWidth
height: gridView.cellHeight
PlasmaExtras.ModelContextMenu {
id: getWidgetsDialog
visualParent: getWidgetsButton
placement: PlasmaExtras.Menu.TopPosedLeftAlignedPopup
// model set on first invocation
onClicked: model.trigger()
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
RowLayout {
id: header
spacing: Kirigami.Units.largeSpacing
anchors.left: parent.left
anchors.leftMargin: Kirigami.Units.gridUnit
anchors.top: parent.top
anchors.topMargin: Kirigami.Units.gridUnit * 3 + root.homeScreen.topMargin
anchors.right: parent.right
anchors.rightMargin: Kirigami.Units.gridUnit
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
PC3.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: 'go-previous'
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 2
padding: Kirigami.Units.smallSpacing
onClicked: root.requestClose()
}
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
);
PC3.Label {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
text: i18n("Widgets")
wrapMode: Text.Wrap
font.weight: Font.Bold
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5
Layout.fillWidth: true
}
Rectangle {
id: background
color: Qt.rgba(255, 255, 255, 0.3)
visible: delegate.containsMouse
radius: Kirigami.Units.cornerRadius
anchors.fill: parent
}
PC3.ToolButton {
id: getWidgetsButton
icon.name: "get-hot-new-stuff"
text: i18ndc("plasma_shell_org.kde.plasma.mobile", "@action:button The word 'new' refers to widgets", "Get New Widgets…")
Accessible.name: i18ndc("plasma_shell_org.kde.plasma.mobile", "@action:button", "Get New Widgets…")
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
onClicked: {
getWidgetsDialog.model = widgetExplorer.widgetsMenuActions
getWidgetsDialog.openRelative()
}
}
}
}
// opacity gradient at grid edges
MobileShell.FlickableOpacityGradient {
anchors.fill: gridView
flickable: gridView
}
GridView {
id: gridView
clip: true
reuseItems: true
WidgetExplorer {
id: widgetExplorer
containment: Plasmoid
opacity: 0 // we display with the opacity gradient below
onShouldClose: root.requestClose()
anchors.top: header.bottom
anchors.topMargin: Kirigami.Units.gridUnit
anchors.left: parent.left
anchors.leftMargin: root.homeScreen.leftMargin
anchors.right: parent.right
anchors.rightMargin: root.homeScreen.rightMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: root.homeScreen.bottomMargin
model: widgetExplorer.widgetsModel
readonly property real intendedCellWidth: Kirigami.Units.gridUnit * 8
readonly property int columns: Math.min(5, (width - leftMargin - rightMargin) / intendedCellWidth)
cellWidth: (width - leftMargin - rightMargin) / columns
cellHeight: cellWidth + Kirigami.Units.gridUnit * 3
readonly property real horizontalMargin: Math.round(width * 0.05)
leftMargin: horizontalMargin
rightMargin: horizontalMargin
delegate: AppletListDelegate {
folio: root.folio
width: gridView.cellWidth
height: gridView.cellHeight
}
}
// opacity gradient at grid edges
MobileShell.FlickableOpacityGradient {
anchors.fill: gridView
flickable: gridView
}
WidgetExplorer {
id: widgetExplorer
containment: Plasmoid
onShouldClose: root.requestClose()
}
}
}

View file

@ -21,23 +21,12 @@ Item {
property var homeScreen
property real settingsModeHomeScreenScale
readonly property bool homeScreenInteractive: !appletListViewerLoader.active
readonly property bool homeScreenInteractive: !appletListViewer.active
property real bottomMargin: 0
property real leftMargin: 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 {
id: closeSettings
@ -84,7 +73,7 @@ Item {
iconName: 'widget-alternatives'
textLabel: i18n("Widgets")
onClicked: {
appletListViewerLoader.active = true;
appletListViewer.active = true;
}
}
}
@ -157,49 +146,13 @@ Item {
}
]
Loader {
id: appletListViewerLoader
asynchronous: true
active: false
signal requestClose()
onRequestClose: item?.requestClose()
AppletListViewer {
id: appletListViewer
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
folio: root.folio
width: parent.width
height: parent.height
onRequestClose: parent.active = false
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
folio: root.folio
homeScreen: root.homeScreen
}
}