mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-08-04 02:34:45 +00:00
taskswitcher: Rewrite without listview and extract state to TaskSwitcherState
This commit is contained in:
parent
7a1a32724b
commit
1c81cd34b4
8 changed files with 542 additions and 192 deletions
|
|
@ -53,11 +53,13 @@ Flickable {
|
||||||
onContentXChanged: mainFlickable.currentIndex = Math.floor(contentX / width)
|
onContentXChanged: mainFlickable.currentIndex = Math.floor(contentX / width)
|
||||||
|
|
||||||
onFooterChanged: {
|
onFooterChanged: {
|
||||||
|
if (footer) {
|
||||||
footer.parent = mainFlickable;
|
footer.parent = mainFlickable;
|
||||||
footer.anchors.left = mainFlickable.left;
|
footer.anchors.left = mainFlickable.left;
|
||||||
footer.anchors.bottom = mainFlickable.bottom;
|
footer.anchors.bottom = mainFlickable.bottom;
|
||||||
footer.anchors.right = mainFlickable.right;
|
footer.anchors.right = mainFlickable.right;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Autoscroll related functions
|
//Autoscroll related functions
|
||||||
function scrollLeft() {
|
function scrollLeft() {
|
||||||
|
|
|
||||||
|
|
@ -69,20 +69,24 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.dragGestureEnabled) {
|
if (root.dragGestureEnabled) {
|
||||||
if (!taskSwitcher.currentlyDragging && Math.abs(startMouseY - oldMouseY) < root.height) {
|
if (!opening && Math.abs(startMouseY - oldMouseY) < root.height) {
|
||||||
oldMouseY = mouse.y;
|
oldMouseY = mouse.y;
|
||||||
return;
|
return;
|
||||||
} else if (mouseArea.pressed) {
|
} else if (mouseArea.pressed) {
|
||||||
taskSwitcher.currentlyDragging = true;
|
opening = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update offsets with drags
|
if (root.taskSwitcher.visible) {
|
||||||
root.taskSwitcher.oldYOffset = root.taskSwitcher.yOffset;
|
// update task switcher drag
|
||||||
root.taskSwitcher.yOffset = Math.max(0, root.taskSwitcher.yOffset - (mouse.y - oldMouseY));
|
let offsetY = (mouse.y - oldMouseY) * 0.5; // we want to make the gesture take a longer swipe than it being pixel perfect
|
||||||
|
let offsetX = (mouse.x - oldMouseX) * 0.5;
|
||||||
|
taskSwitcher.taskSwitcherState.yPosition = Math.max(0, taskSwitcher.taskSwitcherState.yPosition - offsetY);
|
||||||
|
|
||||||
opening = oldMouseY > mouse.y;
|
// TODO add x swipe
|
||||||
|
//taskSwitcher.taskSwitcherState.xPosition -= offsetX;
|
||||||
|
}
|
||||||
|
|
||||||
if (!root.taskSwitcher.visible && Math.abs(startMouseY - mouse.y) > PlasmaCore.Units.gridUnit && root.taskSwitcher.tasksCount) {
|
if (!root.taskSwitcher.visible && Math.abs(startMouseY - mouse.y) > PlasmaCore.Units.gridUnit && taskSwitcher.tasksCount) {
|
||||||
// start task switcher gesture
|
// start task switcher gesture
|
||||||
activeButton = null;
|
activeButton = null;
|
||||||
root.taskSwitcher.show(false);
|
root.taskSwitcher.show(false);
|
||||||
|
|
@ -98,11 +102,8 @@ Item {
|
||||||
onReleased: {
|
onReleased: {
|
||||||
if (activeButton) {
|
if (activeButton) {
|
||||||
activeButton.clicked();
|
activeButton.clicked();
|
||||||
}
|
} else if (root.dragGestureEnabled && taskSwitcher.taskSwitcherState.currentlyBeingOpened) {
|
||||||
|
taskSwitcher.taskSwitcherState.updateState();
|
||||||
if (root.dragGestureEnabled && root.taskSwitcher.currentlyDragging) {
|
|
||||||
root.taskSwitcher.currentlyDragging = false;
|
|
||||||
root.taskSwitcher.snapOffset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
88
components/mobileshell/qml/taskswitcher/FlickContainer.qml
Normal file
88
components/mobileshell/qml/taskswitcher/FlickContainer.qml
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.taskmanager 0.1 as TaskManager
|
||||||
|
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
|
||||||
|
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var taskSwitcherState
|
||||||
|
|
||||||
|
// we use flickable solely for capturing flicks, not positioning elements
|
||||||
|
contentWidth: width + 99999
|
||||||
|
contentHeight: height
|
||||||
|
contentX: startContentX
|
||||||
|
|
||||||
|
readonly property real startContentX: contentWidth / 2
|
||||||
|
|
||||||
|
property bool positionChangedDueToFlickable: false
|
||||||
|
|
||||||
|
// ensure that flickable is not moving when other sources are changing position
|
||||||
|
Connections {
|
||||||
|
target: root.taskSwitcherState
|
||||||
|
|
||||||
|
onXPositionChanged: {
|
||||||
|
if (!root.positionChangedDueToFlickable) {
|
||||||
|
root.cancelMovement();
|
||||||
|
}
|
||||||
|
root.positionChangedDueToFlickable = true;
|
||||||
|
}
|
||||||
|
onYPositionChanged: {
|
||||||
|
if (!root.positionChangedDueToFlickable) {
|
||||||
|
root.cancelMovement();
|
||||||
|
}
|
||||||
|
root.positionChangedDueToFlickable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update position from horizontal flickable movement
|
||||||
|
property real oldContentX
|
||||||
|
onContentXChanged: {
|
||||||
|
positionChangedDueToFlickable = true;
|
||||||
|
taskSwitcherState.xPosition += contentX - oldContentX;
|
||||||
|
oldContentX = contentX;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMovementStarted: taskSwitcherState.cancelAnimations();
|
||||||
|
onMovementEnded: {
|
||||||
|
resetPosition();
|
||||||
|
taskSwitcherState.updateState();
|
||||||
|
}
|
||||||
|
onFlickEnded: {
|
||||||
|
resetPosition();
|
||||||
|
taskSwitcherState.updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDraggingChanged: {
|
||||||
|
if (!dragging) {
|
||||||
|
cancelMovement();
|
||||||
|
resetPosition();
|
||||||
|
taskSwitcherState.updateState();
|
||||||
|
} else {
|
||||||
|
taskSwitcherState.cancelAnimations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelMovement() {
|
||||||
|
root.cancelFlick();
|
||||||
|
|
||||||
|
// HACK: cancelFlick() doesn't seem to cancel flicks...
|
||||||
|
root.flick(-horizontalVelocity, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPosition() {
|
||||||
|
positionChangedDueToFlickable = true;
|
||||||
|
oldContentX = startContentX;
|
||||||
|
contentX = startContentX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
|
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
|
||||||
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
|
|
@ -22,20 +22,23 @@ Item {
|
||||||
required property var model
|
required property var model
|
||||||
required property var displaysModel
|
required property var displaysModel
|
||||||
|
|
||||||
readonly property point taskScreenPoint: Qt.point(model.ScreenGeometry.x, model.ScreenGeometry.y)
|
readonly property point taskScreenPoint: model ? Qt.point(model.ScreenGeometry.x, model.ScreenGeometry.y) : Qt.point(0, 0)
|
||||||
readonly property real dragOffset: -control.y
|
readonly property real dragOffset: -control.y
|
||||||
|
|
||||||
property bool active: model.IsActive
|
property bool active: model ? model.IsActive : false
|
||||||
|
|
||||||
required property real previewHeight
|
required property real previewHeight
|
||||||
required property real previewWidth
|
required property real previewWidth
|
||||||
|
|
||||||
|
property bool showHeader: true
|
||||||
|
|
||||||
property real scale: 1
|
property real scale: 1
|
||||||
|
|
||||||
opacity: 1 - dragOffset / taskSwitcher.height
|
opacity: 1 - dragOffset / taskSwitcher.height
|
||||||
|
|
||||||
//BEGIN functions
|
//BEGIN functions
|
||||||
function syncDelegateGeometry() {
|
function syncDelegateGeometry() {
|
||||||
let pos = pipeWireLoader.mapToItem(tasksView, 0, 0);
|
let pos = pipeWireLoader.mapToItem(delegate, 0, 0);
|
||||||
if (taskSwitcher.visible) {
|
if (taskSwitcher.visible) {
|
||||||
tasksModel.requestPublishDelegateGeometry(tasksModel.index(model.index, 0), Qt.rect(pos.x, pos.y, pipeWireLoader.width, pipeWireLoader.height), pipeWireLoader);
|
tasksModel.requestPublishDelegateGeometry(tasksModel.index(model.index, 0), Qt.rect(pos.x, pos.y, pipeWireLoader.width, pipeWireLoader.height), pipeWireLoader);
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +116,11 @@ Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.minimumHeight: column.height - appView.height
|
Layout.minimumHeight: column.height - appView.height
|
||||||
spacing: PlasmaCore.Units.smallSpacing * 2
|
spacing: PlasmaCore.Units.smallSpacing * 2
|
||||||
|
opacity: delegate.showHeader ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: PlasmaCore.Units.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
PlasmaCore.IconItem {
|
PlasmaCore.IconItem {
|
||||||
Layout.preferredHeight: PlasmaCore.Units.iconSizes.smallMedium
|
Layout.preferredHeight: PlasmaCore.Units.iconSizes.smallMedium
|
||||||
|
|
@ -164,6 +172,9 @@ Item {
|
||||||
Layout.maximumWidth: delegate.previewWidth
|
Layout.maximumWidth: delegate.previewWidth
|
||||||
Layout.maximumHeight: delegate.previewHeight
|
Layout.maximumHeight: delegate.previewHeight
|
||||||
|
|
||||||
|
// prevent thumbnails from "leaking" out of the control
|
||||||
|
clip: true
|
||||||
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
rightPadding: 0
|
rightPadding: 0
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
|
|
|
||||||
91
components/mobileshell/qml/taskswitcher/TaskList.qml
Normal file
91
components/mobileshell/qml/taskswitcher/TaskList.qml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.1
|
||||||
|
|
||||||
|
import org.kde.taskmanager 0.1 as TaskManager
|
||||||
|
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
|
||||||
|
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var taskSwitcher
|
||||||
|
readonly property var taskSwitcherState: taskSwitcher.taskSwitcherState
|
||||||
|
|
||||||
|
opacity: taskSwitcherState.wasInActiveTask ? 1 : Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition)
|
||||||
|
|
||||||
|
// taphandler activates even if delegate touched
|
||||||
|
TapHandler {
|
||||||
|
onPressedChanged: {
|
||||||
|
if (pressed) {
|
||||||
|
// ensure animations aren't running when finger is pressed
|
||||||
|
taskSwitcherState.cancelAnimations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: repeater
|
||||||
|
model: taskSwitcher.model
|
||||||
|
|
||||||
|
// left margin from root edge such that the task is centered
|
||||||
|
readonly property real leftMargin: (root.width / 2) - (taskSwitcherState.taskWidth / 2)
|
||||||
|
|
||||||
|
delegate: Task {
|
||||||
|
id: task
|
||||||
|
|
||||||
|
readonly property int currentIndex: model.index
|
||||||
|
|
||||||
|
// this is the x-position with respect to the list
|
||||||
|
property real listX: taskSwitcherState.xPositionFromTaskIndex(currentIndex);
|
||||||
|
|
||||||
|
Behavior on listX {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: PlasmaCore.Units.longDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the actual displayed x-position on screen
|
||||||
|
x: listX + repeater.leftMargin - taskSwitcherState.xPosition
|
||||||
|
|
||||||
|
// account for system header and footer offset (center the preview image)
|
||||||
|
y: {
|
||||||
|
let headerHeight = MobileShell.TopPanelControls.panelHeight;
|
||||||
|
let footerHeight = MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0;
|
||||||
|
let diff = headerHeight - footerHeight;
|
||||||
|
|
||||||
|
let baseY = (taskSwitcher.height / 2) - (height / 2) - (taskSwitcherState.taskHeaderHeight / 2)
|
||||||
|
|
||||||
|
return baseY + diff / 2 - MobileShell.TopPanelControls.panelHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure current task is above others
|
||||||
|
z: taskSwitcherState.currentTaskIndex === currentIndex ? 1 : 0
|
||||||
|
|
||||||
|
showHeader: currentIndex !== taskSwitcherState.currentTaskIndex || !taskSwitcherState.currentlyBeingOpened
|
||||||
|
|
||||||
|
width: taskSwitcherState.taskWidth
|
||||||
|
height: taskSwitcherState.taskHeight
|
||||||
|
previewWidth: taskSwitcherState.previewWidth
|
||||||
|
previewHeight: taskSwitcherState.previewHeight
|
||||||
|
|
||||||
|
taskSwitcher: root.taskSwitcher
|
||||||
|
displaysModel: root.taskSwitcher.displaysModel
|
||||||
|
|
||||||
|
scale: {
|
||||||
|
if (taskSwitcherState.currentTaskIndex == currentIndex) {
|
||||||
|
return taskSwitcherState.currentScale;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
|
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
|
||||||
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
import QtQuick.Window 2.2
|
|
||||||
|
|
||||||
import org.kde.taskmanager 0.1 as TaskManager
|
import org.kde.taskmanager 0.1 as TaskManager
|
||||||
import org.kde.plasma.core 2.1 as PlasmaCore
|
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||||
|
|
@ -15,109 +15,82 @@ import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
|
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
|
||||||
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
|
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
|
||||||
|
|
||||||
|
import "../components" as Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
visible: false
|
visible: false
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
readonly property real taskPanelHeight: MobileShell.TaskPanelControls.panelHeight
|
// state object
|
||||||
readonly property real taskPanelWidth: MobileShell.TaskPanelControls.panelWidth
|
property var taskSwitcherState: TaskSwitcherState {
|
||||||
readonly property bool isPortrait: MobileShell.TaskPanelControls.isPortrait
|
taskSwitcher: root
|
||||||
|
}
|
||||||
|
|
||||||
// dimensions of a window on the screen
|
// task list model
|
||||||
readonly property real windowHeight: root.height - (root.isPortrait ? root.taskPanelHeight : 0) - MobileShell.TopPanelControls.panelHeight
|
|
||||||
readonly property real windowWidth: root.width - (root.isPortrait ? 0 : root.taskPanelWidth)
|
|
||||||
|
|
||||||
readonly property int tasksCount: root.model.count
|
|
||||||
readonly property int currentTaskIndex: tasksView.currentIndex
|
|
||||||
property TaskManager.TasksModel model
|
property TaskManager.TasksModel model
|
||||||
|
readonly property int tasksCount: model.count
|
||||||
// offset constants
|
|
||||||
readonly property real targetYOffsetDist: root.height - tasksView.height // offset distance to perfect opening
|
|
||||||
readonly property real dismissYOffsetDist: root.height
|
|
||||||
|
|
||||||
// properties controlled from NavigationPanel (swipe to open gesture)
|
|
||||||
property real oldYOffset: 0
|
|
||||||
property real yOffset: 0
|
|
||||||
|
|
||||||
// set from NavigationPanel in taskpanel containment
|
|
||||||
property bool wasInActiveTask: false // whether we were in an app before opening the task switcher
|
|
||||||
property bool currentlyDragging: false // whether we are in a swipe up gesture
|
|
||||||
|
|
||||||
property var displaysModel: MobileShell.DisplaysModel {}
|
property var displaysModel: MobileShell.DisplaysModel {}
|
||||||
|
|
||||||
enum MovementDirection {
|
|
||||||
None = 0,
|
|
||||||
Left,
|
|
||||||
Right
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: MobileShell.HomeScreenControls.taskSwitcherVisible = visible;
|
onVisibleChanged: MobileShell.HomeScreenControls.taskSwitcherVisible = visible;
|
||||||
|
|
||||||
|
property int oldTasksCount: tasksCount
|
||||||
onTasksCountChanged: {
|
onTasksCountChanged: {
|
||||||
if (tasksCount == 0) {
|
if (tasksCount == 0) {
|
||||||
hide();
|
hide();
|
||||||
}
|
} else if (tasksCount < oldTasksCount && taskSwitcherState.currentTaskIndex >= tasksCount - 1) {
|
||||||
|
// if the user is on the last task, and it is closed, scroll left
|
||||||
|
taskSwitcherState.animateGoToTaskIndex(tasksCount - 1, PlasmaCore.Units.longDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
oldTasksCount = tasksCount;
|
||||||
id: backgroundRect
|
}
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.6 * (root.wasInActiveTask ? 1 : Math.min(1, root.yOffset / root.targetYOffsetDist)))
|
|
||||||
|
|
||||||
MouseArea {
|
// TODO close task switcher when an app opens while it is open, otherwise the navbar becomes glitched
|
||||||
anchors.fill: parent
|
// TODO click outside of delegate to close
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//BEGIN functions
|
//BEGIN functions
|
||||||
|
|
||||||
function show(animation) {
|
function show(animation) {
|
||||||
root.yOffset = 0;
|
// reset values
|
||||||
root.wasInActiveTask = root.model.activeTask.row >= 0;
|
taskSwitcherState.cancelAnimations();
|
||||||
|
taskSwitcherState.yPosition = 0;
|
||||||
|
taskSwitcherState.xPosition = 0;
|
||||||
|
taskSwitcherState.wasInActiveTask = root.model.activeTask.row >= 0;
|
||||||
|
taskSwitcherState.currentlyBeingOpened = true;
|
||||||
|
|
||||||
// skip to first active task
|
// skip to first active task
|
||||||
if (root.wasInActiveTask) {
|
if (taskSwitcherState.wasInActiveTask) {
|
||||||
tasksView.currentIndex = root.model.activeTask.row;
|
taskSwitcherState.goToTaskIndex(root.model.activeTask.row);
|
||||||
tasksView.positionViewAtIndex(root.model.activeTask.row, ListView.SnapPosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.visible = true;
|
// show task switcher, hide all running apps
|
||||||
|
visible = true;
|
||||||
|
opacity = 1;
|
||||||
minimizeAll();
|
minimizeAll();
|
||||||
|
|
||||||
// animate app shrink
|
// fully open the panel (if this is a button press, not gesture)
|
||||||
if (animation) {
|
if (animation) {
|
||||||
offsetAnimator.to = root.targetYOffsetDist;
|
taskSwitcherState.open();
|
||||||
offsetAnimator.restart();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function instantHide() {
|
||||||
|
opacity = 0;
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
if (!root.visible) return;
|
closeAnim.restart();
|
||||||
root.visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapOffset() {
|
|
||||||
let movingUp = root.yOffset > root.oldYOffset;
|
|
||||||
|
|
||||||
if (movingUp || root.yOffset >= root.targetYOffsetDist) { // open task switcher and stay
|
|
||||||
offsetAnimator.to = root.targetYOffsetDist;
|
|
||||||
offsetAnimator.restart();
|
|
||||||
} else { // close task switcher and return to app
|
|
||||||
if (!root.wasInActiveTask) { // if pulled up from homescreen, don't activate app
|
|
||||||
offsetAnimator.activateApp = false;
|
|
||||||
}
|
|
||||||
offsetAnimator.to = 0;
|
|
||||||
offsetAnimator.restart();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// scroll to delegate index, and activate it
|
// scroll to delegate index, and activate it
|
||||||
function activateWindow(id) {
|
function activateWindow(id) {
|
||||||
offsetAnimator.to = 0;
|
taskSwitcherState.openApp(id);
|
||||||
offsetAnimator.restart();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSingleActiveWindow(id, delegate) {
|
function setSingleActiveWindow(id) {
|
||||||
if (id < 0) {
|
if (id < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -130,14 +103,14 @@ Item {
|
||||||
root.model.requestActivate(idx);
|
root.model.requestActivate(idx);
|
||||||
} else if (!tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized)) {
|
} else if (!tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized)) {
|
||||||
var geo = tasksModel.data(idx, TaskManager.AbstractTasksModel.ScreenGeometry)
|
var geo = tasksModel.data(idx, TaskManager.AbstractTasksModel.ScreenGeometry)
|
||||||
// Only minimize the other windows in the same screen
|
// only minimize the other windows in the same screen
|
||||||
if (geo === newActiveGeo) {
|
if (geo === newActiveGeo) {
|
||||||
tasksModel.requestToggleMinimized(idx);
|
tasksModel.requestToggleMinimized(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.visible = false;
|
instantHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function minimizeAll() {
|
function minimizeAll() {
|
||||||
|
|
@ -149,40 +122,29 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreAll() {
|
|
||||||
for (var i = 0 ; i < tasksModel.count; i++) {
|
|
||||||
var idx = tasksModel.makeModelIndex(i);
|
|
||||||
if (tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized)) {
|
|
||||||
tasksModel.requestToggleMinimized(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//END functions
|
//END functions
|
||||||
|
|
||||||
// animate app grow and shrink
|
NumberAnimation on opacity {
|
||||||
NumberAnimation on yOffset {
|
id: closeAnim
|
||||||
id: offsetAnimator
|
|
||||||
duration: PlasmaCore.Units.longDuration
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
|
|
||||||
property bool activateApp: true
|
|
||||||
|
|
||||||
// states of to:
|
|
||||||
// 0 - open/resume app (zoom up the thumbnail)
|
|
||||||
// root.targetYOffsetDist - animate shrinking of thumbnail, to listview (open task switcher)
|
|
||||||
to: 0
|
to: 0
|
||||||
|
duration: PlasmaCore.Units.shortDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
onFinished: {
|
onFinished: {
|
||||||
if (to === 0) { // close task switcher, and switch to current app
|
|
||||||
if (!root.visible) return;
|
|
||||||
root.visible = false;
|
root.visible = false;
|
||||||
|
|
||||||
if (activateApp) {
|
|
||||||
setSingleActiveWindow(root.currentTaskIndex);
|
|
||||||
}
|
}
|
||||||
activateApp = true;
|
}
|
||||||
} else if (to == root.dismissYOffsetDist) {
|
|
||||||
root.hide();
|
// background colour
|
||||||
|
Rectangle {
|
||||||
|
id: backgroundRect
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
color: {
|
||||||
|
// animate background colour only if opening from the homescreen
|
||||||
|
if (taskSwitcherState.wasInActiveTask) {
|
||||||
|
return Qt.rgba(0, 0, 0, 0.6);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,73 +154,29 @@ Item {
|
||||||
|
|
||||||
// provide shell margins
|
// provide shell margins
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.rightMargin: root.isPortrait ? 0 : root.taskPanelWidth
|
anchors.rightMargin: MobileShell.TaskPanelControls.isPortrait ? 0 : MobileShell.TaskPanelControls.panelWidth
|
||||||
anchors.bottomMargin: root.isPortrait ? root.taskPanelHeight : 0
|
anchors.bottomMargin: MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0
|
||||||
anchors.topMargin: MobileShell.TopPanelControls.panelHeight
|
anchors.topMargin: MobileShell.TopPanelControls.panelHeight
|
||||||
|
|
||||||
// applications list
|
FlickContainer {
|
||||||
ListView {
|
id: flickable
|
||||||
id: tasksView
|
anchors.fill: parent
|
||||||
opacity: root.wasInActiveTask ? 1 : Math.min(1, root.yOffset / root.targetYOffsetDist)
|
taskSwitcherState: root.taskSwitcherState
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
readonly property real sizeFactor: 0.75
|
// the item is effectively anchored to the flickable bounds
|
||||||
readonly property real taskHeaderHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing * 2
|
QQC2.Control {
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
|
||||||
width: root.windowWidth * sizeFactor
|
x: flickable.contentX
|
||||||
height: root.windowHeight * sizeFactor + taskHeaderHeight
|
width: flickable.width
|
||||||
|
height: flickable.height
|
||||||
model: root.model
|
|
||||||
orientation: ListView.Horizontal
|
|
||||||
|
|
||||||
highlightRangeMode: ListView.StrictlyEnforceRange // ensures currentIndex is updated
|
|
||||||
snapMode: ListView.SnapToItem
|
|
||||||
|
|
||||||
spacing: PlasmaCore.Units.largeSpacing
|
|
||||||
displayMarginBeginning: 2 * (width + spacing)
|
|
||||||
displayMarginEnd: 2 * (width + spacing)
|
|
||||||
displaced: Transition {
|
|
||||||
NumberAnimation { properties: "x,y"; duration: PlasmaCore.Units.longDuration; easing.type: Easing.InOutQuad }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure that window previews are exactly to the scale of the device screen
|
|
||||||
property real scalingFactor: {
|
|
||||||
let candidateHeight = (tasksView.width / root.windowWidth) * root.windowHeight;
|
|
||||||
if (candidateHeight > tasksView.height) {
|
|
||||||
return tasksView.height / root.windowHeight;
|
|
||||||
} else {
|
|
||||||
return tasksView.width / root.windowWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Task {
|
|
||||||
id: task
|
|
||||||
property int curIndex: model.index
|
|
||||||
z: root.currentTaskIndex === curIndex ? 1 : 0
|
|
||||||
width: tasksView.width
|
|
||||||
height: tasksView.height
|
|
||||||
|
|
||||||
|
contentItem: TaskList {
|
||||||
taskSwitcher: root
|
taskSwitcher: root
|
||||||
displaysModel: root.displaysModel
|
}
|
||||||
|
|
||||||
// account for header offset (center the preview)
|
|
||||||
y: -tasksView.taskHeaderHeight / 2
|
|
||||||
|
|
||||||
// scale gesture
|
|
||||||
scale: {
|
|
||||||
let maxScale = 1 / tasksView.scalingFactor;
|
|
||||||
let subtract = (maxScale - 1) * (root.yOffset / root.targetYOffsetDist);
|
|
||||||
let finalScale = Math.max(0, Math.min(maxScale, maxScale - subtract));
|
|
||||||
|
|
||||||
if ((root.wasInActiveTask || !taskSwitcher.currentlyDragging) && root.currentTaskIndex === task.curIndex) {
|
|
||||||
return finalScale;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure that window previews are exactly to the scale of the device screen
|
|
||||||
previewWidth: tasksView.scalingFactor * root.windowWidth
|
|
||||||
previewHeight: tasksView.scalingFactor * root.windowHeight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
239
components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml
Normal file
239
components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||||
|
|
||||||
|
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State object for the task switcher.
|
||||||
|
*/
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// TaskSwitcher item component
|
||||||
|
// We assume that the taskSwitcher the size of the entire screen.
|
||||||
|
required property var taskSwitcher
|
||||||
|
|
||||||
|
|
||||||
|
// ~~ positioning ~~
|
||||||
|
|
||||||
|
// Position of the list view:
|
||||||
|
//
|
||||||
|
// xPosition:
|
||||||
|
// We start at 0, which is the position at which the first task in the task switcher is centered on the screen.
|
||||||
|
// Increasing xPosition results in the task switcher moving forward (to the second task, third task, etc).
|
||||||
|
//
|
||||||
|
// yPosition:
|
||||||
|
// 0 - Start of swipe up gesture, if window was showing, the thumbnail is the size of it
|
||||||
|
// Increasing yPosition results in the task switcher moving up (and thumbnails shrinking)
|
||||||
|
property real xPosition: 0
|
||||||
|
property real yPosition: 0
|
||||||
|
|
||||||
|
// direction of the movement
|
||||||
|
property bool movingRight: false
|
||||||
|
property bool movingUp: false
|
||||||
|
|
||||||
|
// used for calculating movement direction
|
||||||
|
property real oldXPosition: 0
|
||||||
|
property real oldYPosition: 0
|
||||||
|
onXPositionChanged: {
|
||||||
|
movingRight = xPosition > oldXPosition;
|
||||||
|
oldXPosition = xPosition;
|
||||||
|
}
|
||||||
|
onYPositionChanged: {
|
||||||
|
movingUp = yPosition > oldYPosition;
|
||||||
|
oldYPosition = yPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// yPosition when the task switcher is completely open
|
||||||
|
readonly property real openedYPosition: (taskSwitcher.height - taskHeight) / 2
|
||||||
|
|
||||||
|
// ~~ active state ~~
|
||||||
|
|
||||||
|
// whether the user was in an active task before the task switcher was opened
|
||||||
|
property bool wasInActiveTask: false
|
||||||
|
|
||||||
|
// whether we are in a swipe up gesture to open the task switcher
|
||||||
|
property bool currentlyBeingOpened: false
|
||||||
|
|
||||||
|
readonly property int currentTaskIndex: {
|
||||||
|
let candidateIndex = Math.round(xPosition / (taskSpacing + taskWidth));
|
||||||
|
return Math.max(0, Math.min(taskSwitcher.tasksCount - 1, candidateIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~~ measurement constants ~~
|
||||||
|
|
||||||
|
// dimensions of a real window on the screen
|
||||||
|
readonly property real windowHeight: taskSwitcher.height - (MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0) - MobileShell.TopPanelControls.panelHeight
|
||||||
|
readonly property real windowWidth: taskSwitcher.width - (MobileShell.TaskPanelControls.isPortrait ? 0 : MobileShell.TaskPanelControls.panelWidth)
|
||||||
|
|
||||||
|
// dimensions of the task previews
|
||||||
|
readonly property real previewHeight: windowHeight * scalingFactor
|
||||||
|
readonly property real previewWidth: windowWidth * scalingFactor
|
||||||
|
readonly property real taskHeight: previewHeight + taskHeaderHeight
|
||||||
|
readonly property real taskWidth: previewWidth
|
||||||
|
|
||||||
|
// spacing between each task preview
|
||||||
|
readonly property real taskSpacing: PlasmaCore.Units.largeSpacing
|
||||||
|
|
||||||
|
// height of the task preview header
|
||||||
|
readonly property real taskHeaderHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing * 2
|
||||||
|
|
||||||
|
// the scaling factor of the window preview compared to the actual window
|
||||||
|
// we need to ensure that window previews always fit on screen
|
||||||
|
readonly property real scalingFactor: {
|
||||||
|
let candidateFactor = 0.6;
|
||||||
|
let candidateTaskHeight = windowHeight * candidateFactor + taskHeaderHeight;
|
||||||
|
let candidateTaskWidth = windowWidth * candidateFactor;
|
||||||
|
|
||||||
|
let candidateHeight = (candidateTaskWidth / windowWidth) * windowHeight;
|
||||||
|
if (candidateHeight > windowHeight) {
|
||||||
|
return candidateTaskHeight / windowHeight;
|
||||||
|
} else {
|
||||||
|
return candidateTaskWidth / windowWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scale of the task list (based on the progress of the swipe up gesture)
|
||||||
|
readonly property real currentScale: {
|
||||||
|
let maxScale = 1 / scalingFactor;
|
||||||
|
let subtract = (maxScale - 1) * (yPosition / openedYPosition);
|
||||||
|
let finalScale = Math.max(0, Math.min(maxScale, maxScale - subtract));
|
||||||
|
|
||||||
|
if (wasInActiveTask || !currentlyBeingOpened) {
|
||||||
|
return finalScale;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~~ signals and functions ~~
|
||||||
|
|
||||||
|
// cancel all animated moving, as another flick source is taking over
|
||||||
|
signal cancelAnimations()
|
||||||
|
onCancelAnimations: {
|
||||||
|
openAnim.stop();
|
||||||
|
openAppAnim.stop();
|
||||||
|
closeAnim.stop();
|
||||||
|
xAnim.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
openAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
closeAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openApp(index) {
|
||||||
|
animateGoToTaskIndex(index, PlasmaCore.Units.shortDuration);
|
||||||
|
openAppAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the xPosition where the task will be centered on the screen
|
||||||
|
function xPositionFromTaskIndex(index) {
|
||||||
|
return index * (taskWidth + taskSpacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// instantly go to the task index
|
||||||
|
function goToTaskIndex(index) {
|
||||||
|
xPosition = xPositionFromTaskIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// go to the task index, animated
|
||||||
|
function animateGoToTaskIndex(index, duration) {
|
||||||
|
xAnim.duration = duration;
|
||||||
|
xAnim.to = xPositionFromTaskIndex(index);
|
||||||
|
xAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// called after a user finishes an interaction (ex. lets go of the screen)
|
||||||
|
function updateState() {
|
||||||
|
cancelAnimations();
|
||||||
|
|
||||||
|
// update vertical state
|
||||||
|
if (movingUp || root.yPosition >= openedYPosition) {
|
||||||
|
// open task switcher and stay
|
||||||
|
openAnim.restart();
|
||||||
|
} else {
|
||||||
|
// close task switcher and return to app
|
||||||
|
closeAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update horizontal state
|
||||||
|
if (!currentlyBeingOpened) {
|
||||||
|
let currentTaskIndexPosition = xPositionFromTaskIndex(currentTaskIndex);
|
||||||
|
let duration = PlasmaCore.Units.longDuration * 2;
|
||||||
|
if (xPosition < currentTaskIndexPosition) {
|
||||||
|
if (movingRight) {
|
||||||
|
animateGoToTaskIndex(currentTaskIndex, duration);
|
||||||
|
} else {
|
||||||
|
animateGoToTaskIndex(Math.max(0, currentTaskIndex - 1), duration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (movingRight) {
|
||||||
|
animateGoToTaskIndex(Math.min(taskSwitcher.tasksCount - 1, currentTaskIndex + 1), duration);
|
||||||
|
} else {
|
||||||
|
animateGoToTaskIndex(currentTaskIndex, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~~ property animators ~~
|
||||||
|
|
||||||
|
property var xAnim: NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "xPosition"
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
|
||||||
|
property var openAnim: NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "yPosition"
|
||||||
|
to: openedYPosition
|
||||||
|
duration: PlasmaCore.Units.longDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
|
||||||
|
onFinished: {
|
||||||
|
root.currentlyBeingOpened = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var closeAnim: NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "yPosition"
|
||||||
|
to: 0
|
||||||
|
duration: PlasmaCore.Units.longDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
|
||||||
|
onFinished: {
|
||||||
|
root.currentlyBeingOpened = false;
|
||||||
|
taskSwitcher.instantHide();
|
||||||
|
|
||||||
|
if (root.wasInActiveTask) {
|
||||||
|
taskSwitcher.setSingleActiveWindow(root.currentTaskIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var openAppAnim: NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "yPosition"
|
||||||
|
to: 0
|
||||||
|
duration: PlasmaCore.Units.longDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
|
||||||
|
onFinished: {
|
||||||
|
root.currentlyBeingOpened = false;
|
||||||
|
taskSwitcher.setSingleActiveWindow(root.currentTaskIndex);
|
||||||
|
taskSwitcher.instantHide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,9 +102,9 @@ PlasmaCore.ColorScope {
|
||||||
taskSwitcher.show(true);
|
taskSwitcher.show(true);
|
||||||
} else {
|
} else {
|
||||||
// when task switcher is open
|
// when task switcher is open
|
||||||
if (taskSwitcher.wasInActiveTask) {
|
if (taskSwitcher.taskSwitcherState.wasInActiveTask) {
|
||||||
// restore active window
|
// restore active window
|
||||||
taskSwitcher.activateWindow(root.currentTaskIndex);
|
taskSwitcher.activateWindow(taskSwitcher.taskSwitcherState.currentTaskIndex);
|
||||||
} else {
|
} else {
|
||||||
taskSwitcher.hide();
|
taskSwitcher.hide();
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +169,7 @@ PlasmaCore.ColorScope {
|
||||||
|
|
||||||
// do not enable drag gesture when task switcher is already open
|
// do not enable drag gesture when task switcher is already open
|
||||||
// also don't disable drag gesture mid-drag
|
// also don't disable drag gesture mid-drag
|
||||||
dragGestureEnabled: !taskSwitcher.visible || taskSwitcher.currentlyDragging
|
dragGestureEnabled: !taskSwitcher.visible || taskSwitcher.taskSwitcherState.currentlyBeingOpened
|
||||||
|
|
||||||
leftAction: taskSwitcherAction
|
leftAction: taskSwitcherAction
|
||||||
middleAction: homeAction
|
middleAction: homeAction
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue