shift-shell/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml
Devin Lin 28f7224a5b gestures: Add optional gesture guide panel
This adds a gesture handle mode to the navigation panel, which can be
enabled during gesture-only mode. This reserves space for the system
gesture to be able to be used, allowing us to extend the height in KWin
of the gesture recognition area (which is currently far too short for devices such as Pixel 3a).

This also allows for navigation with a mouse; clicking on the handle
triggers the task switcher, holding it triggers the "home" action.
2025-12-14 18:36:42 -05:00

830 lines
37 KiB
QML

// SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2024-2025 Luis Büchi <luis.buechi@kdemail.net>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.taskswitcherplugin as TaskSwitcherPlugin
import org.kde.kwin 3.0 as KWinComponents
import org.kde.kwin.private.effects 1.0
import org.kde.kitemmodels
/**
* Component that provides a task switcher.
*/
FocusScope {
id: root
focus: true
property TaskSwitcherPlugin.MobileTaskSwitcherState state
readonly property QtObject effect: KWinComponents.SceneView.effect
readonly property QtObject targetScreen: KWinComponents.SceneView.screen
readonly property real navBottomMargin: MobileShell.Constants.navigationPanelOnSide(width, height) ? 0 : MobileShell.Constants.navigationPanelThickness
readonly property real navRightMargin: MobileShell.Constants.navigationPanelOnSide(width, height) ? MobileShell.Constants.navigationPanelThickness : 0
readonly property real topMargin: ShellSettings.Settings.autoHidePanelsEnabled ? 0 : MobileShell.Constants.topPanelHeight
readonly property real bottomMargin: ShellSettings.Settings.autoHidePanelsEnabled ? 0 : navBottomMargin
readonly property real leftMargin: 0
readonly property real rightMargin: ShellSettings.Settings.autoHidePanelsEnabled ? 0 : navRightMargin
property var taskSwitcherHelpers: TaskSwitcherHelpers {
taskSwitcher: root
taskList: taskList
}
MobileShell.HapticsEffect {
id: haptics
}
property TaskSwitcherPlugin.TaskFilterModel tasksModel: TaskSwitcherPlugin.TaskFilterModel {
screenName: root.targetScreen.name
windowModel: root.state.taskModel
}
readonly property int tasksCount: taskList.count
// keep track of task list events
property int oldTasksCount: tasksCount
onTasksCountChanged: {
// we need to subtract 1 from the current index when the closed task index is smaller
// this is because this part of the list has been shifted down by 1 when the closed task was removed.
if (taskSwitcherHelpers.lastClosedTask < state.currentTaskIndex) {
state.currentTaskIndex -= 1;
// animated at the same speed as the task x position in the TaskList so that the task appears not to move from the perspective of the user.
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex, Kirigami.Units.longDuration, Easing.InOutQuad);
taskSwitcherHelpers.lastClosedTask = -1;
}
if (tasksCount === 0 && oldTasksCount !== 0) {
hide();
} else if (tasksCount < oldTasksCount) {
if (state.currentTaskIndex < 0) {
// if the user is on the first task, and it is closed, scroll right
taskSwitcherHelpers.animateGoToTaskIndex(0, Kirigami.Units.longDuration);
} else if (state.currentTaskIndex >= tasksCount) {
// if the user is on the last task, and it is closed, scroll left
taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration);
}
}
oldTasksCount = tasksCount;
}
Keys.onEscapePressed: hide();
Component.onCompleted: {
initialSetup();
}
function initialSetup(): void {
taskSwitcherHelpers.cancelAnimations();
state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow);
// ensure the task drawer is not opened and reset values to defaults
taskSwitcherHelpers.reachedHeightThreshold = false;
taskSwitcherHelpers.gestureState = TaskSwitcherHelpers.GestureStates.Undecided;
taskSwitcherHelpers.isInTaskScrubMode = false;
taskSwitcherHelpers.inLastFrame = false;
taskSwitcherHelpers.hasVibrated = false;
taskSwitcherHelpers.closingFactor = 1;
taskSwitcherHelpers.taskSwitchCanLaunch = false;
taskSwitchCanLaunchTimer.restart()
taskList.taskOffsetEasing = Easing.InOutQuart;
taskList.homeTouchPositionX = 0;
backgroundColorOpacityAn.enabled = false;
backgroundColorOpacity = state.wasInActiveTask ? 1 : 0;
backgroundColorOpacityAn.enabled = true;
// reset the offset to have the task drawer off screen
taskList.setTaskOffsetValue(state.wasInActiveTask ? taskSwitcherHelpers.taskOffsetValue : taskSwitcherHelpers.homeOffsetValue, true);
// task index from the last time using the switcher
state.initialTaskIndex = Math.min(state.currentTaskIndex, tasksCount - 1);
if (state.wasInActiveTask) {
// if we were in an active task instead set initial task index to the position of that task
state.initialTaskIndex = taskSwitcherHelpers.getTaskIndexFromWindow(KWinComponents.Workspace.activeWindow);
} else {
// reset the task index to the start if on home screen
state.initialTaskIndex = 0
}
state.currentTaskIndex = state.initialTaskIndex
taskSwitcherHelpers.currentDisplayTask = state.currentTaskIndex;
taskSwitcherHelpers.goToTaskIndex(state.initialTaskIndex);
taskList.minimizeAll();
// fully open the switcher (if this is a button press, not gesture)
if (!state.gestureInProgress) {
taskSwitcherHelpers.fromButton = true;
if (state.wasInActiveTask) {
taskList.setTaskOffsetValue(0, true);
} else {
taskList.setTaskOffsetValue(0);
}
backgroundColorOpacity = 1;
taskSwitcherHelpers.open();
}
}
// called by c++ plugin
function hideAnimation(): void {
closeAnim.restart();
}
function instantHide(): void {
state.deactivate(true);
}
function hide(): void {
state.deactivate(false);
}
Connections {
target: root.state
// task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion
function taskScrubMode(): void {
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
if (!root.taskSwitcherHelpers.isInTaskScrubMode) {
root.backgroundColorOpacity = 1;
root.taskSwitcherHelpers.cancelAnimations();
root.taskSwitcherHelpers.open();
if (!root.taskSwitcherHelpers.hasVibrated) {
// Haptic feedback when the task scrub mode engages
haptics.buttonVibrate();
root.taskSwitcherHelpers.hasVibrated = true;
}
}
// TODO this makes sense, but makes scrub mode feel a bit weird
// improve trigger distance logic for task scrub mode to fix
let newTaskIndex = Math.max(0, Math.min(root.tasksCount - 1, Math.floor(root.state.touchXPosition / root.taskSwitcherHelpers.taskScrubDistance) + root.state.initialTaskIndex - (root.state.wasInActiveTask ? 0 : 1)));
if (newTaskIndex != root.state.currentTaskIndex || !root.taskSwitcherHelpers.isInTaskScrubMode) {
root.taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex);
root.taskSwitcherHelpers.isInTaskScrubMode = true;
}
}
function onTouchPositionChanged(): void {
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
if (root.taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX
(Math.abs(root.state.xVelocity) > Math.abs(root.state.yVelocity) * 3 && // gesture needs to be almost completely horizontal
Math.abs(root.state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward
Math.abs(root.state.touchXPosition) > root.taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways
unmodifiedYposition < Kirigami.Units.largeSpacing * 2 && // and be close to the screen edge
root.tasksCount > 0 && // and there needs to be more than none task open
!root.taskSwitcherHelpers.taskDrawerOpened // and the task drawer must not be open
)) {
taskScrubMode();
} else {
if (root.taskSwitcherHelpers.currentlyBeingClosed) {
// if the task switch is still open but playing the close animation
// setup some values and return to the initial setup so that the user can always navigate with no down time
root.state.wasInActiveTask = root.taskSwitcherHelpers.openAppAnim.running ? true : false
taskList.setTaskOffsetValue(root.state.wasInActiveTask ? root.taskSwitcherHelpers.taskOffsetValue : root.taskSwitcherHelpers.homeOffsetValue, true);
root.state.status = !root.state.wasInActiveTask ? (root.taskSwitcherHelpers.openAppAnim.closeAnim && !root.taskSwitcherHelpers.taskDrawerWillOpen ? TaskSwitcherPlugin.MobileTaskSwitcherState.Active : TaskSwitcherPlugin.MobileTaskSwitcherState.Inactive) : TaskSwitcherPlugin.MobileTaskSwitcherState.Inactive
root.initialSetup();
} else if (root.taskSwitcherHelpers.openAnim.running) {
root.taskSwitcherHelpers.cancelAnimations();
root.state.status = root.taskSwitcherHelpers.stateClass.Active;
}
root.state.yPosition = unmodifiedYposition + (root.taskSwitcherHelpers.taskDrawerOpened || !root.state.wasInActiveTask ? root.taskSwitcherHelpers.openedYPosition : 0);
let newXPosition = root.taskSwitcherHelpers.xPositionFromTaskIndex(root.state.initialTaskIndex);
if (root.taskSwitcherHelpers.notHomeScreenState && !root.taskSwitcherHelpers.currentlyBeingClosed) {
newXPosition = newXPosition - (root.state.touchXPosition / root.taskSwitcherHelpers.currentScale);
}
root.state.xPosition = newXPosition;
// allows the user to move the task drawer left and right when on the home screen
taskList.homeTouchPositionX = root.taskSwitcherHelpers.notHomeScreenState ? 0 : (root.state.touchXPosition * 0.35);
// dynamically update the task switcher state based off of the touch position and velocity
updateTaskSwitcherState()
}
}
function updateTaskSwitcherState(): void {
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
// if the touch is above heightThreshold, set reachedHeightThreshold to true
if (unmodifiedYposition > root.taskSwitcherHelpers.heightThreshold) {
// set reachedHeightThreshold when above or below two separate points to helps prevent flickering when the task switcher moves in and out of view
root.taskSwitcherHelpers.reachedHeightThreshold = true;
root.backgroundColorOpacity = root.taskSwitcherHelpers.notHomeScreenState ? 0 : 1;
} else if (unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
root.backgroundColorOpacity = 1;
} else {
root.backgroundColorOpacity = root.taskSwitcherHelpers.notHomeScreenState ? 1 : 0;
}
if (root.state.totalSquaredVelocity > root.state.flickVelocityThreshold) {
// flick
// ratio between y and x velocity as threshold between vertical and horizontal flick
let xyVelocityRatio = 1.7; // with 1.7 swipes up to ~60° from horizontal are counted as horizontal
if (root.state.yVelocity > Math.abs(root.state.xVelocity) * xyVelocityRatio) {
// downwards flick
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided);
if (unmodifiedYposition < root.taskSwitcherHelpers.undoYThreshold) {
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.notHomeScreenState ? 0 : root.taskSwitcherHelpers.homeOffsetValue);
}
} else if (-root.state.yVelocity > Math.abs(root.state.xVelocity) * xyVelocityRatio || (root.taskSwitcherHelpers.reachedHeightThreshold && root.taskSwitcherHelpers.notHomeScreenState)) {
// upwards flick or if the touch is above heightThreshold
if (root.taskSwitcherHelpers.notHomeScreenState) {
// if in app or task switcher, go home
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
if (root.taskSwitcherHelpers.reachedHeightThreshold) {
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.taskOffsetValue);
}
} else if (unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
// else, keep the task switcher in view
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.peekOffsetValue);
}
} else if (!root.taskSwitcherHelpers.reachedHeightThreshold && !root.taskSwitcherHelpers.isInTaskScrubMode) {
// sideways flick
if (root.taskSwitcherHelpers.notHomeScreenState) {
taskList.setTaskOffsetValue(0, unmodifiedYposition < root.taskSwitcherHelpers.openedYPosition ? true : false);
}
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.HorizontalSwipe);
}
} else {
if (unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
// if just moveing out of undoYThreshold, set the state to home
if (root.taskSwitcherHelpers.gestureState < TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home)
}
// if the touch is above heightThreshold, it will return home
if (unmodifiedYposition > root.taskSwitcherHelpers.heightThreshold) {
root.taskSwitcherHelpers.hasVibrated = true;
if (root.taskSwitcherHelpers.notHomeScreenState) {
// move the task switcher out of view
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.taskOffsetValue);
} else {
// keep the task switcher in view when above heightThreshold and from home
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.peekOffsetValue);
}
// minus largeSpacing from the heightThreshold to help prevent flickering when the task switcher moves in and out of view
} else if ((unmodifiedYposition < root.taskSwitcherHelpers.heightThreshold - Kirigami.Units.largeSpacing) || root.taskSwitcherHelpers.reachedHeightThreshold == false) {
// set reachedHeightThreshold when above or below two separate points to helps prevent flickering when the task switcher moves in and out of view
root.taskSwitcherHelpers.reachedHeightThreshold = false;
if (root.state.totalSquaredVelocity < root.state.flickVelocityThreshold && root.taskSwitcherHelpers.taskSwitchCanLaunch) {
// if velocity is small enough, move the task switcher into view
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.notHomeScreenState ? 0 : root.taskSwitcherHelpers.peekOffsetValue);
}
}
} else {
// if under the undo threshold, it will go back to the task switcher if it is open
if (root.taskSwitcherHelpers.taskDrawerOpened) {
root.taskSwitcherHelpers.reachedHeightThreshold = false;
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher)
taskList.setTaskOffsetValue(0);
} else {
root.taskSwitcherHelpers.reachedHeightThreshold = false;
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided)
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.notHomeScreenState ? 0 : root.taskSwitcherHelpers.homeOffsetValue);
}
}
}
}
// returns to the currently centered app. usually used to "back out" of the switcher
// if accidentally invoked, but can also be used to switch to an adjacent app and then open it
function returnToApp(): void {
let newIndex = root.taskSwitcherHelpers.getNearestTaskIndex();
root.taskSwitcherHelpers.openApp(newIndex);
}
// diagonal quick switch gesture logic
function quickSwitch(): void {
// should "quick switch" to adjacent app in task switcher, but only if we were in an app before
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
let newIndex = root.state.currentTaskIndex;
let shouldSwitch = false;
if (root.state.xVelocity > 0) {
if (root.taskSwitcherHelpers.notHomeScreenState) {
// flick to the right, go to the app on the left
newIndex = root.state.currentTaskIndex + 1;
}
if (newIndex < root.tasksCount) {
// switch only if flick doesn't go over end of list
shouldSwitch = true;
}
} else if (root.state.xVelocity < 0) {
if (root.taskSwitcherHelpers.notHomeScreenState) {
// flick to the left, go to app to the right
newIndex = root.state.currentTaskIndex - 1;
if (newIndex >= 0) {
// switch only if flick doesn't go over end of list
shouldSwitch = true;
}
} else {
// flick to the left on the home screen, dismiss the gesture
root.taskSwitcherHelpers.close();
return;
}
}
if (shouldSwitch) {
if (!root.taskSwitcherHelpers.taskDrawerOpened && unmodifiedYposition < root.taskSwitcherHelpers.openedYPosition) {
// if in a app, switch it to the new task when it is under the openedYPosition
taskList.setTaskOffsetValue(0, unmodifiedYposition < root.taskSwitcherHelpers.openedYPosition && root.taskSwitcherHelpers.notHomeScreenState);
root.taskSwitcherHelpers.openApp(newIndex, Kirigami.Units.longDuration * 4, Easing.OutExpo);
} else {
// if already in the task switcher or above the openedYPosition, only change the focus to the new task
root.taskSwitcherHelpers.animateGoToTaskIndex(newIndex);
root.taskSwitcherHelpers.open();
}
} else {
// if not switching, just open task switcher
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
}
}
// Logic for deciding how to handle the end of a gesture input
function onGestureInProgressChanged(): void {
root.taskSwitcherHelpers.fromButton = false;
if (root.state.gestureInProgress) {
root.taskSwitcherHelpers.currentDisplayTask = root.state.currentTaskIndex;
return;
}
if (taskList.count === 0) {
// dismiss the gesture if the task list is empty
root.taskSwitcherHelpers.close();
} if (root.taskSwitcherHelpers.isInTaskScrubMode) {
// TODO! do we want to handle upwards flick to dismiss in task scrub mode?
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
root.backgroundColorOpacity = 1;
if (root.taskSwitcherHelpers.taskDrawerOpened || unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
root.taskSwitcherHelpers.isInTaskScrubMode = false;
} else {
root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex);
}
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Undecided) {
if (root.taskSwitcherHelpers.taskDrawerOpened) {
// if in the task switcher, return to it
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
} else if (root.state.wasInActiveTask) {
// if inside a app, return to it
returnToApp();
} else {
// else dismiss the gesture
root.taskSwitcherHelpers.close();
}
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.HorizontalSwipe) {
// sideways flick
root.backgroundColorOpacity = 1;
quickSwitch();
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
// open the task drawer
root.backgroundColorOpacity = 1;
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Home) {
root.taskSwitcherHelpers.close();
}
}
function onVelocityChanged(): void {
}
function onXPositionChanged(): void {
root.taskSwitcherHelpers.updateTaskIndex();
}
}
// kind of a hack, but this prevents the gesture from immediately activating the task switcher when it is not supposed to
Timer {
id: taskSwitchCanLaunchTimer
interval: 1; running: true; repeat: false
onTriggered: root.taskSwitcherHelpers.taskSwitchCanLaunch = true;
}
function setTaskDrawerState(value: int): void {
if (taskSwitcherHelpers.gestureState != TaskSwitcherHelpers.GestureStates.TaskSwitcher && value == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
// vibrate only if switching to task drawer
if (!taskSwitcherHelpers.hasVibrated) {
// Haptic feedback when the task scrub mode engages
haptics.buttonVibrate();
taskSwitcherHelpers.hasVibrated = true;
}
}
taskSwitcherHelpers.gestureState = value;
}
// view of the desktop background
KWinComponents.DesktopBackground {
id: backgroundItem
activity: KWinComponents.Workspace.currentActivity
desktop: KWinComponents.Workspace.currentDesktop
outputName: root.targetScreen.name
}
// background colour
Rectangle {
id: backgroundRect
anchors.fill: root
opacity: container.opacity
color: {
return Qt.rgba(0, 0, 0, 0.6 * root.taskSwitcherHelpers.closingFactor * root.backgroundColorOpacity);
}
}
// animate the background opacity based off of the state.
property real backgroundColorOpacity: 1
Behavior on backgroundColorOpacity {
id: backgroundColorOpacityAn
NumberAnimation {
duration: Kirigami.Units.longDuration
}
}
// status bar
// TODO: improve load times, it is quite slow
// MobileShell.StatusBar {
// id: statusBar
// z: 1
// colorGroup: Kirigami.Theme.ComplementaryColorGroup
// backgroundColor: "transparent"
//
// height: root.topMargin
// anchors.top: parent.top
// anchors.left: parent.left
// anchors.right: parent.right
// }
// Gesture panel
Component {
id: gesturePanelComponent
MobileShell.GesturePanel {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
opaqueBar: false
// Trigger home action on tap
onHandleClicked: {
if (taskList.count === 0) {
root.hide();
} else {
if (taskList.count > 1 &&
root.state.elapsedTimeSinceStart != -1 &&
root.state.elapsedTimeSinceStart < root.state.doubleClickInterval) {
root.taskSwitcherHelpers.openApp(1);
return;
}
const currentIndex = root.state.currentTaskIndex;
root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex);
}
}
}
}
// Navigation panel
Component {
id: navigationPanelComponent
MobileShell.NavigationPanel {
id: navigationPanel
backgroundColor: Qt.rgba(0, 0, 0, 0.1)
foregroundColorGroup: Kirigami.Theme.Complementary
shadow: false
isVertical: MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
MobileShellState.PanelSettingsDBusClient {
id: panelSettings
screenName: Screen.name
}
leftPadding: panelSettings.navigationPanelLeftPadding
rightPadding: panelSettings.navigationPanelRightPadding
leftAction: MobileShell.NavigationPanelAction {
enabled: true
iconSource: "mobile-task-switcher"
shrinkSize: 4
onTriggered: {
if (taskList.count === 0) {
root.hide();
} else {
if (taskList.count > 1 &&
root.state.elapsedTimeSinceStart != -1 &&
root.state.elapsedTimeSinceStart < root.state.doubleClickInterval) {
root.taskSwitcherHelpers.openApp(1);
return;
}
const currentIndex = root.state.currentTaskIndex;
root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex);
}
}
}
// home button
middleAction: MobileShell.NavigationPanelAction {
enabled: true
iconSource: "start-here-kde"
onTriggered: root.hide()
}
// close app/keyboard button
rightAction: MobileShell.NavigationPanelAction {
enabled: true
iconSource: "mobile-close-app"
shrinkSize: 4
onTriggered: {
taskList.getTaskAt(root.state.currentTaskIndex).closeApp();
}
}
rightCornerAction: MobileShell.NavigationPanelAction {
visible: false
}
}
}
Loader {
id: panelLoader
z: !root.taskSwitcherHelpers.currentlyBeingClosed ? 1 : 0
sourceComponent: {
if (ShellSettings.Settings.navigationPanelEnabled) {
return navigationPanelComponent;
} else if (ShellSettings.Settings.gesturePanelEnabled) {
return gesturePanelComponent;
} else {
return null;
}
}
}
states: [
State {
name: "landscape"
when: MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
AnchorChanges {
target: panelLoader
anchors {
right: root.right
top: root.top
bottom: root.bottom
left: undefined
}
}
PropertyChanges {
target: panelLoader
width: navRightMargin
anchors.topMargin: root.topMargin
}
},
State {
name: "portrait"
when: !MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
AnchorChanges {
target: panelLoader
anchors {
top: undefined
right: root.right
left: root.left
bottom: root.bottom
}
}
PropertyChanges {
target: panelLoader
height: navBottomMargin
}
}
]
// task list
Item {
id: container
// provide shell margins
anchors.fill: root
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: root.bottomMargin
anchors.topMargin: root.topMargin
NumberAnimation on opacity {
id: closeAnim
running: false
to: 0
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
onFinished: {
closeAllButton.closeRequested = false;
}
}
// placeholder message
ColumnLayout {
id: placeholder
spacing: Kirigami.Units.gridUnit
opacity: {
let baseOpacity = ((root.tasksCount === 0 && !root.taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0);
return root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher ? baseOpacity : 0;
}
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.veryLongDuration } }
anchors.centerIn: container
Kirigami.Icon {
id: icon
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
source: "edit-none-symbolic"
color: "white"
}
Kirigami.Heading {
Layout.fillWidth: true
Layout.maximumWidth: root.width * 0.75
Layout.alignment: Qt.AlignHCenter
color: "white"
level: 3
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
text: i18n("No applications are running.")
}
}
RowLayout {
id: scrubIconList
opacity: root.taskSwitcherHelpers.isInTaskScrubMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration } }
anchors.bottom: container.bottom
anchors.right: container.horizontalCenter
anchors.bottomMargin: root.taskSwitcherHelpers.scrubModeBottomMargin
anchors.rightMargin: {
let size = Kirigami.Units.iconSizes.large + Kirigami.Units.largeSpacing * 2;
let offset = (root.state.currentTaskIndex + 0.5) * size;
return -offset;
}
Behavior on anchors.rightMargin {
NumberAnimation {
duration: root.taskSwitcherHelpers.xAnimDuration;
easing.type: root.taskSwitcherHelpers.xAnimEasingType;
}
}
spacing: Kirigami.Units.largeSpacing * 2
layoutDirection: Qt.RightToLeft
Repeater {
model: root.tasksModel
delegate: Kirigami.Icon {
id: iconDelegate
required property QtObject window
required property int index
readonly property bool isCenteredIcon: iconDelegate.index === root.state.currentTaskIndex;
Layout.preferredHeight: isCenteredIcon ? Kirigami.Units.iconSizes.huge : Kirigami.Units.iconSizes.large
Layout.preferredWidth: isCenteredIcon ? Kirigami.Units.iconSizes.huge : Kirigami.Units.iconSizes.large
Layout.alignment: Qt.AlignVCenter
source: iconDelegate.window.icon
}
}
}
RowLayout {
id: scrubIndicator
opacity: root.taskSwitcherHelpers.isInTaskScrubMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
anchors.bottom: container.bottom
anchors.horizontalCenter: container.horizontalCenter
anchors.bottomMargin: root.taskSwitcherHelpers.openedYPosition * 1 / 4
Kirigami.Icon {
id: iconScrubBack
opacity: root.state.currentTaskIndex == 0 ? 0.3 : 1
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration * 2; easing.type: Easing.OutExpo } }
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
source: "draw-arrow-back"
color: "white"
}
Item {
width: root.taskSwitcherHelpers.windowWidth / 4
}
Kirigami.Icon {
id: iconScrubFront
opacity: root.state.currentTaskIndex == root.tasksCount - 1 ? 0.3 : 1
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration * 2; easing.type: Easing.OutExpo } }
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
source: "draw-arrow-forward"
color: "white"
}
}
// flicking area for task switcher
FlickContainer {
id: flickable
anchors.fill: container
taskSwitcherState: root.state
taskSwitcherHelpers: root.taskSwitcherHelpers
tasksCount: root.tasksCount
// don't allow FlickContainer to steal from swiping on tasks
interactive: taskList.taskInteractingCount === 0
// the item is effectively anchored to the flickable bounds
TaskList {
id: taskList
taskSwitcher: root
shellTopMargin: root.topMargin
shellBottomMargin: root.bottomMargin
x: flickable.contentX
width: flickable.width
height: flickable.height
}
PlasmaComponents.ToolButton {
id: closeAllButton
property bool closeRequested: false
visible: root.tasksCount !== 0 && !root.taskSwitcherHelpers.isInTaskScrubMode
enabled: !root.taskSwitcherHelpers.currentlyBeingClosed && !root.state.gestureInProgress
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
anchors {
bottom: taskList.bottom
bottomMargin: (taskList.taskYBase) * 0.75
horizontalCenter: taskList.horizontalCenter
}
opacity: (root.taskSwitcherHelpers.currentlyBeingClosed || root.state.gestureInProgress || !root.taskSwitcherHelpers.taskDrawerOpened) ? 0.0 : 1.0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
icon.name: "edit-clear-history"
font.bold: true
text: closeRequested ? i18n("Confirm Close All") : i18n("Close All")
onClicked: {
if (closeRequested) {
taskList.closeAll();
} else {
closeRequested = true;
}
}
}
}
}
}