taskswitcher: Port to kwin effect

This commit is contained in:
Devin Lin 2023-03-05 22:38:43 -08:00
parent f87c7c5526
commit eb03fe8c94
27 changed files with 912 additions and 675 deletions

View file

@ -14,7 +14,7 @@ set(QT_MIN_VERSION "6.4.0")
set(KF6_MIN_VERSION "5.240.0")
set(KDE_COMPILERSETTINGS_LEVEL "5.82")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE)
@ -53,6 +53,7 @@ find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
I18n
GlobalAccel
KIO
Config
DBusAddons
@ -74,6 +75,12 @@ pkg_check_modules(GOBJECT gobject-2.0 REQUIRED IMPORTED_TARGET)
pkg_check_modules(GIO gio-2.0 REQUIRED IMPORTED_TARGET)
find_package(KF6KirigamiAddons 0.6 REQUIRED)
find_package(epoxy REQUIRED)
find_package(XCB REQUIRED COMPONENTS XCB)
find_package(KWinEffects 5.27.0 REQUIRED COMPONENTS
kwineffects
)
find_package(LibKWorkspace CONFIG REQUIRED)
find_package(KWinDBusInterface)

View file

@ -95,9 +95,6 @@ void MobileShellPlugin::registerTypes(const char *uri)
// /statusbar
qmlRegisterType(resolvePath("statusbar/StatusBar.qml"), uri, 1, 0, "StatusBar");
// /taskswitcher
qmlRegisterType(resolvePath("taskswitcher/TaskSwitcher.qml"), uri, 1, 0, "TaskSwitcher");
// /widgets
qmlRegisterType(resolvePath("widgets/krunner/KRunnerWidget.qml"), uri, 1, 0, "KRunnerWidget");
qmlRegisterType(resolvePath("widgets/mediacontrols/MediaControlsWidget.qml"), uri, 1, 0, "MediaControlsWidget");

View file

