mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +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,10 +53,12 @@ Flickable {
|
|||
onContentXChanged: mainFlickable.currentIndex = Math.floor(contentX / width)
|
||||
|
||||
onFooterChanged: {
|
||||
footer.parent = mainFlickable;
|
||||
footer.anchors.left = mainFlickable.left;
|
||||
footer.anchors.bottom = mainFlickable.bottom;
|
||||
footer.anchors.right = mainFlickable.right;
|
||||
if (footer) {
|
||||
footer.parent = mainFlickable;
|
||||
footer.anchors.left = mainFlickable.left;
|
||||
footer.anchors.bottom = mainFlickable.bottom;
|
||||
footer.anchors.right = mainFlickable.right;
|
||||
}
|
||||
}
|
||||
|
||||
//Autoscroll related functions
|
||||
|
|
|
|||
|
|
@ -69,20 +69,24 @@ Item {
|
|||
}
|
||||
|
||||
if (root.dragGestureEnabled) {
|
||||
if (!taskSwitcher.currentlyDragging && Math.abs(startMouseY - oldMouseY) < root.height) {
|
||||
if (!opening && Math.abs(startMouseY - oldMouseY) < root.height) {
|
||||
oldMouseY = mouse.y;
|
||||
return;
|
||||
} else if (mouseArea.pressed) {
|
||||
taskSwitcher.currentlyDragging = true;
|
||||
opening = true;
|
||||
}
|
||||
|
||||
// update offsets with drags
|
||||
root.taskSwitcher.oldYOffset = root.taskSwitcher.yOffset;
|
||||
root.taskSwitcher.yOffset = Math.max(0, root.taskSwitcher.yOffset - (mouse.y - oldMouseY));
|
||||
|
||||
opening = oldMouseY > mouse.y;
|
||||
if (root.taskSwitcher.visible) {
|
||||
// update task switcher drag
|
||||
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);
|
||||
|
||||
// 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
|
||||
activeButton = null;
|
||||
root.taskSwitcher.show(false);
|
||||
|
|
@ -98,11 +102,8 @@ Item {
|
|||
onReleased: {
|
||||
if (activeButton) {
|
||||
activeButton.clicked();
|
||||
}
|
||||
|
||||
if (root.dragGestureEnabled && root.taskSwitcher.currentlyDragging) {
|
||||
root.taskSwitcher.currentlyDragging = false;
|
||||
root.taskSwitcher.snapOffset();
|
||||
} else if (root.dragGestureEnabled && taskSwitcher.taskSwitcherState.currentlyBeingOpened) {
|
||||
taskSwitcher.taskSwitcherState.updateState();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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: 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
|
||||
|
|
@ -22,20 +22,23 @@ Item {
|
|||
required property var model
|
||||
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
|
||||
|
||||
property bool active: model.IsActive
|
||||
property bool active: model ? model.IsActive : false
|
||||
|
||||
required property real previewHeight
|
||||
required property real previewWidth
|
||||
|
||||
property bool showHeader: true
|
||||
|
||||
property real scale: 1
|
||||
|
||||
opacity: 1 - dragOffset / taskSwitcher.height
|
||||
|
||||
//BEGIN functions
|
||||
function syncDelegateGeometry() {
|
||||
let pos = pipeWireLoader.mapToItem(tasksView, 0, 0);
|
||||
let pos = pipeWireLoader.mapToItem(delegate, 0, 0);
|
||||
if (taskSwitcher.visible) {
|
||||
tasksModel.requestPublishDelegateGeometry(tasksModel.index(model.index, 0), Qt.rect(pos.x, pos.y, pipeWireLoader.width, pipeWireLoader.height), pipeWireLoader);
|
||||
}
|
||||
|
|
@ -58,7 +61,7 @@ Item {
|
|||
syncDelegateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QQC2.Control {
|
||||
id: control
|
||||
width: parent.width
|
||||
|
|
@ -113,6 +116,11 @@ Item {
|
|||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: column.height - appView.height
|
||||
spacing: PlasmaCore.Units.smallSpacing * 2
|
||||
opacity: delegate.showHeader ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: PlasmaCore.Units.shortDuration }
|
||||
}
|
||||
|
||||
PlasmaCore.IconItem {
|
||||
Layout.preferredHeight: PlasmaCore.Units.iconSizes.smallMedium
|
||||
|
|
@ -164,6 +172,9 @@ Item {
|
|||
Layout.maximumWidth: delegate.previewWidth
|
||||
Layout.maximumHeight: delegate.previewHeight
|
||||
|
||||
// prevent thumbnails from "leaking" out of the control
|
||||
clip: true
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 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: 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.Window 2.2
|
||||
|
||||
import org.kde.taskmanager 0.1 as TaskManager
|
||||
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.mobileshell 1.0 as MobileShell
|
||||
|
||||
import "../components" as Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
visible: false
|
||||
opacity: 0
|
||||
|
||||
readonly property real taskPanelHeight: MobileShell.TaskPanelControls.panelHeight
|
||||
readonly property real taskPanelWidth: MobileShell.TaskPanelControls.panelWidth
|
||||
readonly property bool isPortrait: MobileShell.TaskPanelControls.isPortrait
|
||||
// state object
|
||||
property var taskSwitcherState: TaskSwitcherState {
|
||||
taskSwitcher: root
|
||||
}
|
||||
|
||||
// dimensions of a window on the screen
|
||||
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
|
||||
// task list model
|
||||
property TaskManager.TasksModel model
|
||||
|
||||
// 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
|
||||
readonly property int tasksCount: model.count
|
||||
|
||||
property var displaysModel: MobileShell.DisplaysModel {}
|
||||
|
||||
enum MovementDirection {
|
||||
None = 0,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
onVisibleChanged: MobileShell.HomeScreenControls.taskSwitcherVisible = visible;
|
||||
|
||||
onTasksCountChanged: {
|
||||
|
||||
property int oldTasksCount: tasksCount
|
||||
onTasksCountChanged: {
|
||||
if (tasksCount == 0) {
|
||||
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 {
|
||||
id: backgroundRect
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.6 * (root.wasInActiveTask ? 1 : Math.min(1, root.yOffset / root.targetYOffsetDist)))
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.hide()
|
||||
}
|
||||
oldTasksCount = tasksCount;
|
||||
}
|
||||
|
||||
// TODO close task switcher when an app opens while it is open, otherwise the navbar becomes glitched
|
||||
// TODO click outside of delegate to close
|
||||
|
||||
//BEGIN functions
|
||||
|
||||
function show(animation) {
|
||||
root.yOffset = 0;
|
||||
root.wasInActiveTask = root.model.activeTask.row >= 0;
|
||||
// reset values
|
||||
taskSwitcherState.cancelAnimations();
|
||||
taskSwitcherState.yPosition = 0;
|
||||
taskSwitcherState.xPosition = 0;
|
||||
taskSwitcherState.wasInActiveTask = root.model.activeTask.row >= 0;
|
||||
taskSwitcherState.currentlyBeingOpened = true;
|
||||
|
||||
// skip to first active task
|
||||
if (root.wasInActiveTask) {
|
||||
tasksView.currentIndex = root.model.activeTask.row;
|
||||
tasksView.positionViewAtIndex(root.model.activeTask.row, ListView.SnapPosition);
|
||||
if (taskSwitcherState.wasInActiveTask) {
|
||||
taskSwitcherState.goToTaskIndex(root.model.activeTask.row);
|
||||
}
|
||||
|
||||
root.visible = true;
|
||||
// show task switcher, hide all running apps
|
||||
visible = true;
|
||||
opacity = 1;
|
||||
minimizeAll();
|
||||
|
||||
// animate app shrink
|
||||
// fully open the panel (if this is a button press, not gesture)
|
||||
if (animation) {
|
||||
offsetAnimator.to = root.targetYOffsetDist;
|
||||
offsetAnimator.restart();
|
||||
taskSwitcherState.open();
|
||||
}
|
||||
}
|
||||
|
||||
function instantHide() {
|
||||
opacity = 0;
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!root.visible) return;
|
||||
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();
|
||||
}
|
||||
closeAnim.restart();
|
||||
}
|
||||
|
||||
// scroll to delegate index, and activate it
|
||||
function activateWindow(id) {
|
||||
offsetAnimator.to = 0;
|
||||
offsetAnimator.restart();
|
||||
taskSwitcherState.openApp(id);
|
||||
}
|
||||
|
||||
function setSingleActiveWindow(id, delegate) {
|
||||
function setSingleActiveWindow(id) {
|
||||
if (id < 0) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -130,14 +103,14 @@ Item {
|
|||
root.model.requestActivate(idx);
|
||||
} else if (!tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized)) {
|
||||
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) {
|
||||
tasksModel.requestToggleMinimized(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.visible = false;
|
||||
instantHide();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
NumberAnimation on opacity {
|
||||
id: closeAnim
|
||||
to: 0
|
||||
duration: PlasmaCore.Units.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
onFinished: {
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
//END functions
|
||||
|
||||
// animate app grow and shrink
|
||||
NumberAnimation on yOffset {
|
||||
id: offsetAnimator
|
||||
duration: PlasmaCore.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
|
||||
// background colour
|
||||
Rectangle {
|
||||
id: backgroundRect
|
||||
anchors.fill: parent
|
||||
|
||||
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
|
||||
onFinished: {
|
||||
if (to === 0) { // close task switcher, and switch to current app
|
||||
if (!root.visible) return;
|
||||
root.visible = false;
|
||||
|
||||
if (activateApp) {
|
||||
setSingleActiveWindow(root.currentTaskIndex);
|
||||
}
|
||||
activateApp = true;
|
||||
} else if (to == root.dismissYOffsetDist) {
|
||||
root.hide();
|
||||
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,74 +154,30 @@ Item {
|
|||
|
||||
// provide shell margins
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: root.isPortrait ? 0 : root.taskPanelWidth
|
||||
anchors.bottomMargin: root.isPortrait ? root.taskPanelHeight : 0
|
||||
anchors.rightMargin: MobileShell.TaskPanelControls.isPortrait ? 0 : MobileShell.TaskPanelControls.panelWidth
|
||||
anchors.bottomMargin: MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0
|
||||
anchors.topMargin: MobileShell.TopPanelControls.panelHeight
|
||||
|
||||
// applications list
|
||||
ListView {
|
||||
id: tasksView
|
||||
opacity: root.wasInActiveTask ? 1 : Math.min(1, root.yOffset / root.targetYOffsetDist)
|
||||
anchors.centerIn: parent
|
||||
FlickContainer {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
taskSwitcherState: root.taskSwitcherState
|
||||
|
||||
readonly property real sizeFactor: 0.75
|
||||
readonly property real taskHeaderHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing * 2
|
||||
|
||||
width: root.windowWidth * sizeFactor
|
||||
height: root.windowHeight * sizeFactor + taskHeaderHeight
|
||||
|
||||
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;
|
||||
// the item is effectively anchored to the flickable bounds
|
||||
QQC2.Control {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
x: flickable.contentX
|
||||
width: flickable.width
|
||||
height: flickable.height
|
||||
|
||||
contentItem: TaskList {
|
||||
taskSwitcher: root
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Task {
|
||||
id: task
|
||||
property int curIndex: model.index
|
||||
z: root.currentTaskIndex === curIndex ? 1 : 0
|
||||
width: tasksView.width
|
||||
height: tasksView.height
|
||||
|
||||
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);
|
||||
} else {
|
||||
// when task switcher is open
|
||||
if (taskSwitcher.wasInActiveTask) {
|
||||
if (taskSwitcher.taskSwitcherState.wasInActiveTask) {
|
||||
// restore active window
|
||||
taskSwitcher.activateWindow(root.currentTaskIndex);
|
||||
taskSwitcher.activateWindow(taskSwitcher.taskSwitcherState.currentTaskIndex);
|
||||
} else {
|
||||
taskSwitcher.hide();
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ PlasmaCore.ColorScope {
|
|||
|
||||
// do not enable drag gesture when task switcher is already open
|
||||
// also don't disable drag gesture mid-drag
|
||||
dragGestureEnabled: !taskSwitcher.visible || taskSwitcher.currentlyDragging
|
||||
dragGestureEnabled: !taskSwitcher.visible || taskSwitcher.taskSwitcherState.currentlyBeingOpened
|
||||
|
||||
leftAction: taskSwitcherAction
|
||||
middleAction: homeAction
|
||||
|
|
|
|||
Loading…
Reference in a new issue