/* * SPDX-FileCopyrightText: 2021 Devin Lin * * SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.15 import org.kde.plasma.core 2.1 as PlasmaCore import org.kde.plasma.private.mobileshell 1.0 as MobileShell /** * State object for the homescreen. * * We expose the data necessary to make custom "swipe-down" gestures from the page view. */ QtObject { id: root required property real totalPagesWidth required property var appDrawerFlickable // dimensions of the homescreen area (not including top panel and task panel) required property real availableScreenHeight required property real availableScreenWidth // offset from the bottom of the screen that the app drawer starts from, // would be the height favourites strip required property real appDrawerBottomOffset // ~~ positioning ~~ // xPosition: // We start at 0, which is the beginning x position of the row of pages (left-most side). // Increasing x moves *right* toward the next page. // // yPosition: // Increasing y results in moving *up* in the view. // appDrawerOpenYPosition - The app drawer is opened (app drawer flickable is active iff it's not at the beginning). // pagesYPosition - The app drawer is closed. Homescreen pages are visible, can swipe left/right between pages. property real xPosition: 0 property real yPosition: pagesYPosition // direction of the movement property bool movingRight: false property bool movingUp: false // used for calculating movement direction property real oldXPosition: 0 property real oldYPosition: 0 onXPositionChanged: { movingRight = xPosition > oldXPosition; oldXPosition = xPosition; } onYPositionChanged: { movingUp = yPosition > oldYPosition; oldYPosition = yPosition; } // yPosition when the homescreen pages are visible readonly property real pagesYPosition: availableScreenHeight - appDrawerBottomOffset // yPosition when drawer is open readonly property real appDrawerOpenYPosition: 0 // ~~ active state ~~ enum View { PageView, // we are viewing the horizontal row of pages AppDrawerBeginningView, // we are at the top of the app drawer (could either close it or scroll down) AppDrawerView // we are in the app drawer, and not at the top of it } // the current view of the homescreen property var currentView: HomeScreenState.PageView // number of homescreen pages readonly property int pagesCount: Math.floor(totalPagesWidth / pageWidth) // current homescreen page index readonly property int currentPageIndex: { let candidateIndex = Math.round(xPosition / (pageSpacing + pageWidth)); return Math.max(0, Math.min(pagesCount - 1, candidateIndex)); } enum PageViewSwipeState { SwipingPages, // horizontal movement between pages SwipingAppDrawerVisibility, // opening/closing app drawer SwipingAppDrawerList, // scrolling app drawer SwipingPagesDown, // custom gesture can be implemented for swiping down on the page view DeterminingType } // when we are at the PageView view, we need to distinguish horizontal swipes (changing pages) // and vertical swipes (opening drawer) property var currentSwipeState: HomeScreenState.DeterminingType // threshold of movement in a direction before we count that as the defining SwipeState readonly property real horizontalSwipeStateDetermineThreshold: PlasmaCore.Units.smallSpacing readonly property real verticalSwipeStateDetermineThreshold: PlasmaCore.Units.smallSpacing * 3 // we put the offset position here when determining the swipe type, before we // transfer movement over to xPosition and yPosition property real xDetermineSwipePosition: 0 property real yDetermineSwipePosition: 0 // whether animations are currently running property bool animationsRunning: openDrawerAnim.running || closeDrawerAnim.running || xAnim.running // whether the app drawer flickable should be interactive property bool appDrawerInteractive: currentView === HomeScreenState.AppDrawerView // ~~ measurement constants ~~ // dimensions of a page readonly property real pageHeight: availableScreenHeight readonly property real pageWidth: availableScreenWidth // spacing between each homescreen page readonly property real pageSpacing: 0 // ~~ signals and functions ~~ // cancel edit mode signal cancelEditModeForItemsRequested // cancel all animated moving, as another flick source is taking over signal cancelAnimations() onCancelAnimations: { openDrawerAnim.stop(); closeDrawerAnim.stop(); xAnim.stop(); } // expose signals necessary to implement any behaviour for the "swipe-down" action on the page view signal swipeDownGestureBegin signal swipeDownGestureEnd signal swipeDownGestureOffset(real value) // be very careful when resetting the swipe state // ensure that we aren't in the middle of a gesture function resetSwipeState() { currentSwipeState = HomeScreenState.DeterminingType; xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } function openAppDrawer() { openDrawerAnim.restart(); } function openAppDrawerInstantly() { yPosition = appDrawerOpenYPosition; currentView = HomeScreenState.AppDrawerBeginningView; } function closeAppDrawer() { closeDrawerAnim.restart(); } function closeAppDrawerInstantly() { yPosition = pagesYPosition; currentView = HomeScreenState.PageView; } // get the xPosition where the page will be centered on the screen function xPositionFromPageIndex(index) { return index * (pageWidth + pageSpacing); } // instantly go to the page index function goToPageIndex(index) { xPosition = xPositionFromPageIndex(index); } // go to the page index, animated function animateGoToPageIndex(index, duration) { xAnim.duration = duration; xAnim.to = xPositionFromPageIndex(index); xAnim.restart(); } // update the position using an offset // called by swipe provider flickable function updatePositionWithOffset(x, y) { switch (currentView) { case HomeScreenState.PageView: { switch (currentSwipeState) { case HomeScreenState.DeterminingType: xDetermineSwipePosition += x; yDetermineSwipePosition += y; // check if a swipetype can be determined and started if (Math.abs(xDetermineSwipePosition) >= horizontalSwipeStateDetermineThreshold) { currentSwipeState = HomeScreenState.SwipingPages; xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } else if (yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { currentSwipeState = HomeScreenState.SwipingPagesDown; root.swipeDownGestureBegin(); xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } else if (-yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { currentSwipeState = HomeScreenState.SwipingAppDrawerVisibility; xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; // reset app drawer position to top appDrawerFlickable.contentY = 0; } break; case HomeScreenState.SwipingPages: xPosition += x; break; case HomeScreenState.SwipingPagesDown: yPosition = pagesYPosition; if (y !== 0) { root.swipeDownGestureOffset(y); } break; case HomeScreenState.SwipingAppDrawerVisibility: yPosition = Math.max(appDrawerOpenYPosition, Math.min(pagesYPosition, yPosition + y)); break; } break; } case HomeScreenState.AppDrawerBeginningView: { switch (currentSwipeState) { case HomeScreenState.DeterminingType: xDetermineSwipePosition += x; yDetermineSwipePosition += y; // check if a swipetype can be determined and started if (yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { currentSwipeState = HomeScreenState.SwipingAppDrawerVisibility; xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } else if (-yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { currentSwipeState = HomeScreenState.SwipingAppDrawerList; yVelocityCalculator.startMeasure(appDrawerFlickable.contentY); xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } break; case HomeScreenState.SwipingAppDrawerVisibility: yPosition = Math.max(appDrawerOpenYPosition, Math.min(pagesYPosition, yPosition + y)); break; case HomeScreenState.SwipingAppDrawerList: // app drawer scrolling let candidateNewPos = appDrawerFlickable.contentY - y; appDrawerFlickable.contentY = candidateNewPos; // update velocity yVelocityCalculator.changePosition(appDrawerFlickable.contentY); break; } break; } case HomeScreenState.AppDrawerView: { break; } } } // called after a user finishes an interaction (ex. lets go of the screen) // called by swipe provider flickable function updateState() { cancelAnimations(); // we need to always call resetSwipeState() after each interaction. // if we have an animation to run, we rely on the animation to call the function. // otherwise, we do it directly here. switch (currentView) { case HomeScreenState.PageView: { // update vertical position switch (currentSwipeState) { case HomeScreenState.DeterminingType: { movingUp ? closeAppDrawer() : openAppDrawer(); break; } case HomeScreenState.SwipingPagesDown: { root.swipeDownGestureEnd(); root.resetSwipeState(); break; } case HomeScreenState.SwipingAppDrawerVisibility: { movingUp ? closeAppDrawer() : openAppDrawer(); break; } case HomeScreenState.SwipingPages: { // update pages position let currentPageIndexPosition = xPositionFromPageIndex(currentPageIndex); let duration = PlasmaCore.Units.longDuration * 2; if (xPosition < currentPageIndexPosition) { if (movingRight) { animateGoToPageIndex(currentPageIndex, duration); } else { animateGoToPageIndex(Math.max(0, currentPageIndex - 1), duration); } } else { if (movingRight) { animateGoToPageIndex(Math.min(pagesCount - 1, currentPageIndex + 1), duration); } else { animateGoToPageIndex(currentPageIndex, duration); } } break; } default: { // this shouldn't occur, but keeps consistent state if it does root.resetSwipeState(); break; } } break; } case HomeScreenState.AppDrawerBeginningView: { switch (currentSwipeState) { case HomeScreenState.DeterminingType: case HomeScreenState.SwipingAppDrawerVisibility: { movingUp ? closeAppDrawer() : openAppDrawer(); break; } case HomeScreenState.SwipingAppDrawerList: { currentView = HomeScreenState.AppDrawerView; appDrawerFlickable.flick(0, -yVelocityCalculator.velocity); root.resetSwipeState(); break; } default: { // this shouldn't occur, but keeps consistent state if it does root.resetSwipeState(); break; } } break; } case HomeScreenState.AppDrawerView: { break; } } } // measure velocity of our swipe in the app drawer, so that we can flick property var yVelocityCalculator: MobileShell.VelocityCalculator {} // listen to the app drawer's flickable for if it goes to the top of the list // we then update our view state property var appDrawerFlickableListener: Connections { target: appDrawerFlickable function onMovementEnded() { if (root.currentView === HomeScreenState.AppDrawerView) { if (appDrawerFlickable.contentY <= 0) { root.currentView = HomeScreenState.AppDrawerBeginningView; } } } function onDraggingChanged() { if (!appDrawerFlickable.dragging) { if (root.currentView === HomeScreenState.AppDrawerView) { if (appDrawerFlickable.contentY <= 0) { root.currentView = HomeScreenState.AppDrawerBeginningView; } } } } } // ~~ property animators ~~ property var xAnim: NumberAnimation { target: root property: "xPosition" easing.type: Easing.OutBack onFinished: { root.resetSwipeState(); } } property var openDrawerAnim: NumberAnimation { target: root property: "yPosition" to: appDrawerOpenYPosition duration: PlasmaCore.Units.longDuration * 2 easing.type: Easing.OutCubic onFinished: { root.currentView = HomeScreenState.AppDrawerBeginningView; root.resetSwipeState(); } } property var closeDrawerAnim: NumberAnimation { target: root property: "yPosition" to: pagesYPosition duration: PlasmaCore.Units.longDuration * 2 easing.type: Easing.OutCubic onFinished: { root.currentView = HomeScreenState.PageView; root.resetSwipeState(); } } }