@ -40,8 +40,6 @@ MouseArea { // use mousearea to ensure clicks don't go behind
}
background.state = "open";
MobileShellState.HomeScreenControls.taskSwitcher.minimizeAll();
}
function close() {

View file

@ -44,7 +44,7 @@ Item {
* Whether a component is being shown on top of the homescreen within the same
* window.
*/
readonly property bool overlayShown: taskSwitcher.visible || startupFeedback.visible
readonly property bool overlayShown: startupFeedback.visible
/**
* Margins for the homescreen, taking panels into account.
@ -82,9 +82,10 @@ Item {
}
MobileShellState.HomeScreenControls.resetHomeScreenPosition();
taskSwitcher.visible = false; // will trigger homescreen open
taskSwitcher.minimizeAll();
MobileShell.WindowUtil.unsetAllMinimizedGeometries(root);
MobileShell.WindowUtil.minimizeAll();
root.homeTriggered();
}
@ -109,7 +110,6 @@ Item {
Plasmoid.onScreenChanged: {
if (plasmoid.screen == 0) {
MobileShellState.HomeScreenControls.taskSwitcher = taskSwitcher;
MobileShellState.HomeScreenControls.homeScreenWindow = root.Window.window;
}
}
@ -127,7 +127,6 @@ Item {
// set API variables
if (plasmoid.screen == 0) {
MobileShellState.HomeScreenControls.taskSwitcher = taskSwitcher;
MobileShellState.HomeScreenControls.homeScreenWindow = root.Window.window;
}
}
@ -203,12 +202,10 @@ Item {
function evaluateAnimChange() {
// only animate if homescreen is visible
if (!taskSwitcher.visible) {
if (!visibleMaximizedWindowsModel.isWindowMaximized || MobileShell.WindowUtil.activeWindowIsShell) {
itemContainer.zoomIn();
} else {
itemContainer.zoomOut();
}
if (!visibleMaximizedWindowsModel.isWindowMaximized || MobileShell.WindowUtil.activeWindowIsShell) {
itemContainer.zoomIn();
} else {
itemContainer.zoomOut();
}
}
@ -234,45 +231,6 @@ Item {
}
}
// task switcher component
MobileShell.TaskSwitcher {
id: taskSwitcher
z: 999999
topMargin: root.topMargin
bottomMargin: root.bottomMargin
leftMargin: root.leftMargin
rightMargin: root.rightMargin
tasksModel: TaskManager.TasksModel {
groupMode: TaskManager.TasksModel.GroupDisabled
screenGeometry: plasmoid.screenGeometry
sortMode: TaskManager.TasksModel.SortLastActivated
virtualDesktop: virtualDesktopInfo.currentDesktop
activity: activityInfo.currentActivity
}
anchors.fill: parent
// hide homescreen elements to make use of wallpaper
onVisibleChanged: {
if (visible) {
startupFeedback.visible = false;
// hide immediately when going from homescreen
if (!taskSwitcher.wasInActiveTask) {
itemContainer.opacity = 0;
}
itemContainer.zoomOut();
} else {
itemContainer.zoomIn();
}
}
}
// start app animation component
MobileShell.StartupFeedback {
id: startupFeedback

View file

@ -1,17 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.0
import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
PlasmaCore.IconItem {
implicitWidth: PlasmaCore.Units.iconSizes.enormous
implicitHeight: PlasmaCore.Units.iconSizes.enormous
usesPlasmaTheme: false
source: model.decoration
}

View file

@ -1,283 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2021-2022 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.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
import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState
import "../components" as Components
/**
* Component that provides a task switcher.
*/
Item {
id: root
visible: false
opacity: 0
/**
* Margins for the content (taking shell panels into account).
*/
required property real topMargin
required property real bottomMargin
required property real leftMargin
required property real rightMargin
// state object
property var taskSwitcherState: TaskSwitcherState {
taskSwitcher: root
}
/**
* The task manager model to use for the tasks switcher.
*/
property TaskManager.TasksModel tasksModel
/**
* The number of tasks in the given task manager model.
*/
readonly property int tasksCount: tasksModel.count
/**
* The screen model to be used for moving windows between screens.
*/
property var displaysModel: MobileShell.DisplaysModel {}
/**
* Whether the window is active.
*/
property bool windowActive: Window.active
onWindowActiveChanged: {
// if a window has popped up in front, close the task switcher
if (visible && !windowActive) {
hide();
}
}
// update API property
onVisibleChanged: MobileShellState.HomeScreenControls.taskSwitcherVisible = visible;
// keep track of task list events
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);
}
oldTasksCount = tasksCount;
}
Timer {
id: reorderTimer
interval: 5000
onTriggered: tasksModel.taskReorderingEnabled = true
}
//BEGIN functions
function show(animation) {
// reset values
taskSwitcherState.cancelAnimations();
taskSwitcherState.yPosition = 0;
taskSwitcherState.xPosition = 0;
taskSwitcherState.wasInActiveTask = tasksModel.activeTask.row >= 0;
taskSwitcherState.currentlyBeingOpened = true;
reorderTimer.stop();
tasksModel.taskReorderingEnabled = false;
// skip to first active task
if (taskSwitcherState.wasInActiveTask) {
taskSwitcherState.goToTaskIndex(tasksModel.activeTask.row);
} else {
taskSwitcherState.goToTaskIndex(0);
}
// show task switcher, hide all running apps
visible = true;
opacity = 1;
minimizeAll();
// fully open the panel (if this is a button press, not gesture)
if (animation) {
taskSwitcherState.open();
}
}
function instantHide() {
opacity = 0;
visible = false;
closeAllButton.closeRequested = false;
}
function hide() {
closeAnim.restart();
}
// scroll to delegate index, and activate it
function activateWindow(id) {
taskSwitcherState.openApp(id);
}
function setSingleActiveWindow(id) {
if (id < 0) {
return;
}
var newActiveIdx = tasksModel.index(id, 0)
var newActiveGeo = tasksModel.data(newActiveIdx, TaskManager.AbstractTasksModel.ScreenGeometry)
for (var i = 0 ; i < tasksModel.count; i++) {
var idx = tasksModel.index(i, 0)
if (i == id) {
tasksModel.requestActivate(idx);
// ensure the window is in maximized state
if (!tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMaximized)) {
tasksModel.requestToggleMaximized(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
if (geo === newActiveGeo) {
tasksModel.requestToggleMinimized(idx);
}
}
}
instantHide();
if (taskSwitcherState.wasInActiveTask) {
reorderTimer.restart();
} else {
tasksModel.taskReorderingEnabled = true;
}
}
function minimizeAll() {
MobileShell.WindowUtil.unsetAllMinimizedGeometries(root);
MobileShell.WindowUtil.minimizeAll();
}
//END functions
NumberAnimation on opacity {
id: closeAnim
to: 0
duration: PlasmaCore.Units.shortDuration
easing.type: Easing.InOutQuad
onFinished: {
root.visible = false;
tasksModel.taskReorderingEnabled = true;
closeAllButton.closeRequested = false;
}
}
// background colour
Rectangle {
id: backgroundRect
anchors.fill: parent
color: {
// animate background colour only if we are *not* opening from the homescreen
if (taskSwitcherState.wasInActiveTask || !taskSwitcherState.currentlyBeingOpened) {
return Qt.rgba(0, 0, 0, 0.6);
} else {
return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition));
}
}
}
Item {
id: container
// provide shell margins
anchors.fill: parent
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: root.bottomMargin
anchors.topMargin: root.topMargin
FlickContainer {
id: flickable
anchors.fill: parent
taskSwitcherState: root.taskSwitcherState
// the item is effectively anchored to the flickable bounds
TaskList {
id: taskList
shellTopMargin: root.topMargin
shellBottomMargin: root.bottomMargin
taskSwitcher: root
opacity: {
// animate opacity only if we are *not* opening from the homescreen
if (taskSwitcherState.wasInActiveTask || !taskSwitcherState.currentlyBeingOpened) {
return 1;
} else {
Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition);
}
}
x: flickable.contentX
width: flickable.width
height: flickable.height
PlasmaComponents.ToolButton {
id: closeAllButton
property bool closeRequested: false
anchors {
bottom: parent.bottom
bottomMargin: taskList.taskY / 2
horizontalCenter: parent.horizontalCenter
}
PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
PlasmaCore.ColorScope.inherit: false
opacity: taskSwitcherState.currentlyBeingOpened || taskSwitcherState.currentlyBeingClosed || !root.visible ? 0.0 : 1.0
Behavior on opacity {
NumberAnimation {
duration: PlasmaCore.Units.shortDuration
}
}
icon.name: "edit-clear-history"
font.bold: true
text: closeRequested ? "Confirm Close All" : "Close All"
onClicked: {
if (closeRequested) {
taskList.closeAll();
} else {
closeRequested = true;
}
}
}
}
}
}
}

View file

@ -1,33 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Marco Martin <notmart@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.0
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import org.kde.pipewire 0.1 as PipeWire
import org.kde.taskmanager 0.1 as TaskManager
PipeWire.PipeWireSourceItem {
id: root
visible: nodeId > 0
nodeId: waylandItem.nodeId
readonly property alias uuid: waylandItem.uuid
function refresh() {
if (model.WinIdList) {
waylandItem.uuid = model.WinIdList[0];
}
}
TaskManager.ScreencastingRequest {
id: waylandItem
uuid: ""
}
}

View file

@ -63,14 +63,6 @@
<file>qml/statusbar/StatusBar.qml</file>
<file>qml/statusbar/TaskWidget.qml</file>
<file>qml/taskswitcher/FlickContainer.qml</file>
<file>qml/taskswitcher/Task.qml</file>
<file>qml/taskswitcher/TaskIcon.qml</file>
<file>qml/taskswitcher/TaskList.qml</file>
<file>qml/taskswitcher/TaskSwitcher.qml</file>
<file>qml/taskswitcher/TaskSwitcherState.qml</file>
<file>qml/taskswitcher/Thumbnail.qml</file>
<file>qml/widgets/krunner/KRunnerWidget.qml</file>
<file>qml/widgets/mediacontrols/BlurredBackground.qml</file>

View file

