/* * 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.Shell.topMargin - MobileShell.Shell.bottomMargin readonly property real windowWidth: taskSwitcher.width - MobileShell.Shell.leftMargin - MobileShell.Shell.rightMargin // 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 let duration = PlasmaCore.Units.longDuration * 2; if (currentlyBeingOpened) { animateGoToTaskIndex(currentTaskIndex, duration); } else { let currentTaskIndexPosition = xPositionFromTaskIndex(currentTaskIndex); 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 * 2 easing.type: Easing.OutBack 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(); } } }