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(KF6_MIN_VERSION "5.240.0")
set(KDE_COMPILERSETTINGS_LEVEL "5.82") set(KDE_COMPILERSETTINGS_LEVEL "5.82")
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE) 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 find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
I18n I18n
GlobalAccel
KIO KIO
Config Config
DBusAddons 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) pkg_check_modules(GIO gio-2.0 REQUIRED IMPORTED_TARGET)
find_package(KF6KirigamiAddons 0.6 REQUIRED) 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(LibKWorkspace CONFIG REQUIRED)
find_package(KWinDBusInterface) find_package(KWinDBusInterface)

View file

@ -95,9 +95,6 @@ void MobileShellPlugin::registerTypes(const char *uri)
// /statusbar // /statusbar
qmlRegisterType(resolvePath("statusbar/StatusBar.qml"), uri, 1, 0, "StatusBar"); qmlRegisterType(resolvePath("statusbar/StatusBar.qml"), uri, 1, 0, "StatusBar");
// /taskswitcher
qmlRegisterType(resolvePath("taskswitcher/TaskSwitcher.qml"), uri, 1, 0, "TaskSwitcher");
// /widgets // /widgets
qmlRegisterType(resolvePath("widgets/krunner/KRunnerWidget.qml"), uri, 1, 0, "KRunnerWidget"); qmlRegisterType(resolvePath("widgets/krunner/KRunnerWidget.qml"), uri, 1, 0, "KRunnerWidget");
qmlRegisterType(resolvePath("widgets/mediacontrols/MediaControlsWidget.qml"), uri, 1, 0, "MediaControlsWidget"); 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"; background.state = "open";
MobileShellState.HomeScreenControls.taskSwitcher.minimizeAll();
} }
function close() { function close() {

View file

@ -44,7 +44,7 @@ Item {
* Whether a component is being shown on top of the homescreen within the same * Whether a component is being shown on top of the homescreen within the same
* window. * window.
*/ */
readonly property bool overlayShown: taskSwitcher.visible || startupFeedback.visible readonly property bool overlayShown: startupFeedback.visible
/** /**
* Margins for the homescreen, taking panels into account. * Margins for the homescreen, taking panels into account.
@ -82,8 +82,9 @@ Item {
} }
MobileShellState.HomeScreenControls.resetHomeScreenPosition(); MobileShellState.HomeScreenControls.resetHomeScreenPosition();
taskSwitcher.visible = false; // will trigger homescreen open
taskSwitcher.minimizeAll(); MobileShell.WindowUtil.unsetAllMinimizedGeometries(root);
MobileShell.WindowUtil.minimizeAll();
root.homeTriggered(); root.homeTriggered();
} }
@ -109,7 +110,6 @@ Item {
Plasmoid.onScreenChanged: { Plasmoid.onScreenChanged: {
if (plasmoid.screen == 0) { if (plasmoid.screen == 0) {
MobileShellState.HomeScreenControls.taskSwitcher = taskSwitcher;
MobileShellState.HomeScreenControls.homeScreenWindow = root.Window.window; MobileShellState.HomeScreenControls.homeScreenWindow = root.Window.window;
} }
} }
@ -127,7 +127,6 @@ Item {
// set API variables // set API variables
if (plasmoid.screen == 0) { if (plasmoid.screen == 0) {
MobileShellState.HomeScreenControls.taskSwitcher = taskSwitcher;
MobileShellState.HomeScreenControls.homeScreenWindow = root.Window.window; MobileShellState.HomeScreenControls.homeScreenWindow = root.Window.window;
} }
} }
@ -203,12 +202,10 @@ Item {
function evaluateAnimChange() { function evaluateAnimChange() {
// only animate if homescreen is visible // only animate if homescreen is visible
if (!taskSwitcher.visible) { if (!visibleMaximizedWindowsModel.isWindowMaximized || MobileShell.WindowUtil.activeWindowIsShell) {
if (!visibleMaximizedWindowsModel.isWindowMaximized || MobileShell.WindowUtil.activeWindowIsShell) { itemContainer.zoomIn();
itemContainer.zoomIn(); } else {
} else { itemContainer.zoomOut();
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 // start app animation component
MobileShell.StartupFeedback { MobileShell.StartupFeedback {
id: 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/StatusBar.qml</file>
<file>qml/statusbar/TaskWidget.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/krunner/KRunnerWidget.qml</file>
<file>qml/widgets/mediacontrols/BlurredBackground.qml</file> <file>qml/widgets/mediacontrols/BlurredBackground.qml</file>

View file

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

View file

@ -52,11 +52,7 @@ ContainmentLayoutManager.ItemContainer {
return; return;
} }
if (!MobileShellState.Shell.taskSwitcherVisible) { desktopModel.setMinimizedDelegate(index, delegate);
desktopModel.setMinimizedDelegate(index, delegate);
} else {
desktopModel.unsetMinimizedDelegate(index, delegate);
}
} }
function launchApp() { function launchApp() {

View file

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

View file

@ -48,7 +48,12 @@ MobileShell.HomeScreen {
if (!MobileShell.WindowUtil.showDesktop && !MobileShellState.Shell.homeScreenVisible if (!MobileShell.WindowUtil.showDesktop && !MobileShellState.Shell.homeScreenVisible
|| search.isOpen || 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) { if (search.isOpen) {
search.close(); search.close();
} }
@ -90,14 +95,6 @@ MobileShell.HomeScreen {
bottomMargin: root.bottomMargin bottomMargin: root.bottomMargin
leftMargin: root.leftMargin leftMargin: root.leftMargin
rightMargin: root.rightMargin 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-2023 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2021-2022 Devin Lin <devin@kde.org> // SPDX-License-Identifier: GPL-2.0-or-later
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.4 import QtQuick 2.4
import QtQuick.Layouts 1.1 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 1.0 as MobileShell
import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState
import org.kde.taskmanager 0.1 as TaskManager
MobileShell.NavigationPanel { MobileShell.NavigationPanel {
id: root id: root
@ -23,19 +21,35 @@ MobileShell.NavigationPanel {
// - opaque if an app is shown or vkbd is shown // - opaque if an app is shown or vkbd is shown
// - translucent if the task switcher is open // - translucent if the task switcher is open
// - transparent if on the homescreen // - transparent if on the homescreen
backgroundColor: { backgroundColor: (Keyboards.KWinVirtualKeyboard.visible || opaqueBar) ? PlasmaCore.ColorScope.backgroundColor : "transparent";
if (root.taskSwitcher.visible) { foregroundColorGroup: opaqueBar ? PlasmaCore.Theme.NormalColorGroup : PlasmaCore.Theme.ComplementaryColorGroup
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
shadow: !opaqueBar shadow: !opaqueBar
// 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: !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 // navigation panel actions
@ -44,24 +58,12 @@ MobileShell.NavigationPanel {
leftAction: MobileShell.NavigationPanelAction { leftAction: MobileShell.NavigationPanelAction {
id: taskSwitcherAction id: taskSwitcherAction
enabled: (root.taskSwitcher.tasksCount > 0) || root.taskSwitcher.visible enabled: true
iconSource: "mobile-task-switcher" iconSource: "mobile-task-switcher"
iconSizeFactor: 0.75 iconSizeFactor: 0.75
onTriggered: { onTriggered: {
MobileShell.WindowUtil.showDesktop = false; plasmoid.nativeInterface.triggerTaskSwitcher();
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();
}
}
} }
} }
@ -83,7 +85,7 @@ MobileShell.NavigationPanel {
rightAction: MobileShell.NavigationPanelAction { rightAction: MobileShell.NavigationPanelAction {
id: closeAppAction 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" 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 // mobile-close-app (from plasma-frameworks) seems to have less margins than icons from breeze-icons
iconSizeFactor: Keyboards.KWinVirtualKeyboard.visible ? 1 : 0.75 iconSizeFactor: Keyboards.KWinVirtualKeyboard.visible ? 1 : 0.75
@ -92,15 +94,10 @@ MobileShell.NavigationPanel {
if (Keyboards.KWinVirtualKeyboard.active) { if (Keyboards.KWinVirtualKeyboard.active) {
// close keyboard if it is open // close keyboard if it is open
Keyboards.KWinVirtualKeyboard.active = false; 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) { } else if (MobileShell.WindowUtil.hasCloseableActiveWindow) {
// if task switcher is closed, but there is an active window // if task switcher is closed, but there is an active window
if (root.taskSwitcher.tasksModel.activeTask !== 0) { if (tasksModel.activeTask !== 0) {
root.taskSwitcher.tasksModel.requestClose(root.taskSwitcher.tasksModel.activeTask); tasksModel.requestClose(tasksModel.activeTask);
} }
MobileShellState.Shell.closeAppLaunchAnimation(); MobileShellState.Shell.closeAppLaunchAnimation();
} }

View file

@ -142,7 +142,6 @@ PlasmaCore.ColorScope {
Component { Component {
id: navigationPanel id: navigationPanel
NavigationPanelComponent { NavigationPanelComponent {
taskSwitcher: MobileShellState.HomeScreenControls.taskSwitcher
opaqueBar: root.opaqueBar opaqueBar: root.opaqueBar
} }
} }
@ -150,9 +149,7 @@ PlasmaCore.ColorScope {
// bottom navigation gesture area component // bottom navigation gesture area component
Component { Component {
id: navigationGesture id: navigationGesture
MobileShell.NavigationGestureArea { MobileShell.NavigationGestureArea {}
taskSwitcher: MobileShellState.HomeScreenControls.taskSwitcher
}
} }
// load appropriate system navigation component // 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") K_PLUGIN_CLASS_WITH_JSON(TaskPanel, "package/metadata.json")
#include "taskpanel.moc" #include "taskpanel.moc"

View file

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

View file

@ -2,3 +2,4 @@
# SPDX-License-Identifier: LGPL-2.1-or-later # SPDX-License-Identifier: LGPL-2.1-or-later
add_subdirectory(scripts) 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-2023 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org> // SPDX-License-Identifier: LGPL-2.0-or-later
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
@ -10,8 +7,6 @@ import QtQuick.Layouts 1.15
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
import org.kde.plasma.components 3.0 as PlasmaComponents 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 { Flickable {
id: root id: root

View file

@ -1,31 +1,30 @@
/* // SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com> // SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org> // SPDX-License-Identifier: GPL-2.0-or-later
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 2.2 as QQC2 import QtQuick.Controls 2.2 as QQC2
import Qt5Compat.GraphicalEffects
import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents 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 { Item {
id: delegate id: delegate
required property var taskSwitcher required property var taskSwitcher
required property QtObject window
required property int index
required property var model required property var model
required property var displaysModel
required property real previewHeight required property real previewHeight
required property real previewWidth 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 readonly property real dragOffset: -control.y
property bool showHeader: true property bool showHeader: true
@ -34,32 +33,21 @@ Item {
opacity: 1 - dragOffset / taskSwitcher.height opacity: 1 - dragOffset / taskSwitcher.height
//BEGIN functions //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() { function closeApp() {
tasksModel.requestClose(tasksModel.index(model.index, 0)); delegate.window.closeWindow();
} }
function activateApp() { function activateApp() {
taskSwitcherState.wasInActiveTask = false; 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 //END functions
Component.onCompleted: syncDelegateGeometry();
Connections {
target: taskSwitcher
function onVisibleChanged() {
syncDelegateGeometry();
}
}
MouseArea { MouseArea {
id: control id: control
width: parent.width width: parent.width
@ -149,33 +137,17 @@ Item {
Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
usesPlasmaTheme: false usesPlasmaTheme: false
source: model.decoration source: delegate.window.icon
} }
PlasmaComponents.Label { PlasmaComponents.Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
text: model.AppName text: delegate.window.caption
color: "white" 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 { PlasmaComponents.ToolButton {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
z: 99 z: 99
@ -194,11 +166,11 @@ Item {
Layout.maximumWidth: delegate.previewWidth Layout.maximumWidth: delegate.previewWidth
Layout.maximumHeight: delegate.previewHeight Layout.maximumHeight: delegate.previewHeight
color: PlasmaCore.Theme.backgroundColor color: "transparent"
clip: true clip: true
// scale animation on press // 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 { Behavior on zoomScale {
NumberAnimation { NumberAnimation {
duration: 200 duration: 200
@ -217,30 +189,17 @@ Item {
id: item id: item
anchors.fill: parent anchors.fill: parent
// app icon (behind window preview in-case it doesn't load) KWinComponents.WindowThumbnail {
TaskIcon { id: thumbSource
// decrease the opacity faster wId: delegate.window.internalId
opacity: pipeWireLoader.item && pipeWireLoader.item.uuid ? 0 : delegate.opacity
anchors.centerIn: parent
}
// attempt to load window preview
Loader {
id: pipeWireLoader
active: (taskSwitcher.visible || taskSwitcher.tasksModel.taskReorderingEnabled) && MobileShell.MobileShellSettings.taskSwitcherPreviewsEnabled
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl("./Thumbnail.qml")
asynchronous: true layer {
enabled: true
onLoaded: this.item.refresh() effect: ColorOverlay {
} color: Qt.rgba(0, 0, 0, delegate.darken)
}
// darken effect }
Rectangle {
anchors.fill: parent
color: "black"
opacity: delegate.darken
} }
TapHandler { TapHandler {
@ -254,3 +213,4 @@ Item {
} }
} }

View file

@ -1,8 +1,5 @@
/* // SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org> // SPDX-License-Identifier: GPL-2.0-or-later
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
@ -10,10 +7,13 @@ import QtQuick.Layouts 1.1
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
import org.kde.plasma.components 3.0 as PlasmaComponents 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 { Item {
id: root id: root
readonly property int count: repeater.count
required property real shellTopMargin required property real shellTopMargin
required property real shellBottomMargin required property real shellBottomMargin
@ -28,7 +28,38 @@ Item {
let baseY = (taskSwitcher.height / 2) - (taskSwitcherState.taskHeight / 2) - (taskSwitcherState.taskHeaderHeight / 2) 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 { transform: Scale {
@ -38,12 +69,6 @@ Item {
yScale: 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 activates even if delegate touched
TapHandler { TapHandler {
enabled: !taskSwitcherState.currentlyBeingOpened enabled: !taskSwitcherState.currentlyBeingOpened
@ -54,6 +79,7 @@ Item {
taskSwitcher.hide(); taskSwitcher.hide();
} }
} }
onPressedChanged: { onPressedChanged: {
if (pressed) { if (pressed) {
// ensure animations aren't running when finger is pressed // ensure animations aren't running when finger is pressed
@ -71,7 +97,6 @@ Item {
delegate: Task { delegate: Task {
id: task id: task
readonly property int currentIndex: model.index readonly property int currentIndex: model.index
// this is the x-position with respect to the list // this is the x-position with respect to the list
@ -86,7 +111,6 @@ Item {
// this is the actual displayed x-position on screen // this is the actual displayed x-position on screen
x: listX + repeater.leftMargin - taskSwitcherState.xPosition x: listX + repeater.leftMargin - taskSwitcherState.xPosition
y: root.taskY y: root.taskY
// ensure current task is above others // ensure current task is above others
@ -108,7 +132,6 @@ Item {
previewHeight: taskSwitcherState.previewHeight previewHeight: taskSwitcherState.previewHeight
taskSwitcher: root.taskSwitcher 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,15 +1,11 @@
/* // SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org> // SPDX-License-Identifier: GPL-2.0-or-later
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15 import QtQuick 2.15
import org.kde.plasma.core 2.1 as PlasmaCore import org.kde.plasma.core 2.1 as PlasmaCore
import org.kde.plasma.private.mobileshell 1.0 as MobileShell import org.kde.kwin 3.0 as KWinComponents
import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState
/** /**
* State object for the task switcher. * State object for the task switcher.
@ -139,9 +135,10 @@ QtObject {
closeAnim.restart(); closeAnim.restart();
} }
function openApp(index) { function openApp(index, window) {
animateGoToTaskIndex(index, PlasmaCore.Units.shortDuration); animateGoToTaskIndex(index, PlasmaCore.Units.shortDuration);
openAppAnim.restart(); openAppAnim.restart();
KWinComponents.Workspace.activeClient = window
} }
// get the xPosition where the task will be centered on the screen // get the xPosition where the task will be centered on the screen
@ -208,7 +205,7 @@ QtObject {
target: root target: root
property: "yPosition" property: "yPosition"
to: openedYPosition to: openedYPosition
duration: MobileShell.MobileShellSettings.animationsEnabled ? 300 : 0 duration: 300
easing.type: Easing.OutBack easing.type: Easing.OutBack
onFinished: { onFinished: {
@ -220,10 +217,13 @@ QtObject {
target: root target: root
property: "yPosition" property: "yPosition"
to: 0 to: 0
duration: MobileShell.MobileShellSettings.animationsEnabled ? PlasmaCore.Units.longDuration : 0 duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad easing.type: Easing.InOutQuad
onStarted: root.currentlyBeingClosed = true
onFinished: { onFinished: {
root.currentlyBeingClosed = false;
root.currentlyBeingOpened = false; root.currentlyBeingOpened = false;
scrollingTasks = false; scrollingTasks = false;
taskSwitcher.instantHide(); taskSwitcher.instantHide();
@ -238,7 +238,7 @@ QtObject {
target: root target: root
property: "yPosition" property: "yPosition"
to: 0 to: 0
duration: MobileShell.MobileShellSettings.animationsEnabled ? 300 : 0 duration: 300
easing.type: Easing.OutQuint easing.type: Easing.OutQuint
onStarted: root.currentlyBeingClosed = true onStarted: root.currentlyBeingClosed = true