@ -12,11 +12,6 @@ pragma Singleton
QtObject {
id: delegate
/**
* Whether the task switcher is open.
*/
readonly property bool taskSwitcherVisible: HomeScreenControls.taskSwitcherVisible
/**
* Whether the homescreen is currently visible.
*/

View file

@ -14,7 +14,7 @@ import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PC3
import org.kde.kquickcontrolsaddons 2.0
import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager
import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
@ -52,11 +52,7 @@ ContainmentLayoutManager.ItemContainer {
return;
}
if (!MobileShellState.Shell.taskSwitcherVisible) {
desktopModel.setMinimizedDelegate(index, delegate);
} else {
desktopModel.unsetMinimizedDelegate(index, delegate);
}
desktopModel.setMinimizedDelegate(index, delegate);
}
function launchApp() {

View file

@ -37,10 +37,8 @@ Item {
}
function openConfigure() {
if (!MobileShellState.Shell.taskSwitcherVisible) {
plasmoid.action("configure").trigger();
plasmoid.editMode = false;
}
plasmoid.action("configure").trigger();
plasmoid.editMode = false;
}
Connections {

View file

@ -48,7 +48,12 @@ MobileShell.HomeScreen {
if (!MobileShell.WindowUtil.showDesktop && !MobileShellState.Shell.homeScreenVisible
|| search.isOpen
) {
// Always close the search widget
// Always close action drawer
if (MobileShellState.Shell.actionDrawerVisible) {
MobileShellState.Shell.closeActionDrawer();
}
// Always close the search widget as well
if (search.isOpen) {
search.close();
}
@ -90,14 +95,6 @@ MobileShell.HomeScreen {
bottomMargin: root.bottomMargin
leftMargin: root.leftMargin
rightMargin: root.rightMargin
// close search component when task switcher is shown or hidden
Connections {
target: MobileShellState.HomeScreenControls.taskSwitcher
function onVisibleChanged() {
search.close();
}
}
}
}
}

View file

@ -1,8 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.4
import QtQuick.Layouts 1.1
@ -14,6 +11,7 @@ import org.kde.plasma.workspace.keyboardlayout 1.0 as Keyboards
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState
import org.kde.taskmanager 0.1 as TaskManager
MobileShell.NavigationPanel {
id: root
@ -23,20 +21,36 @@ MobileShell.NavigationPanel {
// - opaque if an app is shown or vkbd is shown
// - translucent if the task switcher is open
// - transparent if on the homescreen
backgroundColor: {
if (root.taskSwitcher.visible) {
return Qt.rgba(0, 0, 0, 0.1);
} else {
return (Keyboards.KWinVirtualKeyboard.visible || opaqueBar) ? PlasmaCore.ColorScope.backgroundColor : "transparent";
}
}
foregroundColorGroup: (!root.taskSwitcher.visible && opaqueBar) ? PlasmaCore.Theme.NormalColorGroup : PlasmaCore.Theme.ComplementaryColorGroup
backgroundColor: (Keyboards.KWinVirtualKeyboard.visible || opaqueBar) ? PlasmaCore.ColorScope.backgroundColor : "transparent";
foregroundColorGroup: opaqueBar ? PlasmaCore.Theme.NormalColorGroup : PlasmaCore.Theme.ComplementaryColorGroup
shadow: !opaqueBar
// do not enable drag gesture when task switcher is already open
// also don't disable drag gesture mid-drag
dragGestureEnabled: !root.taskSwitcher.visible || root.taskSwitcher.taskSwitcherState.currentlyBeingOpened
dragGestureEnabled: false // !root.taskSwitcher.visible || root.taskSwitcher.taskSwitcherState.currentlyBeingOpened
TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo
}
TaskManager.ActivityInfo {
id: activityInfo
}
TaskManager.TasksModel {
id: tasksModel
filterByVirtualDesktop: true
filterByActivity: true
filterNotMaximized: true
filterByScreen: true
filterHidden: true
virtualDesktop: virtualDesktopInfo.currentDesktop
activity: activityInfo.currentActivity
groupMode: TaskManager.TasksModel.GroupDisabled
}
// ~~~~
// navigation panel actions
@ -44,24 +58,12 @@ MobileShell.NavigationPanel {
leftAction: MobileShell.NavigationPanelAction {
id: taskSwitcherAction
enabled: (root.taskSwitcher.tasksCount > 0) || root.taskSwitcher.visible
enabled: true
iconSource: "mobile-task-switcher"
iconSizeFactor: 0.75
onTriggered: {
MobileShell.WindowUtil.showDesktop = false;
if (!root.taskSwitcher.visible) {
root.taskSwitcher.show(true);
} else {
// when task switcher is open
if (root.taskSwitcher.taskSwitcherState.wasInActiveTask) {
// restore active window
root.taskSwitcher.activateWindow(taskSwitcher.taskSwitcherState.currentTaskIndex);
} else {
root.taskSwitcher.hide();
}
}
plasmoid.nativeInterface.triggerTaskSwitcher();
}
}
@ -83,7 +85,7 @@ MobileShell.NavigationPanel {
rightAction: MobileShell.NavigationPanelAction {
id: closeAppAction
enabled: Keyboards.KWinVirtualKeyboard.visible || root.taskSwitcher.visible || MobileShell.WindowUtil.hasCloseableActiveWindow
enabled: Keyboards.KWinVirtualKeyboard.visible || MobileShell.WindowUtil.hasCloseableActiveWindow
iconSource: Keyboards.KWinVirtualKeyboard.visible ? "go-down-symbolic" : "mobile-close-app"
// mobile-close-app (from plasma-frameworks) seems to have less margins than icons from breeze-icons
iconSizeFactor: Keyboards.KWinVirtualKeyboard.visible ? 1 : 0.75
@ -92,15 +94,10 @@ MobileShell.NavigationPanel {
if (Keyboards.KWinVirtualKeyboard.active) {
// close keyboard if it is open
Keyboards.KWinVirtualKeyboard.active = false;
} else if (taskSwitcher.visible) {
// if task switcher is open, close the current window shown
let indexToClose = root.taskSwitcher.tasksModel.index(root.taskSwitcher.currentTaskIndex, 0);
root.taskSwitcher.tasksModel.requestClose(indexToClose);
} else if (MobileShell.WindowUtil.hasCloseableActiveWindow) {
// if task switcher is closed, but there is an active window
if (root.taskSwitcher.tasksModel.activeTask !== 0) {
root.taskSwitcher.tasksModel.requestClose(root.taskSwitcher.tasksModel.activeTask);
if (tasksModel.activeTask !== 0) {
tasksModel.requestClose(tasksModel.activeTask);
}
MobileShellState.Shell.closeAppLaunchAnimation();
}

View file

@ -142,17 +142,14 @@ PlasmaCore.ColorScope {
Component {
id: navigationPanel
NavigationPanelComponent {
taskSwitcher: MobileShellState.HomeScreenControls.taskSwitcher
opaqueBar: root.opaqueBar
}
}
// bottom navigation gesture area component
Component {
id: navigationGesture
MobileShell.NavigationGestureArea {
taskSwitcher: MobileShellState.HomeScreenControls.taskSwitcher
}
id: navigationGesture
MobileShell.NavigationGestureArea {}
}
// load appropriate system navigation component

View file

@ -120,6 +120,15 @@ void TaskPanel::updatePanelVisibility()
}
}
void TaskPanel::triggerTaskSwitcher() const
{
QDBusMessage message = QDBusMessage::createMethodCall("org.kde.kglobalaccel", "/component/kwin", "org.kde.kglobalaccel.Component", "invokeShortcut");
message.setArguments({QStringLiteral("Mobile Task Switcher")});
// this does not block, so it won't necessarily be called before the method returns
QDBusConnection::sessionBus().send(message);
}
K_PLUGIN_CLASS_WITH_JSON(TaskPanel, "package/metadata.json")
#include "taskpanel.moc"

View file

@ -41,6 +41,8 @@ public:
QAbstractItemModel *outputs() const;
Q_INVOKABLE void triggerTaskSwitcher() const;
Q_SIGNALS:
void panelChanged();
void locationChanged();

View file

@ -2,3 +2,4 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
add_subdirectory(scripts)
add_subdirectory(mobiletaskswitcher)

View file

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
kcoreaddons_add_plugin(kwin4_effect_mobiletaskswitcher INSTALL_NAMESPACE "kwin/effects/plugins")
target_sources(kwin4_effect_mobiletaskswitcher PRIVATE
main.cpp
mobiletaskswitchereffect.cpp
)
install(FILES metadata.json DESTINATION ${KDE_INSTALL_DATADIR}/kwin/builtin-effects/kwin4_effect_mobiletaskswitcher/)
target_link_libraries(kwin4_effect_mobiletaskswitcher
KF6::ConfigGui
KF6::GlobalAccel
KF6::I18n
KF6::CoreAddons
KF6::WindowSystem
Qt::Quick
Qt::Core
KWinEffects::kwineffects
)
# install(TARGETS kwin4_effect_taskswitcher DESTINATION ${PLUGIN_INSTALL_DIR}/kwin/effects/plugins)
install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/mobiletaskswitcher)

View file

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "mobiletaskswitchereffect.h"
namespace KWin
{
KWIN_EFFECT_FACTORY_SUPPORTED(MobileTaskSwitcherEffect, "metadata.json", return MobileTaskSwitcherEffect::supported();)
} // namespace KWin
#include "main.moc"

View file

@ -0,0 +1,12 @@
{
"KPlugin": {
"Category": "Window Management",
"Description": "Allows you to switch between running tasks with a mobile interface.",
"EnabledByDefault": true,
"Id": "mobiletaskswitcher",
"License": "GPL",
"Name": "Mobile Task Switcher"
},
"X-KWin-Border-Activate": true
}

View file

@ -0,0 +1,252 @@
// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "mobiletaskswitchereffect.h"
#include <QKeyEvent>
#include <QMetaObject>
#include <QQuickItem>
namespace KWin
{
MobileTaskSwitcherEffect::MobileTaskSwitcherEffect()
: m_shutdownTimer(new QTimer(this))
{
m_shutdownTimer->setSingleShot(true);
connect(m_shutdownTimer, &QTimer::timeout, this, &MobileTaskSwitcherEffect::realDeactivate);
const QKeySequence defaultToggleShortcut = Qt::META | Qt::Key_C;
m_toggleAction = new QAction(this);
m_toggleAction->setObjectName(QStringLiteral("Mobile Task Switcher"));
m_toggleAction->setText(i18n("Toggle Mobile Task Switcher"));
connect(m_toggleAction, &QAction::triggered, this, &MobileTaskSwitcherEffect::toggle);
KGlobalAccel::self()->setDefaultShortcut(m_toggleAction, {defaultToggleShortcut});
KGlobalAccel::self()->setShortcut(m_toggleAction, {defaultToggleShortcut});
m_realtimeToggleAction = new QAction(this);
connect(m_realtimeToggleAction, &QAction::triggered, this, [this]() {
if (m_status == Status::Deactivating) {
if (m_partialActivationFactor < 0.5) {
deactivate(false);
} else {
cancelPartialDeactivate();
}
} else if (m_status == Status::Activating) {
if (m_partialActivationFactor > 0.5) {
activate();
} else {
cancelPartialActivate();
}
}
});
auto progressCallback = [this](qreal progress) {
if (!effects->hasActiveFullScreenEffect() || effects->activeFullScreenEffect() == this) {
switch (m_status) {
case Status::Inactive:
case Status::Activating:
partialActivate(progress);
break;
case Status::Active:
case Status::Deactivating:
partialDeactivate(progress);
break;
}
}
};
effects->registerTouchpadPinchShortcut(PinchDirection::Contracting, 4, m_realtimeToggleAction, progressCallback);
effects->registerTouchscreenSwipeShortcut(SwipeDirection::Up, 3, m_realtimeToggleAction, progressCallback);
connect(effects, &EffectsHandler::screenAboutToLock, this, &MobileTaskSwitcherEffect::realDeactivate);
setSource(QUrl::fromLocalFile(
QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/mobiletaskswitcher/qml/TaskSwitcher.qml"))));
}
MobileTaskSwitcherEffect::~MobileTaskSwitcherEffect()
{
}
void MobileTaskSwitcherEffect::reconfigure(ReconfigureFlags)
{
setAnimationDuration(animationTime(300));
for (const ElectricBorder &border : std::as_const(m_borderActivate)) {
effects->unreserveElectricBorder(border, this);
}
for (const ElectricBorder &border : std::as_const(m_touchBorderActivate)) {
effects->unregisterTouchBorder(border, m_toggleAction);
}
m_borderActivate.clear();
m_touchBorderActivate.clear();
const QList<int> activateBorders = {ElectricBorder::ElectricBottom};
for (const int &border : activateBorders) {
m_borderActivate.append(ElectricBorder(border));
effects->reserveElectricBorder(ElectricBorder(border), this);
}
const QList<int> touchActivateBorders = {ElectricBorder::ElectricBottom};
for (const int &border : touchActivateBorders) {
m_touchBorderActivate.append(ElectricBorder(border));
effects->registerRealtimeTouchBorder(ElectricBorder(border),
m_realtimeToggleAction,
[this](ElectricBorder border, const QPointF &deltaProgress, const EffectScreen *screen) {
if (m_status == Status::Active) {
return;
}
const int maxDelta = 500; // Arbitrary logical pixels value seems to behave better than scaledScreenSize
if (border == ElectricTop || border == ElectricBottom) {
partialActivate(std::min(1.0, std::abs(deltaProgress.y()) / maxDelta));
} else {
partialActivate(std::min(1.0, std::abs(deltaProgress.x()) / maxDelta));
}
});
}
}
int MobileTaskSwitcherEffect::requestedEffectChainPosition() const
{
return 70;
}
bool MobileTaskSwitcherEffect::borderActivated(ElectricBorder border)
{
return false;
}
void MobileTaskSwitcherEffect::grabbedKeyboardEvent(QKeyEvent *keyEvent)
{
if (m_toggleShortcut.contains(keyEvent->key() | keyEvent->modifiers())) {
if (keyEvent->type() == QEvent::KeyPress) {
toggle();
}
return;
}
QuickSceneEffect::grabbedKeyboardEvent(keyEvent);
}
void MobileTaskSwitcherEffect::toggle()
{
if (!isRunning()) {
activate();
} else {
deactivate(false);
}
}
void MobileTaskSwitcherEffect::activate()
{
if (effects->isScreenLocked()) {
return;
}
m_status = Status::Active;
setRunning(true);
}
void MobileTaskSwitcherEffect::deactivate(bool deactivateInstantly)
{
const auto screens = effects->screens();
for (const auto screen : screens) {
if (QuickSceneView *view = viewForScreen(screen)) {
QMetaObject::invokeMethod(view->rootItem(), "hideAnimation");
}
}
m_shutdownTimer->start(animationTime(deactivateInstantly ? 0 : 200));
setGestureInProgress(false);
setPartialActivationFactor(0.0);
}
void MobileTaskSwitcherEffect::partialActivate(qreal factor)
{
if (effects->isScreenLocked()) {
return;
}
m_status = Status::Activating;
setPartialActivationFactor(factor);
setGestureInProgress(true);
setRunning(true);
}
void MobileTaskSwitcherEffect::partialDeactivate(qreal factor)
{
m_status = Status::Deactivating;
setPartialActivationFactor(1.0 - factor);
setGestureInProgress(true);
}
void MobileTaskSwitcherEffect::cancelPartialDeactivate()
{
activate();
}
void MobileTaskSwitcherEffect::cancelPartialActivate()
{
deactivate(false);
}
void MobileTaskSwitcherEffect::realDeactivate()
{
setRunning(false);
m_status = Status::Inactive;
}
void MobileTaskSwitcherEffect::quickDeactivate()
{
m_shutdownTimer->start(0);
}
int MobileTaskSwitcherEffect::animationDuration() const
{
return m_animationDuration;
}
void MobileTaskSwitcherEffect::setAnimationDuration(int duration)
{
if (m_animationDuration != duration) {
m_animationDuration = duration;
Q_EMIT animationDurationChanged();
}
}
bool MobileTaskSwitcherEffect::gestureInProgress() const
{
return m_gestureInProgress;
}
void MobileTaskSwitcherEffect::setGestureInProgress(bool gesture)
{
if (m_gestureInProgress != gesture) {
m_gestureInProgress = gesture;
Q_EMIT gestureInProgressChanged();
}
}
qreal MobileTaskSwitcherEffect::partialActivationFactor() const
{
return m_partialActivationFactor;
}
void MobileTaskSwitcherEffect::setPartialActivationFactor(qreal factor)
{
if (m_partialActivationFactor != factor) {
qDebug() << factor;
m_partialActivationFactor = factor;
Q_EMIT partialActivationFactorChanged();
}
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <kwinquickeffect.h>
#include <span>
#include <QAction>
#include <QKeySequence>
#include <QTimer>
#include <KGlobalAccel>
#include <KLocalizedString>
namespace KWin
{
class MobileTaskSwitcherEffect : public QuickSceneEffect
{
Q_OBJECT
Q_PROPERTY(qreal partialActivationFactor READ partialActivationFactor NOTIFY partialActivationFactorChanged)
Q_PROPERTY(bool gestureInProgress READ gestureInProgress NOTIFY gestureInProgressChanged)
public:
enum class Status { Inactive, Activating, Deactivating, Active };
MobileTaskSwitcherEffect();
~MobileTaskSwitcherEffect() override;
int animationDuration() const;
void setAnimationDuration(int duration);
bool gestureInProgress() const;
void setGestureInProgress(bool gesture);
qreal partialActivationFactor() const;
void setPartialActivationFactor(qreal factor);
int requestedEffectChainPosition() const override;
bool borderActivated(ElectricBorder border) override;
void reconfigure(ReconfigureFlags flags) override;
void grabbedKeyboardEvent(QKeyEvent *keyEvent) override;
public Q_SLOTS:
void activate();
void realDeactivate();
void partialActivate(qreal factor);
void cancelPartialActivate();
void partialDeactivate(qreal factor);
void cancelPartialDeactivate();
void deactivate(bool deactivateInstantly);
void quickDeactivate();
void toggle();
Q_SIGNALS:
void animationDurationChanged();
void gestureInProgressChanged();
void partialActivationFactorChanged();
private:
QAction *m_realtimeToggleAction = nullptr;
QAction *m_toggleAction = nullptr;
QList<QKeySequence> m_toggleShortcut;
Status m_status = Status::Inactive;
;
QTimer *m_shutdownTimer;
QList<ElectricBorder> m_borderActivate;
QList<ElectricBorder> m_touchBorderActivate;
int m_animationDuration = 400;
qreal m_partialActivationFactor = 0;
bool m_gestureInProgress = false;
};
} // namespace KWin

View file

@ -1,8 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Layouts 1.15
@ -10,34 +7,32 @@ 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 * tasksCount
contentHeight: height
contentX: startContentX
readonly property real startContentX: 0
// update position from horizontal flickable movement
property real oldContentX
onContentXChanged: {
taskSwitcherState.xPosition += contentX - oldContentX;
oldContentX = contentX;
}
onMovementStarted: taskSwitcherState.cancelAnimations();
onMovementEnded: {
resetPosition();
taskSwitcherState.updateState();
}
onFlickStarted: {
root.cancelFlick();
}
@ -45,7 +40,7 @@ Flickable {
resetPosition();
taskSwitcherState.updateState();
}
onDraggingChanged: {
if (!dragging) {
resetPosition();
@ -54,7 +49,7 @@ Flickable {
taskSwitcherState.cancelAnimations();
}
}
function resetPosition() {
oldContentX = startContentX;
contentX = startContentX;

View file

@ -1,97 +1,85 @@
/*
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 2.2 as QQC2
import Qt5Compat.GraphicalEffects
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.kwin 3.0 as KWinComponents
Item {
id: delegate
required property var taskSwitcher
required property QtObject window
required property int index
required property var model
required property var displaysModel
required property real previewHeight
required property real previewWidth
readonly property point taskScreenPoint: (model && model.ScreenGeometry) ? Qt.point(model.ScreenGeometry.x, model.ScreenGeometry.y) : Qt.point(0, 0)
readonly property real dragOffset: -control.y
property bool showHeader: true
property real darken: 0
opacity: 1 - dragOffset / taskSwitcher.height
//BEGIN functions
function syncDelegateGeometry() {
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);
}
}
function closeApp() {
tasksModel.requestClose(tasksModel.index(model.index, 0));
delegate.window.closeWindow();
}
function activateApp() {
taskSwitcherState.wasInActiveTask = false;
taskSwitcher.activateWindow(model.index);
taskSwitcher.activateWindow(model.index, delegate.window);
window.setMaximize(true, true);
}
function minimizeApp() {
delegate.window.minimized = true;
}
//END functions
Component.onCompleted: syncDelegateGeometry();
Connections {
target: taskSwitcher
function onVisibleChanged() {
syncDelegateGeometry();
}
}
MouseArea {
id: control
width: parent.width
height: parent.height
enabled: !taskSwitcher.taskSwitcherState.currentlyBeingOpened
// set cursor shape here, since taphandler seems to not be able to do it
cursorShape: Qt.PointingHandCursor
property bool movingUp: false
property real oldY: y
onYChanged: {
movingUp = y < oldY;
oldY = y;
}
// drag up gesture
DragHandler {
id: dragHandler
target: parent
enabled: !taskSwitcher.taskSwitcherState.currentlyBeingOpened
yAxis.enabled: true
xAxis.enabled: false
yAxis.maximum: 0
// y > 0 - dragging down (opening the app)
// y < 0 - dragging up (dismissing the app)
onActiveChanged: {
yAnimator.stop();
if (control.movingUp && parent.y < -PlasmaCore.Units.gridUnit * 2) {
yAnimator.to = -root.height;
} else {
@ -100,7 +88,7 @@ Item {
yAnimator.start();
}
}
// if the app doesn't close within a certain time, drag it back
Timer {
id: uncloseTimer
@ -110,7 +98,7 @@ Item {
yAnimator.restart();
}
}
NumberAnimation on y {
id: yAnimator
running: !dragHandler.active
@ -130,7 +118,7 @@ Item {
id: column
anchors.fill: parent
spacing: 0
// header
RowLayout {
id: appHeader
@ -139,43 +127,27 @@ Item {
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
Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium
Layout.alignment: Qt.AlignVCenter
usesPlasmaTheme: false
source: model.decoration
source: delegate.window.icon
}
PlasmaComponents.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideRight
text: model.AppName
text: delegate.window.caption
color: "white"
}
Repeater {
id: rep
model: displaysModel
delegate: PlasmaComponents.ToolButton {
Layout.alignment: Qt.AlignVCenter
text: model.modelName
visible: model.position !== delegate.taskScreenPoint
display: rep.count < 3 ? QQC2.Button.IconOnly : QQC2.Button.TextBesideIcon
icon.name: "tv" //TODO provide a more adequate icon
onClicked: {
displaysModel.sendWindowToOutput(delegate.model.WinIdList[0], model.output)
}
}
}
PlasmaComponents.ToolButton {
Layout.alignment: Qt.AlignVCenter
z: 99
@ -185,7 +157,7 @@ Item {
onClicked: delegate.closeApp()
}
}
// app preview
Rectangle {
id: appView
@ -193,56 +165,43 @@ Item {
Layout.preferredHeight: delegate.previewHeight
Layout.maximumWidth: delegate.previewWidth
Layout.maximumHeight: delegate.previewHeight
color: PlasmaCore.Theme.backgroundColor
color: "transparent"
clip: true
// scale animation on press
property real zoomScale: (MobileShell.MobileShellSettings.animationsEnabled && tapHandler.pressed) ? 0.9 : 1
property real zoomScale: tapHandler.pressed ? 0.9 : 1
Behavior on zoomScale {
NumberAnimation {
duration: 200
easing.type: Easing.OutExpo
}
}
transform: Scale {
origin.x: appView.width / 2;
origin.y: appView.height / 2;
transform: Scale {
origin.x: appView.width / 2;
origin.y: appView.height / 2;
xScale: appView.zoomScale
yScale: appView.zoomScale
}
Item {
id: item
anchors.fill: parent
// app icon (behind window preview in-case it doesn't load)
TaskIcon {
// decrease the opacity faster
opacity: pipeWireLoader.item && pipeWireLoader.item.uuid ? 0 : delegate.opacity
anchors.centerIn: parent
KWinComponents.WindowThumbnail {
id: thumbSource
wId: delegate.window.internalId
anchors.fill: parent
layer {
enabled: true
effect: ColorOverlay {
color: Qt.rgba(0, 0, 0, delegate.darken)
}
}
}
// attempt to load window preview
Loader {
id: pipeWireLoader
active: (taskSwitcher.visible || taskSwitcher.tasksModel.taskReorderingEnabled) && MobileShell.MobileShellSettings.taskSwitcherPreviewsEnabled
anchors.fill: parent
source: Qt.resolvedUrl("./Thumbnail.qml")
asynchronous: true
onLoaded: this.item.refresh()
}
// darken effect
Rectangle {
anchors.fill: parent
color: "black"
opacity: delegate.darken
}
TapHandler {
id: tapHandler
enabled: !taskSwitcher.taskSwitcherState.currentlyBeingOpened
@ -254,3 +213,4 @@ Item {
}
}

View file

@ -1,8 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
// 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
@ -10,50 +7,79 @@ 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.mobileshell.state 1.0 as MobileShellState
import org.kde.kwin 3.0 as KWinComponents
Item {
id: root
readonly property int count: repeater.count
required property real shellTopMargin
required property real shellBottomMargin
required property var taskSwitcher
readonly property var taskSwitcherState: taskSwitcher.taskSwitcherState
// account for system header and footer offset (center the preview image)
readonly property real taskY: {
let headerHeight = shellTopMargin;
let footerHeight = shellBottomMargin;
let diff = headerHeight - footerHeight;
let baseY = (taskSwitcher.height / 2) - (taskSwitcherState.taskHeight / 2) - (taskSwitcherState.taskHeaderHeight / 2)
return baseY + diff / 2 - MobileShellState.TopPanelControls.panelHeight;
return baseY + diff / 2;
}
function closeAll() {
for (var i = 0; i < repeater.count; i++) {
repeater.itemAt(i).closeApp();
}
}
function minimizeAll() {
for (var i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
// update property
if (!item.window.minimized) {
taskSwitcherState.wasInActiveTask = true;
}
// minimize window immediately if it shows up
item.minimizeApp();
}
}
function jumpToFirstVisibleWindow() {
for (var i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (!item.window.minimized) {
taskSwitcherState.goToTaskIndex(i);
break;
}
}
}
transform: Scale {
origin.x: root.width / 2
origin.y: root.height / 2
xScale: taskSwitcherState.currentScale
yScale: taskSwitcherState.currentScale
}
function closeAll() {
for (var i = 0; i < repeater.count; i++) {
repeater.itemAt(i).closeApp();
}
}
// taphandler activates even if delegate touched
TapHandler {
enabled: !taskSwitcherState.currentlyBeingOpened
onTapped: {
// if tapped on the background, then hide
if (root.childAt(eventPoint.position.x, eventPoint.position.y) === null) {
taskSwitcher.hide();
}
}
onPressedChanged: {
if (pressed) {
// ensure animations aren't running when finger is pressed
@ -61,54 +87,51 @@ Item {
}
}
}
Repeater {
id: repeater
model: taskSwitcher.tasksModel
// left margin from root edge such that the task is centered
readonly property real leftMargin: (root.width / 2) - (taskSwitcherState.taskWidth / 2)
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 {
Behavior on listX {
NumberAnimation {
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
easing.type: Easing.InOutQuad
}
}
// this is the actual displayed x-position on screen
x: listX + repeater.leftMargin - taskSwitcherState.xPosition
y: root.taskY
// ensure current task is above others
z: taskSwitcherState.currentTaskIndex === currentIndex ? 1 : 0
// only show header once task switcher is opened
showHeader: !taskSwitcherState.currentlyBeingOpened
// darken effect as task gets away from the centre of the screen
darken: {
let distFromCentreProgress = Math.abs(x - repeater.leftMargin) / taskSwitcherState.taskWidth;
let upperBoundAdjust = Math.min(0.5, distFromCentreProgress) - 0.2;
return Math.max(0, upperBoundAdjust);
}
width: taskSwitcherState.taskWidth
height: taskSwitcherState.taskHeight
previewWidth: taskSwitcherState.previewWidth
previewHeight: taskSwitcherState.previewHeight
taskSwitcher: root.taskSwitcher
displaysModel: root.taskSwitcher.displaysModel
}
}
}

View file

@ -0,0 +1,269 @@
// SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.15
import Qt5Compat.GraphicalEffects
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.kirigami 2.19 as Kirigami
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.kwin 3.0 as KWinComponents
import org.kde.kwin.private.effects 1.0
/**
* Component that provides a task switcher.
*/
FocusScope {
id: root
focus: true
readonly property QtObject effect: KWinComponents.SceneView.effect
readonly property QtObject targetScreen: KWinComponents.SceneView.screen
readonly property real topMargin: 0
readonly property real bottomMargin: 0
readonly property real leftMargin: 0
readonly property real rightMargin: 0
property var taskSwitcherState: TaskSwitcherState {
taskSwitcher: root
}
KWinComponents.WindowModel {
id: stackModel
}
KWinComponents.VirtualDesktopModel {
id: desktopModel
}
property var tasksModel: KWinComponents.WindowFilterModel {
activity: KWinComponents.Workspace.currentActivity
desktop: KWinComponents.Workspace.currentDesktop
screenName: root.targetScreen.name
windowModel: stackModel
minimizedWindows: true
windowType: ~KWinComponents.WindowFilterModel.Dock &
~KWinComponents.WindowFilterModel.Desktop &
~KWinComponents.WindowFilterModel.Notification &
~KWinComponents.WindowFilterModel.CriticalNotification
}
readonly property int tasksCount: taskList.count
// keep track of task list events
property int oldTasksCount: tasksCount
onTasksCountChanged: {
if (tasksCount === 0 && oldTasksCount !== 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);
}
oldTasksCount = tasksCount;
}
Keys.onEscapePressed: hide();
Component.onCompleted: {
taskList.jumpToFirstVisibleWindow();
taskList.minimizeAll();
taskSwitcherState.currentlyBeingOpened = true;
// fully open the panel (if this is a button press, not gesture)
if (!root.effect.gestureInProgress) {
taskSwitcherState.open();
}
}
// called by c++ plugin
function hideAnimation() {
closeAnim.restart();
}
function instantHide() {
root.effect.deactivate(true);
}
function hide() {
root.effect.deactivate(false);
}
// scroll to delegate index, and activate it
function activateWindow(index, window) {
KWinComponents.Workspace.activeClient = window;
taskSwitcherState.openApp(index, window);
}
function setSingleActiveWindow(id) {
instantHide();
}
Connections {
target: root.effect
function onPartialActivationFactorChanged() {
taskSwitcherState.positionY = taskSwitcherState.openedYPosition * root.effect.partialActivationFactor;
}
function onGestureInProgressChanged() {
if (!root.effect.gestureInProgress) {
taskSwitcherState.updateState();
}
}
}
KWinComponents.DesktopBackground {
id: backgroundItem
activity: KWinComponents.Workspace.currentActivity
desktop: KWinComponents.Workspace.currentDesktop
outputName: targetScreen.name
}
FastBlur {
source: backgroundItem
anchors.fill: parent
opacity: container.opacity
radius: 50
cached: true
}
// background colour
Rectangle {
id: backgroundRect
anchors.fill: parent
opacity: container.opacity
color: {
// animate background colour only if we are *not* opening from the homescreen
if (taskSwitcherState.wasInActiveTask || !taskSwitcherState.currentlyBeingOpened) {
return Qt.rgba(0, 0, 0, 0.6);
} else {
return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition));
}
}
}
Item {
id: container
// provide shell margins
anchors.fill: parent
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: root.bottomMargin
anchors.topMargin: root.topMargin
NumberAnimation on opacity {
id: closeAnim
running: false
to: 0
duration: 200
easing.type: Easing.InOutQuad
onFinished: {
closeAllButton.closeRequested = false;
}
}
// placeholder message
ColumnLayout {
id: placeholder
spacing: PlasmaCore.Units.gridUnit
opacity: (root.tasksCount === 0 && !taskSwitcherState.currentlyBeingClosed) ? 0.9 : 0
Behavior on opacity { NumberAnimation { duration: 500 } }
anchors.centerIn: parent
Kirigami.Icon {
id: icon
Layout.alignment: Qt.AlignHCenter
implicitWidth: PlasmaCore.Units.iconSizes.large
implicitHeight: PlasmaCore.Units.iconSizes.large
source: "window"
color: "white"
}
PlasmaExtras.Heading {
Layout.fillWidth: true
Layout.maximumWidth: root.width * 0.75
Layout.alignment: Qt.AlignHCenter
color: "white"
level: 3
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
text: i18n("No applications are running.")
}
}
// flicking area for task switcher
FlickContainer {
id: flickable
anchors.fill: parent
taskSwitcherState: root.taskSwitcherState
// the item is effectively anchored to the flickable bounds
TaskList {
id: taskList
taskSwitcher: root
shellTopMargin: root.topMargin
shellBottomMargin: root.bottomMargin
opacity: {
// animate opacity only if we are *not* opening from the homescreen
if (taskSwitcherState.wasInActiveTask || !taskSwitcherState.currentlyBeingOpened) {
return 1;
} else {
return Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition);
}
}
x: flickable.contentX
width: flickable.width
height: flickable.height
PlasmaComponents.ToolButton {
id: closeAllButton
property bool closeRequested: false
visible: root.tasksCount !== 0
anchors {
bottom: parent.bottom
bottomMargin: taskList.taskY / 2
horizontalCenter: parent.horizontalCenter
}
PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
PlasmaCore.ColorScope.inherit: false
opacity: (taskSwitcherState.currentlyBeingOpened || taskSwitcherState.currentlyBeingClosed) ? 0.0 : 1.0
Behavior on opacity { NumberAnimation { duration: PlasmaCore.Units.shortDuration } }
icon.name: "edit-clear-history"
font.bold: true
text: closeRequested ? i18n("Confirm Close All") : i18n("Close All")
onClicked: {
if (closeRequested) {
taskList.closeAll();
} else {
closeRequested = true;
}
}
}
}
}
}
}

View file

@ -1,31 +1,27 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
// 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
import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState
import org.kde.kwin 3.0 as KWinComponents
/**
* 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.
// Decreasing xPosition results in the task switcher moving forward (to the second task, third task, etc), being the layout direction Right to Left.
@ -35,11 +31,11 @@ QtObject {
// 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
@ -51,54 +47,54 @@ QtObject {
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
// whether the task switcher is being closed: an animation is running
property bool currentlyBeingClosed: false
// whether we are in a swipe left/right gesture to walk through tasks
property bool scrollingTasks: 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 - taskSwitcher.topMargin - taskSwitcher.bottomMargin
readonly property real windowWidth: taskSwitcher.width - taskSwitcher.leftMargin - taskSwitcher.rightMargin
// 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;
@ -106,22 +102,22 @@ QtObject {
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));
// animate scale only if we are *not* opening from the homescreen
if ((wasInActiveTask || !currentlyBeingOpened) && !scrollingTasks) {
return finalScale;
}
return scrollingTasks ? maxScale : 1;
}
// ~~ signals and functions ~~
// cancel all animated moving, as another flick source is taking over
signal cancelAnimations()
onCancelAnimations: {
@ -130,37 +126,38 @@ QtObject {
closeAnim.stop();
xAnim.stop();
}
function open() {
openAnim.restart();
}
function close() {
closeAnim.restart();
}
function openApp(index) {
function openApp(index, window) {
animateGoToTaskIndex(index, PlasmaCore.Units.shortDuration);
openAppAnim.restart();
KWinComponents.Workspace.activeClient = window
}
// 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();
@ -173,7 +170,7 @@ QtObject {
// close task switcher and return to app
closeAnim.restart();
}
// update horizontal state
let duration = PlasmaCore.Units.longDuration * 2;
if (currentlyBeingOpened) {
@ -195,35 +192,38 @@ QtObject {
}
}
}
// ~~ property animators ~~
property var xAnim: NumberAnimation {
target: root
property: "xPosition"
easing.type: Easing.OutBack
}
property var openAnim: NumberAnimation {
property var openAnim: NumberAnimation {
target: root
property: "yPosition"
to: openedYPosition
duration: MobileShell.MobileShellSettings.animationsEnabled ? 300 : 0
easing.type: Easing.OutBack
duration: 300
easing.type: Easing.OutBack
onFinished: {
root.currentlyBeingOpened = false;
}
}
property var closeAnim: NumberAnimation {
property var closeAnim: NumberAnimation {
target: root
property: "yPosition"
to: 0
duration: MobileShell.MobileShellSettings.animationsEnabled ? PlasmaCore.Units.longDuration : 0
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
onStarted: root.currentlyBeingClosed = true
onFinished: {
root.currentlyBeingClosed = false;
root.currentlyBeingOpened = false;
scrollingTasks = false;
taskSwitcher.instantHide();
@ -233,16 +233,16 @@ QtObject {
}
}
}
property var openAppAnim: NumberAnimation {
target: root
property var openAppAnim: NumberAnimation {
target: root
property: "yPosition"
to: 0
duration: MobileShell.MobileShellSettings.animationsEnabled ? 300 : 0
duration: 300
easing.type: Easing.OutQuint
onStarted: root.currentlyBeingClosed = true
onFinished: {
root.currentlyBeingClosed = false;
root.currentlyBeingOpened = false;