shift-shell/kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml
Devin Lin 7cb6ebaae1 taskswitcher: Remove all interaction restrictions while the task switcher is opening
This allows users to properly interact (open apps, dismiss) while the task switcher is in its opening animation.

This fixes tapping on an app while the task switcher is opening dismissing the task switcher entirely (due to race condition)
2024-03-04 18:23:17 +00:00

252 lines
8.1 KiB
QML

// SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kwin 3.0 as KWinComponents
/**
* 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.
// Decreasing xPosition results in the task switcher moving forward (to the second task, third task, etc), being the layout direction Right to Left.
//
// 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: true // assume that task switcher is loaded on open
// whether the task switcher is being closed: an animation is running
property bool currentlyBeingClosed: false
// whether we are in a swipe left/right gesture to walk through tasks
property bool scrollingTasks: 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 - taskSwitcher.topMargin - taskSwitcher.bottomMargin
readonly property real windowWidth: taskSwitcher.width - taskSwitcher.leftMargin - taskSwitcher.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: Kirigami.Units.gridUnit
// height of the task preview header
readonly property real taskHeaderHeight: Kirigami.Units.gridUnit * 2 + Kirigami.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));
// animate scale only if we are *not* opening from the homescreen
if ((wasInActiveTask || !currentlyBeingOpened) && !scrollingTasks) {
return finalScale;
}
return scrollingTasks ? maxScale : 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, window) {
// cancel any opening animations ongoing
currentlyBeingOpened = false;
cancelAnimations();
animateGoToTaskIndex(index, Kirigami.Units.shortDuration);
openAppAnim.restart();
KWinComponents.Workspace.activeWindow = window
}
// 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) && !scrollingTasks) {
// open task switcher and stay
openAnim.restart();
} else {
// close task switcher and return to app
closeAnim.restart();
}
// update horizontal state
let duration = Kirigami.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: 300
easing.type: Easing.OutBack
onFinished: {
root.currentlyBeingOpened = false;
}
}
property var closeAnim: NumberAnimation {
target: root
property: "yPosition"
to: 0
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
onStarted: root.currentlyBeingClosed = true
onFinished: {
root.currentlyBeingClosed = false;
root.currentlyBeingOpened = false;
scrollingTasks = false;
taskSwitcher.instantHide();
}
}
property var openAppAnim: NumberAnimation {
target: root
property: "yPosition"
to: 0
duration: 300
easing.type: Easing.OutQuint
onStarted: root.currentlyBeingClosed = true
onFinished: {
root.currentlyBeingClosed = false;
root.currentlyBeingOpened = false;
taskSwitcher.instantHide();
}
}
}