mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
taskswitcher: Gesture Navigation: Quality of Life Improvements
Most Notable Changes Here Include: 1. If the user moves and lifts their finger up halfway up the screen, the navigation gesture will now go home. 2. The task drawer will now move in and out of view depending on the gesture navigation state. 3. The app window will now continue to shrink with resistance as the window moves further up. 4. When in the task drawer, if the user drags up from the bottom, the current task will now follow the users finger like the task does when dragging up from within an app. 5. The task drawer will now slide in from the side when it is not within an app. I would upload a video here to showcase these changes. However, I was unable to get OBS to record my screen while in my plasma mobile session.
This commit is contained in:
parent
971879f10f
commit
fdc8958ce5
5 changed files with 523 additions and 157 deletions
|
|
@ -25,6 +25,9 @@ Flickable {
|
||||||
// update position from horizontal flickable movement
|
// update position from horizontal flickable movement
|
||||||
property real oldContentX
|
property real oldContentX
|
||||||
onContentXChanged: {
|
onContentXChanged: {
|
||||||
|
// disable if animations are running to prevent bugs
|
||||||
|
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
|
||||||
|
|
||||||
if (moving) {
|
if (moving) {
|
||||||
// TODO whenever flicking actually works this should probably be swapped with
|
// TODO whenever flicking actually works this should probably be swapped with
|
||||||
// a minimum velocity after which it should snap to the nearest task
|
// a minimum velocity after which it should snap to the nearest task
|
||||||
|
|
@ -35,14 +38,17 @@ Flickable {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMovementStarted: {
|
onMovementStarted: {
|
||||||
|
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
|
||||||
taskSwitcherHelpers.cancelAnimations();
|
taskSwitcherHelpers.cancelAnimations();
|
||||||
}
|
}
|
||||||
onMovementEnded: {
|
onMovementEnded: {
|
||||||
|
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
|
||||||
taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight);
|
taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight);
|
||||||
resetPosition();
|
resetPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
onFlickStarted: {
|
onFlickStarted: {
|
||||||
|
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
|
||||||
root.cancelFlick();
|
root.cancelFlick();
|
||||||
}
|
}
|
||||||
onFlickEnded: {
|
onFlickEnded: {
|
||||||
|
|
@ -51,6 +57,7 @@ Flickable {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDraggingChanged: {
|
onDraggingChanged: {
|
||||||
|
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
taskSwitcherHelpers.cancelAnimations();
|
taskSwitcherHelpers.cancelAnimations();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ Item {
|
||||||
|
|
||||||
readonly property real dragOffset: -control.y
|
readonly property real dragOffset: -control.y
|
||||||
|
|
||||||
|
readonly property int currentIndex: model.index
|
||||||
|
|
||||||
// whether this task is being interacted with
|
// whether this task is being interacted with
|
||||||
readonly property bool interactingActive: control.pressed && control.passedDragThreshold
|
readonly property bool interactingActive: control.pressed && control.passedDragThreshold
|
||||||
|
|
||||||
|
|
@ -136,6 +138,7 @@ Item {
|
||||||
to: 0
|
to: 0
|
||||||
onFinished: {
|
onFinished: {
|
||||||
if (to != 0) { // close app
|
if (to != 0) { // close app
|
||||||
|
taskSwitcherHelpers.lastClosedTask = currentIndex;
|
||||||
delegate.closeApp();
|
delegate.closeApp();
|
||||||
uncloseTimer.start();
|
uncloseTimer.start();
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +185,10 @@ Item {
|
||||||
icon.name: "window-close"
|
icon.name: "window-close"
|
||||||
icon.width: Kirigami.Units.iconSizes.smallMedium
|
icon.width: Kirigami.Units.iconSizes.smallMedium
|
||||||
icon.height: Kirigami.Units.iconSizes.smallMedium
|
icon.height: Kirigami.Units.iconSizes.smallMedium
|
||||||
onClicked: delegate.closeApp()
|
onClicked: {
|
||||||
|
taskSwitcherHelpers.lastClosedTask = currentIndex;
|
||||||
|
delegate.closeApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,31 +23,22 @@ MouseArea {
|
||||||
property int taskInteractingCount: 0
|
property int taskInteractingCount: 0
|
||||||
|
|
||||||
// account for system header and footer offset (center the preview image)
|
// account for system header and footer offset (center the preview image)
|
||||||
readonly property real taskY: {
|
readonly property real taskYBase: {
|
||||||
let headerHeight = shellTopMargin;
|
let headerHeight = shellTopMargin;
|
||||||
let footerHeight = shellBottomMargin;
|
let footerHeight = shellBottomMargin;
|
||||||
let diff = headerHeight - footerHeight;
|
let diff = headerHeight - footerHeight;
|
||||||
|
|
||||||
let baseY = (taskSwitcher.height / 2) - (taskSwitcherHelpers.taskHeight / 2) - (taskSwitcherHelpers.taskHeaderHeight / 2);
|
let baseY = (taskSwitcher.height / 2) - (taskSwitcherHelpers.taskHeight / 2) - (taskSwitcherHelpers.taskHeaderHeight / 2);
|
||||||
|
|
||||||
return baseY + diff / 2 - shellTopMargin - trackFingerYOffset;
|
return baseY + diff / 2 - shellTopMargin;
|
||||||
}
|
}
|
||||||
readonly property real trackFingerYOffset: {
|
readonly property real taskY: {
|
||||||
if (taskSwitcherHelpers.isScaleClamped) {
|
let trackFingerYOffsetClamped = 0;
|
||||||
let openedPos = taskSwitcherHelpers.openedYPosition;
|
if (taskSwitcherHelpers.isScaleClamped && (taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened)) {
|
||||||
let directTrackingOffset = openedPos * 0.2
|
trackFingerYOffsetClamped = taskSwitcherHelpers.trackFingerYOffset;
|
||||||
if (taskSwitcherState.yPosition < openedPos + directTrackingOffset) {
|
|
||||||
// Allow the task list to move further up than the fully opened position
|
|
||||||
return taskSwitcherState.yPosition - openedPos;
|
|
||||||
} else {
|
|
||||||
// but make it more reluctant the further up it goes
|
|
||||||
let overDragProgress = (taskSwitcherState.yPosition - directTrackingOffset - openedPos) / openedPos;
|
|
||||||
// Base formula is 1-2.3^(-progress) which asymptotically approaches 1
|
|
||||||
return (1 - Math.pow(2.3, -overDragProgress)) * openedPos + directTrackingOffset;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return taskYBase - trackFingerYOffsetClamped;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTaskAt(index) {
|
function getTaskAt(index) {
|
||||||
|
|
@ -82,6 +73,59 @@ MouseArea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the postition offset value for non-active tasks in the task drawer
|
||||||
|
// this value is normalized and is usually set to 0, 1, or 2 (larger the number, the further they are from the active task)
|
||||||
|
property real baseTaskOffset: 0
|
||||||
|
|
||||||
|
// the position offset value tracked to the touch x position when opening the task drawer from the home screen
|
||||||
|
// this allows the task switcher to move left and right without causing problems with the task offset animations
|
||||||
|
property real homeTouchPositionX: 0
|
||||||
|
|
||||||
|
// the touch x position value normalized between 0 and 1
|
||||||
|
// the base value should be 0.5 for when it is on the home screen
|
||||||
|
readonly property real touchPosition: {
|
||||||
|
let value = 0.5
|
||||||
|
if (taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened) {
|
||||||
|
// since the touch position starts at 0, we add half the window width and then divide it by the full width to normalize it
|
||||||
|
value = ((taskSwitcherHelpers.notHomeScreenState ? taskSwitcherState.touchXPosition : 0) + (taskSwitcherHelpers.windowWidth / 2)) / taskSwitcherHelpers.windowWidth
|
||||||
|
value = Math.min(1, Math.max(0, value))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamic task offset animation duration based off of the touch position and task scale
|
||||||
|
function dynamicDuration(left = true) {
|
||||||
|
// if the close animation is running, use the standard long duration time for consistency
|
||||||
|
let duration = Kirigami.Units.longDuration * 1.75
|
||||||
|
if (!taskSwitcherHelpers.closeAnim.running && taskSwitcherHelpers.notHomeScreenState && taskSwitcherHelpers.gestureState != TaskSwitcherHelpers.GestureStates.HorizontalSwipe && !taskSwitcherHelpers.isInTaskScrubMode) {
|
||||||
|
// max out the scale at 1 so it is not too fast when opening the task drawer with the button
|
||||||
|
let taskScale = Math.min(taskSwitcherHelpers.currentScale, 1)
|
||||||
|
// change the duration based off of the touch position and task scale
|
||||||
|
duration = duration * ((left ? touchPosition : (1 - touchPosition)) + 1)
|
||||||
|
}
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// the duration is set to 0 if setOffsetDurationImmediately is true so we can skip the animation
|
||||||
|
readonly property real taskOffsetDurationLeft: setOffsetDurationImmediately ? 0 : dynamicDuration(true)
|
||||||
|
readonly property real taskOffsetDurationRight: setOffsetDurationImmediately ? 0 : dynamicDuration(false)
|
||||||
|
|
||||||
|
// the easing type for the task offset animation
|
||||||
|
property int taskOffsetEasing: Easing.InOutQuart
|
||||||
|
|
||||||
|
// skips the animation and sets the task offset value immediately (only should be set by 'setTaskOffsetValue')
|
||||||
|
property bool setOffsetDurationImmediately: true
|
||||||
|
|
||||||
|
// set the task offset value with an animation unless specified otherwise
|
||||||
|
function setTaskOffsetValue(value, immediately = false, taskEasing = ((taskSwitcherHelpers.notHomeScreenState || (value != 0)) && (baseTaskOffset != taskSwitcherHelpers.taskOffsetValue)) ? Easing.InOutQuart : Easing.OutQuart) {
|
||||||
|
if (baseTaskOffset == value && immediately) {
|
||||||
|
baseTaskOffset = value + 1;
|
||||||
|
}
|
||||||
|
setOffsetDurationImmediately = immediately;
|
||||||
|
taskOffsetEasing = taskEasing;
|
||||||
|
baseTaskOffset = value;
|
||||||
|
}
|
||||||
|
|
||||||
transform: Scale {
|
transform: Scale {
|
||||||
origin.x: root.width / 2
|
origin.x: root.width / 2
|
||||||
origin.y: root.height / 2
|
origin.y: root.height / 2
|
||||||
|
|
@ -95,7 +139,8 @@ MouseArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPressedChanged: {
|
onPressedChanged: {
|
||||||
if (!taskSwitcherState.currentlyBeingOpened && pressed) {
|
// disable if being closed or opened to prevent bugs
|
||||||
|
if (!taskSwitcherHelpers.currentlyBeingOpened && !taskSwitcherHelpers.currentlyBeingClosed && pressed) {
|
||||||
// ensure animations aren't running when finger is pressed
|
// ensure animations aren't running when finger is pressed
|
||||||
taskSwitcherHelpers.cancelAnimations();
|
taskSwitcherHelpers.cancelAnimations();
|
||||||
}
|
}
|
||||||
|
|
@ -111,9 +156,10 @@ MouseArea {
|
||||||
delegate: Task {
|
delegate: Task {
|
||||||
id: task
|
id: task
|
||||||
readonly property int currentIndex: model.index
|
readonly property int currentIndex: model.index
|
||||||
|
readonly property bool isCurrentTask: currentIndex == taskSwitcherHelpers.currentDisplayTask
|
||||||
|
|
||||||
// this is the x-position with respect to the list
|
// this is the x-position with respect to the list
|
||||||
property real listX: taskSwitcherHelpers.xPositionFromTaskIndex(currentIndex);
|
property real listX: taskSwitcherHelpers.xPositionFromTaskIndex(currentIndex)
|
||||||
Behavior on listX {
|
Behavior on listX {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Kirigami.Units.longDuration
|
duration: Kirigami.Units.longDuration
|
||||||
|
|
@ -121,23 +167,62 @@ MouseArea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the animated task offset value (always will be 0 if it is the current task in the task drawer)
|
||||||
|
property real taskOffsetNormalized: (baseTaskOffset * ((taskSwitcherHelpers.taskDrawerOpened && isCurrentTask) ? 0 : 1))
|
||||||
|
Behavior on taskOffsetNormalized {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: taskSwitcherHelpers.currentDisplayTask > currentIndex ? taskOffsetDurationRight : taskOffsetDurationLeft
|
||||||
|
easing.type: taskOffsetEasing
|
||||||
|
easing.overshoot: 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate which direction to offset
|
||||||
|
readonly property real offsetDir: (taskSwitcherHelpers.currentDisplayTask > currentIndex ? -1 : 1)
|
||||||
|
|
||||||
|
// check if this task should be offset
|
||||||
|
readonly property real isOffScreenOffset: {
|
||||||
|
let isOffsetBase = ((!taskSwitcherState.wasInActiveTask && !taskSwitcherHelpers.taskDrawerOpened) || !isCurrentTask) ? (taskSwitcherHelpers.isInTaskScrubMode && taskSwitcherHelpers.notHomeScreenState ? 0 : 1) : 0
|
||||||
|
let isOffsetTaskDrawer = (currentIndex == taskSwitcherHelpers.currentDisplayTask ? 0 : 1)
|
||||||
|
return taskSwitcherHelpers.taskDrawerOpened ? isOffsetTaskDrawer : isOffsetBase
|
||||||
|
}
|
||||||
|
|
||||||
|
// how far the task needs to travel to be off screen
|
||||||
|
readonly property real scrollXOffset: Math.abs(taskSwitcherHelpers.xPositionFromTaskIndex(taskSwitcherHelpers.currentDisplayTask) - (taskSwitcherState.xPosition + (taskSwitcherState.touchXPosition / taskSwitcherHelpers.currentScale)))
|
||||||
|
readonly property real offScreenOffset: (taskSwitcherHelpers.windowWidth * (((taskSwitcherHelpers.notHomeScreenState ? taskSwitcherState.touchXPosition : 0) * offsetDir * ((homeTouchPositionX == 0) ? 1 : 0) + (taskSwitcherHelpers.windowWidth / 2)) / taskSwitcherHelpers.windowWidth));
|
||||||
|
|
||||||
|
|
||||||
|
// calculate the actual task offset
|
||||||
|
readonly property real taskOffset: ((offScreenOffset + (taskSwitcherHelpers.notHomeScreenState ? scrollXOffset : 0)) / taskSwitcherHelpers.currentScale - (homeTouchPositionX * (1 - Math.max(0, Math.min(1, (taskOffsetNormalized - taskSwitcherHelpers.peekOffsetValue) / (taskSwitcherHelpers.homeOffsetValue - taskSwitcherHelpers.peekOffsetValue)))))) * taskOffsetNormalized * isOffScreenOffset * offsetDir
|
||||||
|
|
||||||
|
// extra resistance calculated for non-current task in the task drawer
|
||||||
|
readonly property real nonCurrentScaleResistance: ((isCurrentTask && taskSwitcherHelpers.notHomeScreenState) || taskSwitcherHelpers.fromButton) ? 0 : 1 - Math.min(taskSwitcherHelpers.currentScale, 1)
|
||||||
|
readonly property real nonCurrentScaleXOffset: (isCurrentTask && taskSwitcherHelpers.notHomeScreenState) ? 0 : ((taskSwitcherHelpers.taskWidth) * (scale - 1) * (currentIndex - taskSwitcherHelpers.currentDisplayTask))
|
||||||
|
readonly property real nonCurrentXPositionResistance: (isCurrentTask && taskSwitcherHelpers.notHomeScreenState) ? 0 : (taskSwitcherHelpers.taskWidth * (scale - 1)) * (taskSwitcherHelpers.notHomeScreenState ? 0.25 : 1.0) * offsetDir
|
||||||
|
readonly property real nonCurrentYPositionResistance: (isCurrentTask && taskSwitcherHelpers.notHomeScreenState) ? 0 : ((taskSwitcher.height / 2)) * nonCurrentScaleResistance
|
||||||
|
|
||||||
// this is the actual displayed x-position on screen
|
// this is the actual displayed x-position on screen
|
||||||
x: listX + repeater.leftMargin - taskSwitcherState.xPosition
|
x: listX + repeater.leftMargin - taskSwitcherState.xPosition - taskOffset - nonCurrentScaleXOffset + nonCurrentXPositionResistance
|
||||||
y: root.taskY
|
y: ((taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened) ? root.taskY + nonCurrentYPositionResistance * 0.5: root.taskY / (taskSwitcherHelpers.fromButton ? 1 : (1 + taskOffsetNormalized * 0.075))) // add more resistance when not the current task
|
||||||
|
|
||||||
|
scale: ((isCurrentTask && taskSwitcherHelpers.notHomeScreenState) || taskSwitcherHelpers.fromButton) ? 1 : (1 + nonCurrentScaleResistance) * (1 + taskOffsetNormalized * 0.075) // add more resistance when not the current task and resist even further if the task is offset
|
||||||
|
|
||||||
// ensure current task is above others
|
// ensure current task is above others
|
||||||
z: taskSwitcherState.currentTaskIndex === currentIndex ? 1 : 0
|
z: isCurrentTask ? 1 : 0
|
||||||
|
|
||||||
// only show header once task switcher is opened
|
// only show header once task switcher is opened
|
||||||
showHeader: !taskSwitcherState.gestureInProgress && !taskSwitcherHelpers.currentlyBeingClosed && !taskSwitcherHelpers.isInTaskScrubMode
|
showHeader: !taskSwitcherState.gestureInProgress && !taskSwitcherHelpers.currentlyBeingClosed && !taskSwitcherHelpers.isInTaskScrubMode
|
||||||
|
|
||||||
// darken effect as task gets away from the center of the screen
|
// darken effect as task gets away from the center of the screen
|
||||||
darken: {
|
darken: {
|
||||||
const distFromCentreProgress = Math.abs(x - repeater.leftMargin) / taskSwitcherHelpers.taskWidth;
|
const distFromCentreProgress = Math.abs(x - repeater.leftMargin - (taskSwitcherHelpers.currentlyBeingOpened || taskSwitcherHelpers.currentlyBeingClosed ? (taskSwitcherHelpers.xPositionFromTaskIndex(taskSwitcherHelpers.currentDisplayTask)) - taskSwitcherState.xPosition : 0)) / taskSwitcherHelpers.taskWidth;
|
||||||
const upperBoundAdjust = Math.min(0.5, distFromCentreProgress) - 0.2;
|
const upperBoundAdjust = Math.min(0.25, distFromCentreProgress) - 0.2;
|
||||||
return Math.max(0, upperBoundAdjust);
|
return Math.max(0, upperBoundAdjust);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fade out as the task closes
|
||||||
|
opacity: taskSwitcherHelpers.closingFactor
|
||||||
|
|
||||||
// update count of tasks being interacted with, so we know whether we are in a swipe up action
|
// update count of tasks being interacted with, so we know whether we are in a swipe up action
|
||||||
onInteractingActiveChanged: {
|
onInteractingActiveChanged: {
|
||||||
let offset = interactingActive ? 1 : -1;
|
let offset = interactingActive ? 1 : -1;
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,27 @@ FocusScope {
|
||||||
// keep track of task list events
|
// keep track of task list events
|
||||||
property int oldTasksCount: tasksCount
|
property int oldTasksCount: tasksCount
|
||||||
onTasksCountChanged: {
|
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) {
|
if (tasksCount === 0 && oldTasksCount !== 0) {
|
||||||
hide();
|
hide();
|
||||||
} else if (tasksCount < oldTasksCount && state.currentTaskIndex >= tasksCount) {
|
} else if (tasksCount < oldTasksCount) {
|
||||||
|
if (state.currentTaskIndex < 0) {
|
||||||
|
// if the user is on the frist 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
|
// if the user is on the last task, and it is closed, scroll left
|
||||||
taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration);
|
taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
oldTasksCount = tasksCount;
|
oldTasksCount = tasksCount;
|
||||||
}
|
}
|
||||||
|
|
@ -66,20 +81,60 @@ FocusScope {
|
||||||
Keys.onEscapePressed: hide();
|
Keys.onEscapePressed: hide();
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
initialSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialSetup() {
|
||||||
|
taskSwitcherHelpers.cancelAnimations();
|
||||||
state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow);
|
state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow);
|
||||||
|
|
||||||
// task index from last time using the switcher
|
// ensure the task drawer is not opened and reset values to defaults
|
||||||
|
taskSwitcherHelpers.reachedHeightThreshold = false;
|
||||||
|
taskSwitcherHelpers.gestureState = TaskSwitcherHelpers.GestureStates.Undecided;
|
||||||
|
taskSwitcherHelpers.isInTaskScrubMode = 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);
|
state.initialTaskIndex = Math.min(state.currentTaskIndex, tasksCount - 1);
|
||||||
if (state.wasInActiveTask) {
|
if (state.wasInActiveTask) {
|
||||||
// if we were in an active task instead set initial task index to the position of that task
|
// 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);
|
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);
|
taskSwitcherHelpers.goToTaskIndex(state.initialTaskIndex);
|
||||||
taskList.minimizeAll();
|
taskList.minimizeAll();
|
||||||
|
|
||||||
// fully open the switcher (if this is a button press, not gesture)
|
// fully open the switcher (if this is a button press, not gesture)
|
||||||
if (!root.state.gestureInProgress) {
|
if (!root.state.gestureInProgress) {
|
||||||
|
taskSwitcherHelpers.fromButton = true;
|
||||||
|
if (state.wasInActiveTask) {
|
||||||
|
taskList.setTaskOffsetValue(0, true);
|
||||||
|
} else {
|
||||||
|
taskList.setTaskOffsetValue(0);
|
||||||
|
}
|
||||||
|
backgroundColorOpacity = 1;
|
||||||
taskSwitcherHelpers.open();
|
taskSwitcherHelpers.open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,8 +157,9 @@ FocusScope {
|
||||||
|
|
||||||
// task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion
|
// task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion
|
||||||
function taskScrubMode() {
|
function taskScrubMode() {
|
||||||
|
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
|
||||||
if (!taskSwitcherHelpers.isInTaskScrubMode) {
|
if (!taskSwitcherHelpers.isInTaskScrubMode) {
|
||||||
taskSwitcherHelpers.isInTaskScrubMode = true;
|
backgroundColorOpacity = 1;
|
||||||
taskSwitcherHelpers.cancelAnimations();
|
taskSwitcherHelpers.cancelAnimations();
|
||||||
taskSwitcherHelpers.open();
|
taskSwitcherHelpers.open();
|
||||||
if (!taskSwitcherHelpers.hasVibrated) {
|
if (!taskSwitcherHelpers.hasVibrated) {
|
||||||
|
|
@ -111,43 +167,140 @@ FocusScope {
|
||||||
haptics.buttonVibrate();
|
haptics.buttonVibrate();
|
||||||
taskSwitcherHelpers.hasVibrated = true;
|
taskSwitcherHelpers.hasVibrated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
let newTaskIndex = Math.max(0, Math.min(tasksCount - 1, Math.floor(state.touchXPosition / taskSwitcherHelpers.taskScrubDistance) + state.initialTaskIndex));
|
// TODO this makes sense, but makes scrub mode feel a bit weird
|
||||||
if (newTaskIndex != state.currentTaskIndex) {
|
// improve trigger distance logic for task scrub mode to fix
|
||||||
|
let newTaskIndex = Math.max(0, Math.min(tasksCount - 1, Math.floor(state.touchXPosition / taskSwitcherHelpers.taskScrubDistance) + state.initialTaskIndex - (state.wasInActiveTask ? 0 : 1)));
|
||||||
|
if (newTaskIndex != state.currentTaskIndex || !taskSwitcherHelpers.isInTaskScrubMode) {
|
||||||
taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex);
|
taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex);
|
||||||
|
taskSwitcherHelpers.isInTaskScrubMode = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchPositionChanged() {
|
function onTouchPositionChanged() {
|
||||||
|
let unmodifiedYposition = Math.abs(state.touchYPosition)
|
||||||
if (taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX
|
if (taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX
|
||||||
(Math.abs(state.xVelocity) > Math.abs(state.yVelocity) * 3 && // gesture needs to be almost completely horizontal
|
(Math.abs(state.xVelocity) > Math.abs(state.yVelocity) * 3 && // gesture needs to be almost completely horizontal
|
||||||
Math.abs(state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward
|
Math.abs(state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward
|
||||||
Math.abs(state.touchXPosition) > taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways
|
Math.abs(state.touchXPosition) > taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways
|
||||||
state.yPosition < taskSwitcherHelpers.undoYThreshold && // and be close to the screen edge
|
unmodifiedYposition < Kirigami.Units.largeSpacing * 2 && // and be close to the screen edge
|
||||||
tasksCount > 1 // and there needs to be more than one task open
|
tasksCount > 0 && // and there needs to be more than none task open
|
||||||
|
!taskSwitcherHelpers.taskDrawerOpened // and the task drawer must not be open
|
||||||
)) {
|
)) {
|
||||||
taskScrubMode();
|
taskScrubMode();
|
||||||
} else {
|
} else {
|
||||||
if (state.status == TaskSwitcherData.TaskSwitcherState.Active) {
|
if (taskSwitcherHelpers.currentlyBeingClosed) {
|
||||||
// task switcher is already open
|
// if the task switch is still open but playing the close animation
|
||||||
// TODO add some sort of feedback for dismissing task switcher (maybe opacity reduction?)
|
// setup some values and return to the initial setup so that the user can always navigate with no down time
|
||||||
return;
|
state.wasInActiveTask = taskSwitcherHelpers.openAppAnim.running ? true : false
|
||||||
|
taskList.setTaskOffsetValue(state.wasInActiveTask ? taskSwitcherHelpers.taskOffsetValue : taskSwitcherHelpers.homeOffsetValue, true);
|
||||||
|
state.status = !state.wasInActiveTask ? (taskSwitcherHelpers.openAppAnim.closeAnim && !taskSwitcherHelpers.taskDrawerWillOpen ? TaskSwitcherData.TaskSwitcherState.Active : TaskSwitcherData.TaskSwitcherState.Inactive) : TaskSwitcherData.TaskSwitcherState.Inactive
|
||||||
|
initialSetup();
|
||||||
|
} else if (taskSwitcherHelpers.openAnim.running) {
|
||||||
|
taskSwitcherHelpers.cancelAnimations();
|
||||||
|
state.status = taskSwitcherHelpers.stateClass.Active;
|
||||||
}
|
}
|
||||||
state.yPosition = Math.abs(state.touchYPosition);
|
|
||||||
state.xPosition = taskSwitcherHelpers.xPositionFromTaskIndex(state.initialTaskIndex) - state.touchXPosition;
|
state.yPosition = unmodifiedYposition + (taskSwitcherHelpers.taskDrawerOpened || !state.wasInActiveTask ? taskSwitcherHelpers.openedYPosition : 0);
|
||||||
|
|
||||||
|
let newXPosition = taskSwitcherHelpers.xPositionFromTaskIndex(state.initialTaskIndex);
|
||||||
|
if (taskSwitcherHelpers.notHomeScreenState && !taskSwitcherHelpers.currentlyBeingClosed) {
|
||||||
|
newXPosition = newXPosition - (state.touchXPosition / taskSwitcherHelpers.currentScale);
|
||||||
|
}
|
||||||
|
state.xPosition = newXPosition;
|
||||||
|
|
||||||
|
// allows the user to move the task drawer left and right when on the home screen
|
||||||
|
taskList.homeTouchPositionX = taskSwitcherHelpers.notHomeScreenState ? 0 : (state.touchXPosition * 0.35);
|
||||||
|
|
||||||
|
// dynamically update the task switcher state based off of the touch position and velocity
|
||||||
|
updateTaskSwitcherState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions on an upwards flick
|
function updateTaskSwitcherState() {
|
||||||
function upwardsFlick() {
|
let unmodifiedYposition = Math.abs(state.touchYPosition)
|
||||||
if (state.wasInActiveTask) {
|
|
||||||
// go to homescreen if we were in an active task
|
// if the touch is above heightThreshold, set reachedHeightThreshold to true
|
||||||
taskSwitcherHelpers.close();
|
if (unmodifiedYposition > 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
|
||||||
|
taskSwitcherHelpers.reachedHeightThreshold = true;
|
||||||
|
backgroundColorOpacity = taskSwitcherHelpers.notHomeScreenState ? 0 : 1;
|
||||||
|
} else if (unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
|
||||||
|
backgroundColorOpacity = 1;
|
||||||
} else {
|
} else {
|
||||||
// or normally open task switcher if we were on the homescreen already
|
backgroundColorOpacity = taskSwitcherHelpers.notHomeScreenState ? 1 : 0;
|
||||||
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
}
|
||||||
taskSwitcherHelpers.open();
|
|
||||||
|
if (state.totalSquaredVelocity > 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 (state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) {
|
||||||
|
// downwards flick
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided);
|
||||||
|
if (unmodifiedYposition < taskSwitcherHelpers.undoYThreshold) {
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.notHomeScreenState ? 0 : taskSwitcherHelpers.homeOffsetValue);
|
||||||
|
}
|
||||||
|
} else if (-state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio || (taskSwitcherHelpers.reachedHeightThreshold && taskSwitcherHelpers.notHomeScreenState)) {
|
||||||
|
// upwards flick or if the touch is above heightThreshold
|
||||||
|
if (taskSwitcherHelpers.notHomeScreenState) {
|
||||||
|
// if in app or task switcher, go home
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
|
||||||
|
if (taskSwitcherHelpers.reachedHeightThreshold) {
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.taskOffsetValue);
|
||||||
|
}
|
||||||
|
} else if (unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
|
||||||
|
// else, keep the task switcher in view
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.peekOffsetValue);
|
||||||
|
}
|
||||||
|
} else if (!taskSwitcherHelpers.reachedHeightThreshold && !taskSwitcherHelpers.isInTaskScrubMode) {
|
||||||
|
// sideways flick
|
||||||
|
if (taskSwitcherHelpers.notHomeScreenState) {
|
||||||
|
taskList.setTaskOffsetValue(0, unmodifiedYposition < taskSwitcherHelpers.openedYPosition ? true : false);
|
||||||
|
}
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.HorizontalSwipe);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
|
||||||
|
// if just moveing out of undoYThreshold, set the state to home
|
||||||
|
if (taskSwitcherHelpers.gestureState < TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home)
|
||||||
|
}
|
||||||
|
// if the touch is above heightThreshold, it will retrun home
|
||||||
|
if (unmodifiedYposition > taskSwitcherHelpers.heightThreshold) {
|
||||||
|
taskSwitcherHelpers.hasVibrated = true;
|
||||||
|
if (taskSwitcherHelpers.notHomeScreenState) {
|
||||||
|
// move the task switcher out of view
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.taskOffsetValue);
|
||||||
|
} else {
|
||||||
|
// keep the task switcher in view when above heightThreshold and from home
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.peekOffsetValue);
|
||||||
|
}
|
||||||
|
// minus largeSpacing from the heightThreshold to help prevent flickering when the task switcher moves in and out of view
|
||||||
|
} else if ((unmodifiedYposition < taskSwitcherHelpers.heightThreshold - Kirigami.Units.largeSpacing) || 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
|
||||||
|
taskSwitcherHelpers.reachedHeightThreshold = false;
|
||||||
|
if (state.totalSquaredVelocity < state.flickVelocityThreshold && taskSwitcherHelpers.taskSwitchCanLaunch) {
|
||||||
|
// if velocity is small enough, move the task switcher into view
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.notHomeScreenState ? 0 : taskSwitcherHelpers.peekOffsetValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if under the undo threshold, it will go back to the task switcher if it is open
|
||||||
|
if (taskSwitcherHelpers.taskDrawerOpened) {
|
||||||
|
taskSwitcherHelpers.reachedHeightThreshold = false;
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher)
|
||||||
|
taskList.setTaskOffsetValue(0);
|
||||||
|
} else {
|
||||||
|
taskSwitcherHelpers.reachedHeightThreshold = false;
|
||||||
|
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided)
|
||||||
|
taskList.setTaskOffsetValue(taskSwitcherHelpers.notHomeScreenState ? 0 : taskSwitcherHelpers.homeOffsetValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,26 +315,43 @@ FocusScope {
|
||||||
// diagonal quick switch gesture logic
|
// diagonal quick switch gesture logic
|
||||||
function quickSwitch() {
|
function quickSwitch() {
|
||||||
// should "quick switch" to adjacent app in task switcher, but only if we were in an app before
|
// should "quick switch" to adjacent app in task switcher, but only if we were in an app before
|
||||||
|
let unmodifiedYposition = Math.abs(state.touchYPosition)
|
||||||
let newIndex = state.currentTaskIndex;
|
let newIndex = state.currentTaskIndex;
|
||||||
let shouldSwitch = false;
|
let shouldSwitch = false;
|
||||||
if (state.xVelocity > 0 && state.wasInActiveTask) {
|
if (state.xVelocity > 0) {
|
||||||
// flick to the right, go to app to the left
|
if (taskSwitcherHelpers.notHomeScreenState) {
|
||||||
|
// flick to the right, go to the app on the left
|
||||||
newIndex = state.currentTaskIndex + 1;
|
newIndex = state.currentTaskIndex + 1;
|
||||||
|
}
|
||||||
if (newIndex < tasksCount) {
|
if (newIndex < tasksCount) {
|
||||||
// switch only if flick doesn't go over end of list
|
// switch only if flick doesn't go over end of list
|
||||||
shouldSwitch = true;
|
shouldSwitch = true;
|
||||||
}
|
}
|
||||||
} else if (state.xVelocity < 0 && state.wasInActiveTask) {
|
} else if (state.xVelocity < 0) {
|
||||||
|
if (taskSwitcherHelpers.notHomeScreenState) {
|
||||||
// flick to the left, go to app to the right
|
// flick to the left, go to app to the right
|
||||||
newIndex = state.currentTaskIndex - 1;
|
newIndex = state.currentTaskIndex - 1;
|
||||||
if (newIndex >= 0) {
|
if (newIndex >= 0) {
|
||||||
// switch only if flick doesn't go over end of list
|
// switch only if flick doesn't go over end of list
|
||||||
shouldSwitch = true;
|
shouldSwitch = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// flick to the left on the home screen, dismiss the gesture
|
||||||
|
taskSwitcherHelpers.close();
|
||||||
|
retrun;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (shouldSwitch) {
|
if (shouldSwitch) {
|
||||||
|
if (!taskSwitcherHelpers.taskDrawerOpened && unmodifiedYposition < taskSwitcherHelpers.openedYPosition) {
|
||||||
|
// if in a app, switch it to the new task when it is under the openedYPosition
|
||||||
|
taskList.setTaskOffsetValue(0, unmodifiedYposition < taskSwitcherHelpers.openedYPosition && taskSwitcherHelpers.notHomeScreenState);
|
||||||
let appAtNewIndex = taskList.getTaskAt(newIndex).window;
|
let appAtNewIndex = taskList.getTaskAt(newIndex).window;
|
||||||
taskSwitcherHelpers.openApp(newIndex, appAtNewIndex, Kirigami.Units.longDuration * 4, Easing.OutExpo);
|
taskSwitcherHelpers.openApp(newIndex, appAtNewIndex, 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
|
||||||
|
taskSwitcherHelpers.animateGoToTaskIndex(newIndex);
|
||||||
|
taskSwitcherHelpers.open();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// if not switching, just open task switcher
|
// if not switching, just open task switcher
|
||||||
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
||||||
|
|
@ -191,75 +361,82 @@ FocusScope {
|
||||||
|
|
||||||
// Logic for deciding how to handle the end of a gesture input
|
// Logic for deciding how to handle the end of a gesture input
|
||||||
function onGestureInProgressChanged() {
|
function onGestureInProgressChanged() {
|
||||||
|
taskSwitcherHelpers.fromButton = false;
|
||||||
if (state.gestureInProgress) {
|
if (state.gestureInProgress) {
|
||||||
|
taskSwitcherHelpers.currentDisplayTask = state.currentTaskIndex;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == TaskSwitcherData.TaskSwitcherState.Active) {
|
if (taskList.count === 0) {
|
||||||
if (taskSwitcherHelpers.isInTaskScrubMode) {
|
// dismiss the gesture if the task list is empty
|
||||||
|
taskSwitcherHelpers.close();
|
||||||
|
} if (taskSwitcherHelpers.isInTaskScrubMode) {
|
||||||
// TODO! do we want to handle upwards flick to dismiss in task scrub mode?
|
// TODO! do we want to handle upwards flick to dismiss in task scrub mode?
|
||||||
// TODO do we want to show a list of thumbnails in task scrub mode?
|
// TODO do we want to show a list of thumbnails in task scrub mode?
|
||||||
|
let unmodifiedYposition = Math.abs(state.touchYPosition)
|
||||||
|
backgroundColorOpacity = 1;
|
||||||
|
if (taskSwitcherHelpers.taskDrawerOpened || unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
|
||||||
|
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
||||||
|
taskSwitcherHelpers.open();
|
||||||
|
taskSwitcherHelpers.isInTaskScrubMode = false;
|
||||||
|
} else {
|
||||||
taskSwitcherHelpers.openApp(state.currentTaskIndex, taskList.getTaskAt(state.currentTaskIndex).window);
|
taskSwitcherHelpers.openApp(state.currentTaskIndex, taskList.getTaskAt(state.currentTaskIndex).window);
|
||||||
} else if (state.yPosition > taskSwitcherHelpers.undoYThreshold) {
|
}
|
||||||
// close task switcher if it was already open but only if swipe was higher than the undo threshold
|
} else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Undecided) {
|
||||||
|
if (taskSwitcherHelpers.taskDrawerOpened) {
|
||||||
|
// if in the task switcher, return to it
|
||||||
|
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
||||||
|
taskSwitcherHelpers.open();
|
||||||
|
} else if (state.wasInActiveTask) {
|
||||||
|
// if inside a app, return to it
|
||||||
|
returnToApp();
|
||||||
|
} else {
|
||||||
|
// else dismiss the gesture
|
||||||
taskSwitcherHelpers.close();
|
taskSwitcherHelpers.close();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else if (state.status == TaskSwitcherData.TaskSwitcherState.Inactive) {
|
} else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.HorizontalSwipe) {
|
||||||
if (state.totalSquaredVelocity > 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 (-state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) {
|
|
||||||
upwardsFlick();
|
|
||||||
} else if (state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) {
|
|
||||||
// downwards flick
|
|
||||||
returnToApp();
|
|
||||||
} else {
|
|
||||||
// sideways flick
|
// sideways flick
|
||||||
|
backgroundColorOpacity = 1;
|
||||||
quickSwitch();
|
quickSwitch();
|
||||||
}
|
} else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
|
||||||
} else {
|
// open the task drawer
|
||||||
// no flick
|
backgroundColorOpacity = 1;
|
||||||
if (state.yPosition > taskSwitcherHelpers.undoYThreshold) {
|
|
||||||
// normal task switcher open
|
|
||||||
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
||||||
taskSwitcherHelpers.open();
|
taskSwitcherHelpers.open();
|
||||||
} else {
|
} else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Home) {
|
||||||
// no flick and not enough activation to go to task switcher
|
taskSwitcherHelpers.close();
|
||||||
if (state.wasInActiveTask) {
|
|
||||||
returnToApp();
|
|
||||||
} else {
|
|
||||||
// do open switcher in case we were on homescreen before
|
|
||||||
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
|
|
||||||
state.yPosition = taskSwitcherHelpers.openedYPosition;
|
|
||||||
taskSwitcherHelpers.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onVelocityChanged() {
|
function onVelocityChanged() {
|
||||||
if (!taskSwitcherHelpers.hasVibrated) {
|
|
||||||
if (!state.wasInActiveTask ||
|
|
||||||
(state.wasInActiveTask &&
|
|
||||||
state.yPosition > taskSwitcherHelpers.undoYThreshold &&
|
|
||||||
state.totalSquaredVelocity < state.flickVelocityThreshold)) {
|
|
||||||
// Haptic feedback when conditions are met for the task switcher to open
|
|
||||||
haptics.buttonVibrate();
|
|
||||||
taskSwitcherHelpers.hasVibrated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function onXPositionChanged() {
|
function onXPositionChanged() {
|
||||||
taskSwitcherHelpers.updateTaskIndex();
|
taskSwitcherHelpers.updateTaskIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// kind of a hack, but this prevents the gesture from immediately activting the task switcher when it is not supposed to
|
||||||
|
Timer {
|
||||||
|
id: taskSwitchCanLaunchTimer
|
||||||
|
interval: 1; running: true; repeat: false
|
||||||
|
onTriggered: taskSwitcherHelpers.taskSwitchCanLaunch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTaskDrawerState(value) {
|
||||||
|
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
|
// view of the desktop background
|
||||||
KWinComponents.DesktopBackground {
|
KWinComponents.DesktopBackground {
|
||||||
id: backgroundItem
|
id: backgroundItem
|
||||||
|
|
@ -275,13 +452,17 @@ FocusScope {
|
||||||
|
|
||||||
opacity: container.opacity
|
opacity: container.opacity
|
||||||
color: {
|
color: {
|
||||||
// animate background colour only if we are *not* opening from the homescreen
|
return Qt.rgba(0, 0, 0, 0.6 * taskSwitcherHelpers.closingFactor * backgroundColorOpacity);
|
||||||
if (state.wasInActiveTask || !state.currentlyBeingOpened) {
|
|
||||||
return Qt.rgba(0, 0, 0, 0.6);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, state.yPosition / state.openedYPosition));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// status bar
|
||||||
|
|
@ -413,7 +594,11 @@ FocusScope {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: placeholder
|
id: placeholder
|
||||||
spacing: Kirigami.Units.gridUnit
|
spacing: Kirigami.Units.gridUnit
|
||||||
opacity: (root.tasksCount === 0 && !taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0
|
|
||||||
|
opacity: {
|
||||||
|
let baseOpacity = ((root.tasksCount === 0 && !taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0);
|
||||||
|
return taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher ? baseOpacity : 0;
|
||||||
|
}
|
||||||
Behavior on opacity { NumberAnimation { duration: 500 } }
|
Behavior on opacity { NumberAnimation { duration: 500 } }
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
@ -549,22 +734,22 @@ FocusScope {
|
||||||
x: flickable.contentX
|
x: flickable.contentX
|
||||||
width: flickable.width
|
width: flickable.width
|
||||||
height: flickable.height
|
height: flickable.height
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
PlasmaComponents.ToolButton {
|
PlasmaComponents.ToolButton {
|
||||||
id: closeAllButton
|
id: closeAllButton
|
||||||
property bool closeRequested: false
|
property bool closeRequested: false
|
||||||
visible: root.tasksCount !== 0 && !taskSwitcherHelpers.isInTaskScrubMode
|
visible: root.tasksCount !== 0 && !taskSwitcherHelpers.isInTaskScrubMode
|
||||||
|
enabled: !taskSwitcherHelpers.currentlyBeingClosed && !root.state.gestureInProgress
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
bottom: parent.bottom
|
bottom: parent.bottom
|
||||||
bottomMargin: (taskList.taskY + taskList.trackFingerYOffset) / 2
|
bottomMargin: (taskList.taskYBase) * 0.75
|
||||||
horizontalCenter: parent.horizontalCenter
|
horizontalCenter: taskList.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
opacity: (taskSwitcherHelpers.currentlyBeingClosed || root.state.gestureInProgress || !taskSwitcherHelpers.taskDrawerOpened) ? 0.0 : 1.0
|
||||||
Kirigami.Theme.inherit: false
|
|
||||||
|
|
||||||
opacity: (taskSwitcherHelpers.currentlyBeingClosed) ? 0.0 : 1.0
|
|
||||||
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
|
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
|
||||||
|
|
||||||
icon.name: "edit-clear-history"
|
icon.name: "edit-clear-history"
|
||||||
|
|
@ -582,6 +767,5 @@ FocusScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
|
|
||||||
import org.kde.kirigami 2.20 as Kirigami
|
import org.kde.kirigami 2.20 as Kirigami
|
||||||
|
import org.kde.private.mobileshell.taskswitcher 1.0 as TaskSwitcherData
|
||||||
|
|
||||||
import org.kde.kwin 3.0 as KWinComponents
|
import org.kde.kwin 3.0 as KWinComponents
|
||||||
|
|
||||||
|
|
@ -14,11 +15,27 @@ QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// TaskSwitcher item component
|
// TaskSwitcher item component
|
||||||
// We assume that the taskSwitcher the size of the entire screen.
|
// We assume that the taskSwitcher is the size of the entire screen.
|
||||||
required property var taskSwitcher
|
required property var taskSwitcher
|
||||||
property var state: taskSwitcher.state
|
property var state: taskSwitcher.state
|
||||||
required property var stateClass
|
required property var stateClass
|
||||||
|
|
||||||
|
// task switcher peek and pop setting for when it is toggled from the home screen
|
||||||
|
readonly property real peekOffsetValue: 1.85
|
||||||
|
readonly property real homeOffsetValue: 2.6
|
||||||
|
readonly property real taskOffsetValue: 1.5
|
||||||
|
|
||||||
|
// the index of the last task to be closed
|
||||||
|
// this will get reset to -1 in TaskSwitcher.qml when the TasksCount changes
|
||||||
|
property int lastClosedTask: -1
|
||||||
|
|
||||||
|
// this will only gets set to the currentTaskIndex when the gesture starts
|
||||||
|
// this helps remove visual glitches as the gesture animations play out and the currentTaskIndex changes
|
||||||
|
property int currentDisplayTask: state.currentTaskIndex
|
||||||
|
|
||||||
|
// how much the the task will resist shrinking
|
||||||
|
// value of 1 will match the y position resistance one to one
|
||||||
|
readonly property real scaleResistance: 0.5
|
||||||
|
|
||||||
// direction of the movement
|
// direction of the movement
|
||||||
readonly property bool gestureMovingRight: state.xVelocity > 0
|
readonly property bool gestureMovingRight: state.xVelocity > 0
|
||||||
|
|
@ -33,10 +50,37 @@ QtObject {
|
||||||
// yPosition threshold below which opening the task switcher should be undone and returned to the previously active task
|
// yPosition threshold below which opening the task switcher should be undone and returned to the previously active task
|
||||||
readonly property real undoYThreshold: openedYPosition / 2
|
readonly property real undoYThreshold: openedYPosition / 2
|
||||||
|
|
||||||
|
// the height threshold where if the yPosition is above this value the task switch will return home
|
||||||
|
readonly property real heightThreshold: windowHeight * 0.55
|
||||||
|
|
||||||
|
// whether the switcher is opened or not
|
||||||
|
readonly property bool taskDrawerOpened: state.status == TaskSwitcherData.TaskSwitcherState.Active
|
||||||
|
|
||||||
|
// This is true when the task drawer is already opened or if within an app
|
||||||
|
readonly property bool notHomeScreenState: state.wasInActiveTask || taskDrawerOpened
|
||||||
|
|
||||||
|
// set to true if the taskSwitcher is opened by the navbar button
|
||||||
|
property bool fromButton: false
|
||||||
|
|
||||||
|
// gets set to true after 1 milliseconds
|
||||||
|
property bool taskSwitchCanLaunch: false
|
||||||
|
|
||||||
// whether the switcher has already triggered haptic feedback or not
|
// whether the switcher has already triggered haptic feedback or not
|
||||||
// we don't want to continuously send haptics, just once is enough
|
// we don't want to continuously send haptics, just once is enough
|
||||||
property bool hasVibrated: false
|
property bool hasVibrated: false
|
||||||
|
|
||||||
|
// The current gesture state to decide what will happpen when it is completed
|
||||||
|
enum GestureStates {
|
||||||
|
Undecided,
|
||||||
|
HorizontalSwipe,
|
||||||
|
TaskSwitcher,
|
||||||
|
Home
|
||||||
|
}
|
||||||
|
property int gestureState: TaskSwitcherHelpers.GestureStates.Undecided
|
||||||
|
|
||||||
|
// if the touch has reached the height threshold
|
||||||
|
property bool reachedHeightThreshold: false
|
||||||
|
|
||||||
// made as variables to keep x anim in task list and task scrub icon list in sync
|
// made as variables to keep x anim in task list and task scrub icon list in sync
|
||||||
property int xAnimDuration: Kirigami.Units.longDuration * 2
|
property int xAnimDuration: Kirigami.Units.longDuration * 2
|
||||||
property int xAnimEasingType: Easing.OutExpo
|
property int xAnimEasingType: Easing.OutExpo
|
||||||
|
|
@ -60,6 +104,24 @@ QtObject {
|
||||||
// height of the task preview header
|
// height of the task preview header
|
||||||
readonly property real taskHeaderHeight: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2
|
readonly property real taskHeaderHeight: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2
|
||||||
|
|
||||||
|
// finger position y with resistance
|
||||||
|
readonly property real trackFingerYOffset: {
|
||||||
|
if (taskSwitcherHelpers.isScaleClamped) {
|
||||||
|
let directTrackingOffset = openedYPosition * 0.2
|
||||||
|
if (root.state.yPosition < openedYPosition + directTrackingOffset) {
|
||||||
|
// Allow the task list to move further up than the fully opened position
|
||||||
|
return root.state.yPosition - openedYPosition;
|
||||||
|
} else {
|
||||||
|
// but make it more reluctant the further up it goes
|
||||||
|
let overDragProgress = (root.state.yPosition - directTrackingOffset - openedYPosition) / openedYPosition;
|
||||||
|
// Base formula is 1-2.3^(-progress) which asymptotically approaches 1
|
||||||
|
return (1 - Math.pow(2.3, -overDragProgress)) * openedYPosition + directTrackingOffset;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// the scaling factor of the window preview compared to the actual window
|
// the scaling factor of the window preview compared to the actual window
|
||||||
// we need to ensure that window previews always fit on screen
|
// we need to ensure that window previews always fit on screen
|
||||||
readonly property real scalingFactor: {
|
readonly property real scalingFactor: {
|
||||||
|
|
@ -75,27 +137,26 @@ QtObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scaling factor during closing of the switcher
|
// the closing factor during the closing of the switcher
|
||||||
|
property real closingFactor: 1
|
||||||
|
|
||||||
|
// scaling factor during the closing of the switcher
|
||||||
property real closingScalingFactor: 1
|
property real closingScalingFactor: 1
|
||||||
|
|
||||||
// scale of the task list (based on the progress of the swipe up gesture)
|
// scale of the task list (based on the progress of the swipe up gesture)
|
||||||
readonly property real currentScale: {
|
readonly property real currentScale: {
|
||||||
let maxScale = 1 / scalingFactor;
|
let maxScale = 1 / scalingFactor;
|
||||||
let subtract = (maxScale - 1) * Math.min(root.state.yPosition / openedYPosition, 1);
|
let subtract = (maxScale - 1) * ((Math.min(root.state.yPosition, openedYPosition) + trackFingerYOffset * scaleResistance) / openedYPosition)
|
||||||
let finalScale = Math.min(maxScale, maxScale - subtract);
|
let finalScale = Math.min(maxScale, maxScale - subtract);
|
||||||
|
|
||||||
// if closing scaling factor is below 1 we want it to override the other scale
|
// if closing scaling factor is below 1 we want it to override the other scale
|
||||||
// to allow for a smoother closing animation
|
// to allow for a smoother closing animation
|
||||||
if (closingScalingFactor < 1) {
|
if (closingScalingFactor < 1 && (root.state.wasInActiveTask || root.taskDrawerOpened)) {
|
||||||
return closingScalingFactor;
|
return closingScalingFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// animate scale only if we are *not* opening from the homescreen
|
|
||||||
if (root.state.wasInActiveTask || !root.state.gestureInProgress) {
|
|
||||||
return finalScale;
|
return finalScale;
|
||||||
}
|
}
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
readonly property bool isScaleClamped: root.state.yPosition > openedYPosition
|
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
|
readonly property real taskScrubDistance: windowWidth / (2 * 6) // formula says how many tasks can be scrubbed through in half of the window width
|
||||||
|
|
@ -106,10 +167,10 @@ QtObject {
|
||||||
// cancel all animated moving, as another flick source is taking over
|
// cancel all animated moving, as another flick source is taking over
|
||||||
signal cancelAnimations()
|
signal cancelAnimations()
|
||||||
onCancelAnimations: {
|
onCancelAnimations: {
|
||||||
openAnim.stop();
|
|
||||||
openAppAnim.stop();
|
openAppAnim.stop();
|
||||||
closeAnim.stop();
|
closeAnim.stop();
|
||||||
closeScaleAnim.stop();
|
closeScaleAnim.stop();
|
||||||
|
closeFactorAnim.stop();
|
||||||
xAnim.stop();
|
xAnim.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,22 +191,35 @@ QtObject {
|
||||||
|
|
||||||
// TODO either use updateTaskIndex to always have the "newest current task index" in the state var or use "getNearestTaskIndex", not both it's redundant
|
// 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() {
|
function updateTaskIndex() {
|
||||||
|
// only set if not gesture currently in progress to prevent glitching
|
||||||
|
if (!(state.gestureInProgress || root.closeAnim.running || root.openAppAnim.running) || root.isInTaskScrubMode) {
|
||||||
root.state.currentTaskIndex = getTaskIndexFromXPosition();
|
root.state.currentTaskIndex = getTaskIndexFromXPosition();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
|
root.gestureState = TaskSwitcherHelpers.GestureStates.TaskSwitcher;
|
||||||
openAnim.restart();
|
openAnim.restart();
|
||||||
|
|
||||||
|
// update the task offset position
|
||||||
|
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
// update the task offset position
|
||||||
|
taskList.setTaskOffsetValue(homeOffsetValue + 0.25, false, Easing.Linear);
|
||||||
|
|
||||||
|
root.gestureState = TaskSwitcherHelpers.GestureStates.Undecided;
|
||||||
cancelAnimations();
|
cancelAnimations();
|
||||||
closingScalingFactor = currentScale;
|
closingScalingFactor = currentScale;
|
||||||
closeAnim.restart();
|
closeAnim.restart();
|
||||||
closeScaleAnim.restart();
|
closeScaleAnim.restart();
|
||||||
|
closeFactorAnim.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openApp(index, window, duration = Kirigami.Units.shortDuration, horizontalEasing = Easing.OutBack) {
|
function openApp(index, window, duration = Kirigami.Units.shortDuration, horizontalEasing = Easing.OutBack) {
|
||||||
// cancel any opening animations ongoing
|
// cancel any opening animations ongoing
|
||||||
|
openAnim.stop();
|
||||||
cancelAnimations();
|
cancelAnimations();
|
||||||
|
|
||||||
animateGoToTaskIndex(index, duration);
|
animateGoToTaskIndex(index, duration);
|
||||||
|
|
@ -167,7 +241,7 @@ QtObject {
|
||||||
function animateGoToTaskIndex(index, duration = Kirigami.Units.longDuration * 2, easing = Easing.OutExpo) {
|
function animateGoToTaskIndex(index, duration = Kirigami.Units.longDuration * 2, easing = Easing.OutExpo) {
|
||||||
xAnimDuration = duration;
|
xAnimDuration = duration;
|
||||||
xAnimEasingType = easing;
|
xAnimEasingType = easing;
|
||||||
xAnim.to = xPositionFromTaskIndex(index);
|
xAnim.to = xPositionFromTaskIndex(index) - (gestureState == TaskSwitcherHelpers.GestureStates.HorizontalSwipe && !state.gestureInProgress && notHomeScreenState ? taskSpacing / 2 : 0);
|
||||||
xAnim.restart();
|
xAnim.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,12 +306,14 @@ QtObject {
|
||||||
property: "yPosition"
|
property: "yPosition"
|
||||||
to: openedYPosition
|
to: openedYPosition
|
||||||
duration: 250
|
duration: 250
|
||||||
easing.type: Easing.OutExpo
|
easing.type: Easing.OutQuart
|
||||||
|
|
||||||
onFinished: {
|
onFinished: {
|
||||||
|
if (!isInTaskScrubMode) {
|
||||||
root.state.status = stateClass.Active;
|
root.state.status = stateClass.Active;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This animation should ideally be replaced by some
|
// TODO: This animation should ideally be replaced by some
|
||||||
// speed tracking to track finger movement better. Until then
|
// speed tracking to track finger movement better. Until then
|
||||||
|
|
@ -267,6 +343,14 @@ QtObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var closeFactorAnim: NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "closingFactor"
|
||||||
|
to: 0
|
||||||
|
duration: Kirigami.Units.longDuration
|
||||||
|
easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
|
||||||
property var openAppAnim: NumberAnimation {
|
property var openAppAnim: NumberAnimation {
|
||||||
target: root.state
|
target: root.state
|
||||||
property: "yPosition"
|
property: "yPosition"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue