From 1c81cd34b48945fba1169f524dfacf33acd0566b Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Mon, 27 Dec 2021 01:32:24 -0500 Subject: [PATCH] taskswitcher: Rewrite without listview and extract state to TaskSwitcherState --- .../qml/FlickablePages.qml | 10 +- .../qml/navigationpanel/NavigationPanel.qml | 27 +- .../qml/taskswitcher/FlickContainer.qml | 88 ++++++ .../mobileshell/qml/taskswitcher/Task.qml | 21 +- .../mobileshell/qml/taskswitcher/TaskList.qml | 91 +++++++ .../qml/taskswitcher/TaskSwitcher.qml | 252 ++++++------------ .../qml/taskswitcher/TaskSwitcherState.qml | 239 +++++++++++++++++ .../taskpanel/package/contents/ui/main.qml | 6 +- 8 files changed, 542 insertions(+), 192 deletions(-) create mode 100644 components/mobileshell/qml/taskswitcher/FlickContainer.qml create mode 100644 components/mobileshell/qml/taskswitcher/TaskList.qml create mode 100644 components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml diff --git a/components/mobilehomescreencomponents/qml/FlickablePages.qml b/components/mobilehomescreencomponents/qml/FlickablePages.qml index 7ca888e6..84b337ea 100644 --- a/components/mobilehomescreencomponents/qml/FlickablePages.qml +++ b/components/mobilehomescreencomponents/qml/FlickablePages.qml @@ -53,10 +53,12 @@ Flickable { onContentXChanged: mainFlickable.currentIndex = Math.floor(contentX / width) onFooterChanged: { - footer.parent = mainFlickable; - footer.anchors.left = mainFlickable.left; - footer.anchors.bottom = mainFlickable.bottom; - footer.anchors.right = mainFlickable.right; + if (footer) { + footer.parent = mainFlickable; + footer.anchors.left = mainFlickable.left; + footer.anchors.bottom = mainFlickable.bottom; + footer.anchors.right = mainFlickable.right; + } } //Autoscroll related functions diff --git a/components/mobileshell/qml/navigationpanel/NavigationPanel.qml b/components/mobileshell/qml/navigationpanel/NavigationPanel.qml index f993bdb6..7d55d48a 100644 --- a/components/mobileshell/qml/navigationpanel/NavigationPanel.qml +++ b/components/mobileshell/qml/navigationpanel/NavigationPanel.qml @@ -69,20 +69,24 @@ Item { } if (root.dragGestureEnabled) { - if (!taskSwitcher.currentlyDragging && Math.abs(startMouseY - oldMouseY) < root.height) { + if (!opening && Math.abs(startMouseY - oldMouseY) < root.height) { oldMouseY = mouse.y; return; } else if (mouseArea.pressed) { - taskSwitcher.currentlyDragging = true; + opening = true; } - // update offsets with drags - root.taskSwitcher.oldYOffset = root.taskSwitcher.yOffset; - root.taskSwitcher.yOffset = Math.max(0, root.taskSwitcher.yOffset - (mouse.y - oldMouseY)); - - opening = oldMouseY > mouse.y; + if (root.taskSwitcher.visible) { + // update task switcher drag + let offsetY = (mouse.y - oldMouseY) * 0.5; // we want to make the gesture take a longer swipe than it being pixel perfect + let offsetX = (mouse.x - oldMouseX) * 0.5; + taskSwitcher.taskSwitcherState.yPosition = Math.max(0, taskSwitcher.taskSwitcherState.yPosition - offsetY); + + // TODO add x swipe + //taskSwitcher.taskSwitcherState.xPosition -= offsetX; + } - if (!root.taskSwitcher.visible && Math.abs(startMouseY - mouse.y) > PlasmaCore.Units.gridUnit && root.taskSwitcher.tasksCount) { + if (!root.taskSwitcher.visible && Math.abs(startMouseY - mouse.y) > PlasmaCore.Units.gridUnit && taskSwitcher.tasksCount) { // start task switcher gesture activeButton = null; root.taskSwitcher.show(false); @@ -98,11 +102,8 @@ Item { onReleased: { if (activeButton) { activeButton.clicked(); - } - - if (root.dragGestureEnabled && root.taskSwitcher.currentlyDragging) { - root.taskSwitcher.currentlyDragging = false; - root.taskSwitcher.snapOffset(); + } else if (root.dragGestureEnabled && taskSwitcher.taskSwitcherState.currentlyBeingOpened) { + taskSwitcher.taskSwitcherState.updateState(); } } diff --git a/components/mobileshell/qml/taskswitcher/FlickContainer.qml b/components/mobileshell/qml/taskswitcher/FlickContainer.qml new file mode 100644 index 00000000..372c1669 --- /dev/null +++ b/components/mobileshell/qml/taskswitcher/FlickContainer.qml @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.taskmanager 0.1 as TaskManager +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.nanoshell 2.0 as NanoShell +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +Flickable { + id: root + + required property var taskSwitcherState + + // we use flickable solely for capturing flicks, not positioning elements + contentWidth: width + 99999 + contentHeight: height + contentX: startContentX + + readonly property real startContentX: contentWidth / 2 + + property bool positionChangedDueToFlickable: false + + // ensure that flickable is not moving when other sources are changing position + Connections { + target: root.taskSwitcherState + + onXPositionChanged: { + if (!root.positionChangedDueToFlickable) { + root.cancelMovement(); + } + root.positionChangedDueToFlickable = true; + } + onYPositionChanged: { + if (!root.positionChangedDueToFlickable) { + root.cancelMovement(); + } + root.positionChangedDueToFlickable = true; + } + } + + // update position from horizontal flickable movement + property real oldContentX + onContentXChanged: { + positionChangedDueToFlickable = true; + taskSwitcherState.xPosition += contentX - oldContentX; + oldContentX = contentX; + } + + onMovementStarted: taskSwitcherState.cancelAnimations(); + onMovementEnded: { + resetPosition(); + taskSwitcherState.updateState(); + } + onFlickEnded: { + resetPosition(); + taskSwitcherState.updateState(); + } + + onDraggingChanged: { + if (!dragging) { + cancelMovement(); + resetPosition(); + taskSwitcherState.updateState(); + } else { + taskSwitcherState.cancelAnimations(); + } + } + + function cancelMovement() { + root.cancelFlick(); + + // HACK: cancelFlick() doesn't seem to cancel flicks... + root.flick(-horizontalVelocity, 0); + } + + function resetPosition() { + positionChangedDueToFlickable = true; + oldContentX = startContentX; + contentX = startContentX; + } +} diff --git a/components/mobileshell/qml/taskswitcher/Task.qml b/components/mobileshell/qml/taskswitcher/Task.qml index daa8b392..d1f67ae9 100644 --- a/components/mobileshell/qml/taskswitcher/Task.qml +++ b/components/mobileshell/qml/taskswitcher/Task.qml @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2015 Marco Martin * SPDX-FileCopyrightText: 2021 Devin Lin * - * SPDX-License-Identifier: LGPL-2.0-or-later + * SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.15 @@ -22,20 +22,23 @@ Item { required property var model required property var displaysModel - readonly property point taskScreenPoint: Qt.point(model.ScreenGeometry.x, model.ScreenGeometry.y) + readonly property point taskScreenPoint: model ? Qt.point(model.ScreenGeometry.x, model.ScreenGeometry.y) : Qt.point(0, 0) readonly property real dragOffset: -control.y - property bool active: model.IsActive + property bool active: model ? model.IsActive : false required property real previewHeight required property real previewWidth + + property bool showHeader: true + property real scale: 1 opacity: 1 - dragOffset / taskSwitcher.height //BEGIN functions function syncDelegateGeometry() { - let pos = pipeWireLoader.mapToItem(tasksView, 0, 0); + let pos = pipeWireLoader.mapToItem(delegate, 0, 0); if (taskSwitcher.visible) { tasksModel.requestPublishDelegateGeometry(tasksModel.index(model.index, 0), Qt.rect(pos.x, pos.y, pipeWireLoader.width, pipeWireLoader.height), pipeWireLoader); } @@ -58,7 +61,7 @@ Item { syncDelegateGeometry(); } } - + QQC2.Control { id: control width: parent.width @@ -113,6 +116,11 @@ Item { Layout.fillHeight: true Layout.minimumHeight: column.height - appView.height spacing: PlasmaCore.Units.smallSpacing * 2 + opacity: delegate.showHeader ? 1 : 0 + + Behavior on opacity { + NumberAnimation { duration: PlasmaCore.Units.shortDuration } + } PlasmaCore.IconItem { Layout.preferredHeight: PlasmaCore.Units.iconSizes.smallMedium @@ -164,6 +172,9 @@ Item { Layout.maximumWidth: delegate.previewWidth Layout.maximumHeight: delegate.previewHeight + // prevent thumbnails from "leaking" out of the control + clip: true + leftPadding: 0 rightPadding: 0 topPadding: 0 diff --git a/components/mobileshell/qml/taskswitcher/TaskList.qml b/components/mobileshell/qml/taskswitcher/TaskList.qml new file mode 100644 index 00000000..e7102519 --- /dev/null +++ b/components/mobileshell/qml/taskswitcher/TaskList.qml @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.1 + +import org.kde.taskmanager 0.1 as TaskManager +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.nanoshell 2.0 as NanoShell +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +Item { + id: root + + required property var taskSwitcher + readonly property var taskSwitcherState: taskSwitcher.taskSwitcherState + + opacity: taskSwitcherState.wasInActiveTask ? 1 : Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition) + + // taphandler activates even if delegate touched + TapHandler { + onPressedChanged: { + if (pressed) { + // ensure animations aren't running when finger is pressed + taskSwitcherState.cancelAnimations(); + } + } + } + + Repeater { + id: repeater + model: taskSwitcher.model + + // left margin from root edge such that the task is centered + readonly property real leftMargin: (root.width / 2) - (taskSwitcherState.taskWidth / 2) + + delegate: Task { + id: task + + readonly property int currentIndex: model.index + + // this is the x-position with respect to the list + property real listX: taskSwitcherState.xPositionFromTaskIndex(currentIndex); + + Behavior on listX { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + // this is the actual displayed x-position on screen + x: listX + repeater.leftMargin - taskSwitcherState.xPosition + + // account for system header and footer offset (center the preview image) + y: { + let headerHeight = MobileShell.TopPanelControls.panelHeight; + let footerHeight = MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0; + let diff = headerHeight - footerHeight; + + let baseY = (taskSwitcher.height / 2) - (height / 2) - (taskSwitcherState.taskHeaderHeight / 2) + + return baseY + diff / 2 - MobileShell.TopPanelControls.panelHeight; + } + + // ensure current task is above others + z: taskSwitcherState.currentTaskIndex === currentIndex ? 1 : 0 + + showHeader: currentIndex !== taskSwitcherState.currentTaskIndex || !taskSwitcherState.currentlyBeingOpened + + width: taskSwitcherState.taskWidth + height: taskSwitcherState.taskHeight + previewWidth: taskSwitcherState.previewWidth + previewHeight: taskSwitcherState.previewHeight + + taskSwitcher: root.taskSwitcher + displaysModel: root.taskSwitcher.displaysModel + + scale: { + if (taskSwitcherState.currentTaskIndex == currentIndex) { + return taskSwitcherState.currentScale; + } + return 1; + } + } + } +} diff --git a/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml b/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml index 0a45fc56..9a368225 100644 --- a/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml +++ b/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml @@ -2,12 +2,12 @@ * SPDX-FileCopyrightText: 2015 Marco Martin * SPDX-FileCopyrightText: 2021 Devin Lin * - * SPDX-License-Identifier: LGPL-2.0-or-later + * SPDX-License-Identifier: GPL-2.0-or-later */ -import QtQuick 2.12 +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.1 -import QtQuick.Window 2.2 import org.kde.taskmanager 0.1 as TaskManager import org.kde.plasma.core 2.1 as PlasmaCore @@ -15,109 +15,82 @@ import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import "../components" as Components + Item { id: root visible: false + opacity: 0 - readonly property real taskPanelHeight: MobileShell.TaskPanelControls.panelHeight - readonly property real taskPanelWidth: MobileShell.TaskPanelControls.panelWidth - readonly property bool isPortrait: MobileShell.TaskPanelControls.isPortrait + // state object + property var taskSwitcherState: TaskSwitcherState { + taskSwitcher: root + } - // dimensions of a window on the screen - readonly property real windowHeight: root.height - (root.isPortrait ? root.taskPanelHeight : 0) - MobileShell.TopPanelControls.panelHeight - readonly property real windowWidth: root.width - (root.isPortrait ? 0 : root.taskPanelWidth) - - readonly property int tasksCount: root.model.count - readonly property int currentTaskIndex: tasksView.currentIndex + // task list model property TaskManager.TasksModel model - - // offset constants - readonly property real targetYOffsetDist: root.height - tasksView.height // offset distance to perfect opening - readonly property real dismissYOffsetDist: root.height - - // properties controlled from NavigationPanel (swipe to open gesture) - property real oldYOffset: 0 - property real yOffset: 0 - - // set from NavigationPanel in taskpanel containment - property bool wasInActiveTask: false // whether we were in an app before opening the task switcher - property bool currentlyDragging: false // whether we are in a swipe up gesture + readonly property int tasksCount: model.count property var displaysModel: MobileShell.DisplaysModel {} - enum MovementDirection { - None = 0, - Left, - Right - } - onVisibleChanged: MobileShell.HomeScreenControls.taskSwitcherVisible = visible; - - onTasksCountChanged: { + + property int oldTasksCount: tasksCount + onTasksCountChanged: { if (tasksCount == 0) { hide(); + } else if (tasksCount < oldTasksCount && taskSwitcherState.currentTaskIndex >= tasksCount - 1) { + // if the user is on the last task, and it is closed, scroll left + taskSwitcherState.animateGoToTaskIndex(tasksCount - 1, PlasmaCore.Units.longDuration); } - } - - Rectangle { - id: backgroundRect - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.6 * (root.wasInActiveTask ? 1 : Math.min(1, root.yOffset / root.targetYOffsetDist))) - MouseArea { - anchors.fill: parent - onClicked: root.hide() - } + oldTasksCount = tasksCount; } + // TODO close task switcher when an app opens while it is open, otherwise the navbar becomes glitched + // TODO click outside of delegate to close + //BEGIN functions function show(animation) { - root.yOffset = 0; - root.wasInActiveTask = root.model.activeTask.row >= 0; + // reset values + taskSwitcherState.cancelAnimations(); + taskSwitcherState.yPosition = 0; + taskSwitcherState.xPosition = 0; + taskSwitcherState.wasInActiveTask = root.model.activeTask.row >= 0; + taskSwitcherState.currentlyBeingOpened = true; // skip to first active task - if (root.wasInActiveTask) { - tasksView.currentIndex = root.model.activeTask.row; - tasksView.positionViewAtIndex(root.model.activeTask.row, ListView.SnapPosition); + if (taskSwitcherState.wasInActiveTask) { + taskSwitcherState.goToTaskIndex(root.model.activeTask.row); } - root.visible = true; + // show task switcher, hide all running apps + visible = true; + opacity = 1; minimizeAll(); - // animate app shrink + // fully open the panel (if this is a button press, not gesture) if (animation) { - offsetAnimator.to = root.targetYOffsetDist; - offsetAnimator.restart(); + taskSwitcherState.open(); } } + + function instantHide() { + opacity = 0; + visible = false; + } + function hide() { - if (!root.visible) return; - root.visible = false; - } - - function snapOffset() { - let movingUp = root.yOffset > root.oldYOffset; - - if (movingUp || root.yOffset >= root.targetYOffsetDist) { // open task switcher and stay - offsetAnimator.to = root.targetYOffsetDist; - offsetAnimator.restart(); - } else { // close task switcher and return to app - if (!root.wasInActiveTask) { // if pulled up from homescreen, don't activate app - offsetAnimator.activateApp = false; - } - offsetAnimator.to = 0; - offsetAnimator.restart(); - } + closeAnim.restart(); } // scroll to delegate index, and activate it function activateWindow(id) { - offsetAnimator.to = 0; - offsetAnimator.restart(); + taskSwitcherState.openApp(id); } - function setSingleActiveWindow(id, delegate) { + function setSingleActiveWindow(id) { if (id < 0) { return; } @@ -130,14 +103,14 @@ Item { root.model.requestActivate(idx); } else if (!tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized)) { var geo = tasksModel.data(idx, TaskManager.AbstractTasksModel.ScreenGeometry) - // Only minimize the other windows in the same screen + // only minimize the other windows in the same screen if (geo === newActiveGeo) { tasksModel.requestToggleMinimized(idx); } } } - root.visible = false; + instantHide(); } function minimizeAll() { @@ -149,40 +122,29 @@ Item { } } - function restoreAll() { - for (var i = 0 ; i < tasksModel.count; i++) { - var idx = tasksModel.makeModelIndex(i); - if (tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized)) { - tasksModel.requestToggleMinimized(idx); - } +//END functions + + NumberAnimation on opacity { + id: closeAnim + to: 0 + duration: PlasmaCore.Units.shortDuration + easing.type: Easing.InOutQuad + onFinished: { + root.visible = false; } } - -//END functions - - // animate app grow and shrink - NumberAnimation on yOffset { - id: offsetAnimator - duration: PlasmaCore.Units.longDuration - easing.type: Easing.InOutQuad + + // background colour + Rectangle { + id: backgroundRect + anchors.fill: parent - property bool activateApp: true - - // states of to: - // 0 - open/resume app (zoom up the thumbnail) - // root.targetYOffsetDist - animate shrinking of thumbnail, to listview (open task switcher) - to: 0 - onFinished: { - if (to === 0) { // close task switcher, and switch to current app - if (!root.visible) return; - root.visible = false; - - if (activateApp) { - setSingleActiveWindow(root.currentTaskIndex); - } - activateApp = true; - } else if (to == root.dismissYOffsetDist) { - root.hide(); + color: { + // animate background colour only if opening from the homescreen + if (taskSwitcherState.wasInActiveTask) { + return Qt.rgba(0, 0, 0, 0.6); + } else { + return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition)); } } } @@ -192,74 +154,30 @@ Item { // provide shell margins anchors.fill: parent - anchors.rightMargin: root.isPortrait ? 0 : root.taskPanelWidth - anchors.bottomMargin: root.isPortrait ? root.taskPanelHeight : 0 + anchors.rightMargin: MobileShell.TaskPanelControls.isPortrait ? 0 : MobileShell.TaskPanelControls.panelWidth + anchors.bottomMargin: MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0 anchors.topMargin: MobileShell.TopPanelControls.panelHeight - // applications list - ListView { - id: tasksView - opacity: root.wasInActiveTask ? 1 : Math.min(1, root.yOffset / root.targetYOffsetDist) - anchors.centerIn: parent + FlickContainer { + id: flickable + anchors.fill: parent + taskSwitcherState: root.taskSwitcherState - readonly property real sizeFactor: 0.75 - readonly property real taskHeaderHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing * 2 - - width: root.windowWidth * sizeFactor - height: root.windowHeight * sizeFactor + taskHeaderHeight - - model: root.model - orientation: ListView.Horizontal - - highlightRangeMode: ListView.StrictlyEnforceRange // ensures currentIndex is updated - snapMode: ListView.SnapToItem - - spacing: PlasmaCore.Units.largeSpacing - displayMarginBeginning: 2 * (width + spacing) - displayMarginEnd: 2 * (width + spacing) - displaced: Transition { - NumberAnimation { properties: "x,y"; duration: PlasmaCore.Units.longDuration; easing.type: Easing.InOutQuad } - } - - // ensure that window previews are exactly to the scale of the device screen - property real scalingFactor: { - let candidateHeight = (tasksView.width / root.windowWidth) * root.windowHeight; - if (candidateHeight > tasksView.height) { - return tasksView.height / root.windowHeight; - } else { - return tasksView.width / root.windowWidth; + // the item is effectively anchored to the flickable bounds + QQC2.Control { + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + x: flickable.contentX + width: flickable.width + height: flickable.height + + contentItem: TaskList { + taskSwitcher: root } } - - delegate: Task { - id: task - property int curIndex: model.index - z: root.currentTaskIndex === curIndex ? 1 : 0 - width: tasksView.width - height: tasksView.height - - taskSwitcher: root - displaysModel: root.displaysModel - - // account for header offset (center the preview) - y: -tasksView.taskHeaderHeight / 2 - - // scale gesture - scale: { - let maxScale = 1 / tasksView.scalingFactor; - let subtract = (maxScale - 1) * (root.yOffset / root.targetYOffsetDist); - let finalScale = Math.max(0, Math.min(maxScale, maxScale - subtract)); - - if ((root.wasInActiveTask || !taskSwitcher.currentlyDragging) && root.currentTaskIndex === task.curIndex) { - return finalScale; - } - return 1; - } - - // ensure that window previews are exactly to the scale of the device screen - previewWidth: tasksView.scalingFactor * root.windowWidth - previewHeight: tasksView.scalingFactor * root.windowHeight - } } } } diff --git a/components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml b/components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml new file mode 100644 index 00000000..a8fdb1f0 --- /dev/null +++ b/components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml @@ -0,0 +1,239 @@ +/* + * 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 task switcher. + */ +QtObject { + id: root + + // TaskSwitcher item component + // We assume that the taskSwitcher the size of the entire screen. + required property var taskSwitcher + + + // ~~ positioning ~~ + + // Position of the list view: + // + // xPosition: + // We start at 0, which is the position at which the first task in the task switcher is centered on the screen. + // Increasing xPosition results in the task switcher moving forward (to the second task, third task, etc). + // + // yPosition: + // 0 - Start of swipe up gesture, if window was showing, the thumbnail is the size of it + // Increasing yPosition results in the task switcher moving up (and thumbnails shrinking) + property real xPosition: 0 + property real yPosition: 0 + + // 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 task switcher is completely open + readonly property real openedYPosition: (taskSwitcher.height - taskHeight) / 2 + + // ~~ active state ~~ + + // whether the user was in an active task before the task switcher was opened + property bool wasInActiveTask: false + + // whether we are in a swipe up gesture to open the task switcher + property bool currentlyBeingOpened: false + + readonly property int currentTaskIndex: { + let candidateIndex = Math.round(xPosition / (taskSpacing + taskWidth)); + return Math.max(0, Math.min(taskSwitcher.tasksCount - 1, candidateIndex)); + } + + // ~~ measurement constants ~~ + + // dimensions of a real window on the screen + readonly property real windowHeight: taskSwitcher.height - (MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0) - MobileShell.TopPanelControls.panelHeight + readonly property real windowWidth: taskSwitcher.width - (MobileShell.TaskPanelControls.isPortrait ? 0 : MobileShell.TaskPanelControls.panelWidth) + + // dimensions of the task previews + readonly property real previewHeight: windowHeight * scalingFactor + readonly property real previewWidth: windowWidth * scalingFactor + readonly property real taskHeight: previewHeight + taskHeaderHeight + readonly property real taskWidth: previewWidth + + // spacing between each task preview + readonly property real taskSpacing: PlasmaCore.Units.largeSpacing + + // height of the task preview header + readonly property real taskHeaderHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing * 2 + + // the scaling factor of the window preview compared to the actual window + // we need to ensure that window previews always fit on screen + readonly property real scalingFactor: { + let candidateFactor = 0.6; + let candidateTaskHeight = windowHeight * candidateFactor + taskHeaderHeight; + let candidateTaskWidth = windowWidth * candidateFactor; + + let candidateHeight = (candidateTaskWidth / windowWidth) * windowHeight; + if (candidateHeight > windowHeight) { + return candidateTaskHeight / windowHeight; + } else { + return candidateTaskWidth / windowWidth; + } + } + + // scale of the task list (based on the progress of the swipe up gesture) + readonly property real currentScale: { + let maxScale = 1 / scalingFactor; + let subtract = (maxScale - 1) * (yPosition / openedYPosition); + let finalScale = Math.max(0, Math.min(maxScale, maxScale - subtract)); + + if (wasInActiveTask || !currentlyBeingOpened) { + return finalScale; + } + return 1; + } + + // ~~ signals and functions ~~ + + // cancel all animated moving, as another flick source is taking over + signal cancelAnimations() + onCancelAnimations: { + openAnim.stop(); + openAppAnim.stop(); + closeAnim.stop(); + xAnim.stop(); + } + + function open() { + openAnim.restart(); + } + + function close() { + closeAnim.restart(); + } + + function openApp(index) { + animateGoToTaskIndex(index, PlasmaCore.Units.shortDuration); + openAppAnim.restart(); + } + + // get the xPosition where the task will be centered on the screen + function xPositionFromTaskIndex(index) { + return index * (taskWidth + taskSpacing); + } + + // instantly go to the task index + function goToTaskIndex(index) { + xPosition = xPositionFromTaskIndex(index); + } + + // go to the task index, animated + function animateGoToTaskIndex(index, duration) { + xAnim.duration = duration; + xAnim.to = xPositionFromTaskIndex(index); + xAnim.restart(); + } + + // called after a user finishes an interaction (ex. lets go of the screen) + function updateState() { + cancelAnimations(); + + // update vertical state + if (movingUp || root.yPosition >= openedYPosition) { + // open task switcher and stay + openAnim.restart(); + } else { + // close task switcher and return to app + closeAnim.restart(); + } + + // update horizontal state + if (!currentlyBeingOpened) { + let currentTaskIndexPosition = xPositionFromTaskIndex(currentTaskIndex); + let duration = PlasmaCore.Units.longDuration * 2; + if (xPosition < currentTaskIndexPosition) { + if (movingRight) { + animateGoToTaskIndex(currentTaskIndex, duration); + } else { + animateGoToTaskIndex(Math.max(0, currentTaskIndex - 1), duration); + } + } else { + if (movingRight) { + animateGoToTaskIndex(Math.min(taskSwitcher.tasksCount - 1, currentTaskIndex + 1), duration); + } else { + animateGoToTaskIndex(currentTaskIndex, duration); + } + } + } + } + + // ~~ property animators ~~ + + property var xAnim: NumberAnimation { + target: root + property: "xPosition" + easing.type: Easing.OutBack + } + + property var openAnim: NumberAnimation { + target: root + property: "yPosition" + to: openedYPosition + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + + onFinished: { + root.currentlyBeingOpened = false; + } + } + + property var closeAnim: NumberAnimation { + target: root + property: "yPosition" + to: 0 + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + + onFinished: { + root.currentlyBeingOpened = false; + taskSwitcher.instantHide(); + + if (root.wasInActiveTask) { + taskSwitcher.setSingleActiveWindow(root.currentTaskIndex); + } + } + } + + property var openAppAnim: NumberAnimation { + target: root + property: "yPosition" + to: 0 + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + + onFinished: { + root.currentlyBeingOpened = false; + taskSwitcher.setSingleActiveWindow(root.currentTaskIndex); + taskSwitcher.instantHide(); + } + } +} diff --git a/containments/taskpanel/package/contents/ui/main.qml b/containments/taskpanel/package/contents/ui/main.qml index be65f88f..3d18a7df 100644 --- a/containments/taskpanel/package/contents/ui/main.qml +++ b/containments/taskpanel/package/contents/ui/main.qml @@ -102,9 +102,9 @@ PlasmaCore.ColorScope { taskSwitcher.show(true); } else { // when task switcher is open - if (taskSwitcher.wasInActiveTask) { + if (taskSwitcher.taskSwitcherState.wasInActiveTask) { // restore active window - taskSwitcher.activateWindow(root.currentTaskIndex); + taskSwitcher.activateWindow(taskSwitcher.taskSwitcherState.currentTaskIndex); } else { taskSwitcher.hide(); } @@ -169,7 +169,7 @@ PlasmaCore.ColorScope { // do not enable drag gesture when task switcher is already open // also don't disable drag gesture mid-drag - dragGestureEnabled: !taskSwitcher.visible || taskSwitcher.currentlyDragging + dragGestureEnabled: !taskSwitcher.visible || taskSwitcher.taskSwitcherState.currentlyBeingOpened leftAction: taskSwitcherAction middleAction: homeAction