mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
haptics are triggered when conditions are met to open task switcher or task scrub mode is engaged. fixes related bug in task switcher gesture logic when invoked from homescreen fixes https://invent.kde.org/plasma/plasma-mobile/-/issues/366
276 lines
9.8 KiB
QML
276 lines
9.8 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
|
|
property var state: taskSwitcher.state
|
|
required property var stateClass
|
|
|
|
|
|
// direction of the movement
|
|
readonly property bool gestureMovingRight: state.xVelocity > 0
|
|
readonly property bool gestureMovingUp: state.yVelocity < 0
|
|
|
|
readonly property bool currentlyBeingOpened: state.gestureInProgress || openAnim.running
|
|
readonly property bool currentlyBeingClosed: closeAnim.running || openAppAnim.running
|
|
|
|
// yPosition when the task switcher is completely open
|
|
readonly property real openedYPosition: (taskSwitcher.height - taskHeight) / 2
|
|
|
|
// yPosition threshold below which opening the task switcher should be undone and returned to the previously active task
|
|
readonly property real undoYThreshold: openedYPosition / 2
|
|
|
|
// whether the switcher has already triggered haptic feedback or not
|
|
// we don't want to continuously send haptics, just once is enough
|
|
property bool hasVibrated: false
|
|
|
|
// ~~ 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 previewAspectRatio: previewWidth / previewHeight
|
|
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;
|
|
}
|
|
}
|
|
|
|
// scaling factor during closing of the switcher
|
|
property real closingScalingFactor: 1
|
|
|
|
// 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) * Math.min(root.state.yPosition / openedYPosition, 1);
|
|
let finalScale = Math.min(maxScale, maxScale - subtract);
|
|
|
|
// if closing scaling factor is below 1 we want it to override the other scale
|
|
// to allow for a smoother closing animation
|
|
if (closingScalingFactor < 1) {
|
|
return closingScalingFactor;
|
|
}
|
|
|
|
// animate scale only if we are *not* opening from the homescreen
|
|
if (root.state.wasInActiveTask || !root.state.gestureInProgress) {
|
|
return finalScale;
|
|
}
|
|
return 1;
|
|
}
|
|
readonly property bool isScaleClamped: root.state.yPosition > openedYPosition
|
|
|
|
readonly property real taskScrubDistance: windowWidth / (2 * 6) // formula says how many tasks can be scrubbed through in half of the window width
|
|
property bool isInTaskScrubMode: false
|
|
|
|
// ~~ signals and functions ~~
|
|
|
|
// cancel all animated moving, as another flick source is taking over
|
|
signal cancelAnimations()
|
|
onCancelAnimations: {
|
|
openAnim.stop();
|
|
openAppAnim.stop();
|
|
closeAnim.stop();
|
|
closeScaleAnim.stop();
|
|
xAnim.stop();
|
|
}
|
|
|
|
function getTaskIndexFromWindow(window) {
|
|
for (let i = 0; i < taskSwitcher.tasksModel.rowCount(); i++) {
|
|
const modelWindow = taskSwitcher.tasksModel.data(taskSwitcher.tasksModel.index(i, 0), Qt.DisplayRole);
|
|
if (modelWindow == window) {
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getTaskIndexFromXPosition() {
|
|
let candidateIndex = Math.round(-root.state.xPosition / (taskSpacing + taskWidth));
|
|
return Math.max(0, Math.min(taskSwitcher.tasksCount - 1, candidateIndex));
|
|
}
|
|
|
|
// TODO either use updateTaskIndex to always have the "newest current task index" in the state var or use "getNearestTaskIndex", not both it's redundant
|
|
function updateTaskIndex() {
|
|
root.state.currentTaskIndex = getTaskIndexFromXPosition();
|
|
}
|
|
|
|
function open() {
|
|
openAnim.restart();
|
|
}
|
|
|
|
function close() {
|
|
cancelAnimations();
|
|
closingScalingFactor = currentScale;
|
|
closeAnim.restart();
|
|
closeScaleAnim.restart();
|
|
}
|
|
|
|
function openApp(index, window, duration = Kirigami.Units.shortDuration, horizontalEasing = Easing.OutBack) {
|
|
// cancel any opening animations ongoing
|
|
cancelAnimations();
|
|
|
|
animateGoToTaskIndex(index, duration);
|
|
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) {
|
|
root.state.xPosition = xPositionFromTaskIndex(index);
|
|
}
|
|
|
|
// go to the task index, animated
|
|
function animateGoToTaskIndex(index, duration = Kirigami.Units.longDuration * 2, easing = Easing.OutExpo) {
|
|
xAnim.duration = duration;
|
|
xAnim.to = xPositionFromTaskIndex(index);
|
|
xAnim.easing.type = easing;
|
|
xAnim.restart();
|
|
}
|
|
|
|
function getNearestTaskIndex() {
|
|
let newTaskIndex = getTaskIndexFromXPosition();
|
|
let currentTaskIndexPosition = xPositionFromTaskIndex(root.state.currentTaskIndex);
|
|
if (root.state.xPosition > currentTaskIndexPosition) {
|
|
// moving to task further to the right
|
|
if (newTaskIndex != root.state.currentTaskIndex) {
|
|
// reset back to current task index
|
|
return root.state.currentTaskIndex;
|
|
} else {
|
|
// animate snapping to new task index
|
|
return Math.max(0, newTaskIndex);
|
|
}
|
|
} else {
|
|
// moving to task further to the left
|
|
if (newTaskIndex != root.state.currentTaskIndex) {
|
|
// animate snapping to new task index
|
|
return Math.min(taskSwitcher.tasksCount - 1, newTaskIndex);
|
|
} else {
|
|
// reset back to current task index
|
|
return root.state.currentTaskIndex;
|
|
}
|
|
}
|
|
}
|
|
function snapToNearestTask() {
|
|
let index = getNearestTaskIndex();
|
|
animateGoToTaskIndex(index);
|
|
}
|
|
|
|
// This is a workaround for flickable not actually flicking, so we just snap to the next task
|
|
// based on old movement direction, ignoring momentum (because flickable doesn't give us any momentum)
|
|
function snapToNearestTaskWorkaround(movingRight) {
|
|
let currentTaskIndexPosition = xPositionFromTaskIndex(root.state.currentTaskIndex);
|
|
if (root.state.xPosition > currentTaskIndexPosition) {
|
|
if (movingRight) {
|
|
animateGoToTaskIndex(root.state.currentTaskIndex);
|
|
} else {
|
|
animateGoToTaskIndex(Math.max(0, root.state.currentTaskIndex - 1));
|
|
}
|
|
} else {
|
|
if (movingRight) {
|
|
animateGoToTaskIndex(Math.min(taskSwitcher.tasksCount - 1, root.state.currentTaskIndex + 1));
|
|
} else {
|
|
animateGoToTaskIndex(root.state.currentTaskIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ~~ property animators ~~
|
|
|
|
property var xAnim: NumberAnimation {
|
|
target: root.state
|
|
property: "xPosition"
|
|
easing.type: Easing.OutBack
|
|
}
|
|
|
|
property var openAnim: NumberAnimation {
|
|
target: root.state
|
|
property: "yPosition"
|
|
to: openedYPosition
|
|
duration: 250
|
|
easing.type: Easing.OutExpo
|
|
|
|
onFinished: {
|
|
root.state.status = stateClass.Active;
|
|
}
|
|
}
|
|
|
|
// TODO: This animation should ideally be replaced by some
|
|
// speed tracking to track finger movement better. Until then
|
|
// InBack at least pretends to go in the finger move direction
|
|
property var closeAnim: NumberAnimation {
|
|
target: root.state
|
|
property: "yPosition"
|
|
to: 0
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InBack
|
|
|
|
onFinished: {
|
|
root.state.status = stateClass.Inactive;
|
|
taskSwitcher.instantHide();
|
|
}
|
|
}
|
|
|
|
property var closeScaleAnim: NumberAnimation {
|
|
target: root
|
|
property: "closingScalingFactor"
|
|
to: 0.1
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InQuad
|
|
|
|
onStopped: {
|
|
closingScalingFactor = 1;
|
|
}
|
|
}
|
|
|
|
property var openAppAnim: NumberAnimation {
|
|
target: root.state
|
|
property: "yPosition"
|
|
to: 0
|
|
duration: 300
|
|
easing.type: Easing.OutQuint
|
|
onFinished: {
|
|
root.state.status = stateClass.Inactive;
|
|
taskSwitcher.instantHide();
|
|
}
|
|
}
|
|
}
|