mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
This adds support for specifying options needed to deal with phone display panel pecularities (ex. screen curves, notches, punch holes) This is implemented as settings in ~/.config/plasmamobilerc, which can set panel heights, paddings, and center spacings to duck display cutouts. The pixel values are scaling independent, and so are not affected when the display scaling is changed. This is then exposed over DBus, so that components from outside of plasmashell (ex. KWin) can access it easily without needing to connect to kscreen themselves. Each screen is exposed as a single object. Currently support is only added in the status bar and the navigation panel. Currently all screens have the settings applied. In the future, we may want to limit this just to the internal screen (?) --- This also adds a "devices" folder (in `devices/configs`) where per-device configs can be set. This is installed to `/usr/share/plasma-mobile-device-configs`. In `plasmamobilerc` (installed to `/etc/xdg/plasmamobilerc`, or `~/.config/plasmamobilerc`), envmanager will read: ```toml [Device] device=oneplus-enchilada ``` for the device config to use and write its settings to `~/.config/plasma-mobile/plasmamobilerc`.
782 lines
35 KiB
QML
782 lines
35 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
|
|
// }
|
|
|
|
// navigation panel
|
|
MobileShell.NavigationPanel {
|
|
id: navigationPanel
|
|
z: !root.taskSwitcherHelpers.currentlyBeingClosed ? 1 : 0
|
|
visible: ShellSettings.Settings.navigationPanelEnabled
|
|
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
|
|
}
|
|
}
|
|
|
|
states: [
|
|
State {
|
|
name: "landscape"
|
|
when: MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
|
|
AnchorChanges {
|
|
target: navigationPanel
|
|
anchors {
|
|
right: root.right
|
|
top: root.top
|
|
bottom: root.bottom
|
|
left: undefined
|
|
}
|
|
}
|
|
PropertyChanges {
|
|
target: navigationPanel
|
|
width: navRightMargin
|
|
anchors.topMargin: root.topMargin
|
|
}
|
|
},
|
|
State {
|
|
name: "portrait"
|
|
when: !MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
|
|
AnchorChanges {
|
|
target: navigationPanel
|
|
anchors {
|
|
top: undefined
|
|
right: root.right
|
|
left: root.left
|
|
bottom: root.bottom
|
|
}
|
|
}
|
|
PropertyChanges {
|
|
target: navigationPanel
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|