diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e60648a..f618c7fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/components/mobileshell/mobileshellplugin.cpp b/components/mobileshell/mobileshellplugin.cpp index 2d604e7c..88b024d1 100644 --- a/components/mobileshell/mobileshellplugin.cpp +++ b/components/mobileshell/mobileshellplugin.cpp @@ -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"); diff --git a/components/mobileshell/qml/components/StartupFeedback.qml b/components/mobileshell/qml/components/StartupFeedback.qml index aee435d3..adf64d2e 100644 --- a/components/mobileshell/qml/components/StartupFeedback.qml +++ b/components/mobileshell/qml/components/StartupFeedback.qml @@ -40,8 +40,6 @@ MouseArea { // use mousearea to ensure clicks don't go behind } background.state = "open"; - - MobileShellState.HomeScreenControls.taskSwitcher.minimizeAll(); } function close() { diff --git a/components/mobileshell/qml/homescreen/HomeScreen.qml b/components/mobileshell/qml/homescreen/HomeScreen.qml index 8bfc3e96..9f4c973d 100644 --- a/components/mobileshell/qml/homescreen/HomeScreen.qml +++ b/components/mobileshell/qml/homescreen/HomeScreen.qml @@ -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 diff --git a/components/mobileshell/qml/taskswitcher/TaskIcon.qml b/components/mobileshell/qml/taskswitcher/TaskIcon.qml deleted file mode 100644 index 1668bd44..00000000 --- a/components/mobileshell/qml/taskswitcher/TaskIcon.qml +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015 Marco Martin - * - * 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 -} diff --git a/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml b/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml deleted file mode 100644 index fb7d3be4..00000000 --- a/components/mobileshell/qml/taskswitcher/TaskSwitcher.qml +++ /dev/null @@ -1,283 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015 Marco Martin - * SPDX-FileCopyrightText: 2021-2022 Devin Lin - * - * 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; - } - } - } - } - } - } -} diff --git a/components/mobileshell/qml/taskswitcher/Thumbnail.qml b/components/mobileshell/qml/taskswitcher/Thumbnail.qml deleted file mode 100644 index 8fda0883..00000000 --- a/components/mobileshell/qml/taskswitcher/Thumbnail.qml +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Marco Martin - * - * 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: "" - } -} - - diff --git a/components/mobileshell/resources.qrc b/components/mobileshell/resources.qrc index 59732d1a..97d868be 100644 --- a/components/mobileshell/resources.qrc +++ b/components/mobileshell/resources.qrc @@ -63,14 +63,6 @@ qml/statusbar/StatusBar.qml qml/statusbar/TaskWidget.qml - qml/taskswitcher/FlickContainer.qml - qml/taskswitcher/Task.qml - qml/taskswitcher/TaskIcon.qml - qml/taskswitcher/TaskList.qml - qml/taskswitcher/TaskSwitcher.qml - qml/taskswitcher/TaskSwitcherState.qml - qml/taskswitcher/Thumbnail.qml - qml/widgets/krunner/KRunnerWidget.qml qml/widgets/mediacontrols/BlurredBackground.qml diff --git a/components/mobileshellstate/qml/Shell.qml b/components/mobileshellstate/qml/Shell.qml index 8309d861..11db8c99 100644 --- a/components/mobileshellstate/qml/Shell.qml +++ b/components/mobileshellstate/qml/Shell.qml @@ -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. */ diff --git a/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml b/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml index 232aa194..117d68b3 100644 --- a/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml +++ b/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml @@ -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() { diff --git a/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml b/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml index dfa05ec2..e909768e 100644 --- a/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml +++ b/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml @@ -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 { diff --git a/containments/homescreens/halcyon/package/contents/ui/main.qml b/containments/homescreens/halcyon/package/contents/ui/main.qml index f1eab767..2f2ec5db 100644 --- a/containments/homescreens/halcyon/package/contents/ui/main.qml +++ b/containments/homescreens/halcyon/package/contents/ui/main.qml @@ -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(); - } - } } } } diff --git a/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml b/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml index 340b9253..843bb0a4 100644 --- a/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml +++ b/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml @@ -1,8 +1,5 @@ -/* - * SPDX-FileCopyrightText: 2021-2022 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2021-2023 Devin Lin +// 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(); } diff --git a/containments/taskpanel/package/contents/ui/main.qml b/containments/taskpanel/package/contents/ui/main.qml index e60a638d..877c0bd2 100644 --- a/containments/taskpanel/package/contents/ui/main.qml +++ b/containments/taskpanel/package/contents/ui/main.qml @@ -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 diff --git a/containments/taskpanel/taskpanel.cpp b/containments/taskpanel/taskpanel.cpp index a9654f55..4799cdf6 100644 --- a/containments/taskpanel/taskpanel.cpp +++ b/containments/taskpanel/taskpanel.cpp @@ -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" diff --git a/containments/taskpanel/taskpanel.h b/containments/taskpanel/taskpanel.h index 30e81cdd..5ce59ff1 100644 --- a/containments/taskpanel/taskpanel.h +++ b/containments/taskpanel/taskpanel.h @@ -41,6 +41,8 @@ public: QAbstractItemModel *outputs() const; + Q_INVOKABLE void triggerTaskSwitcher() const; + Q_SIGNALS: void panelChanged(); void locationChanged(); diff --git a/kwin/CMakeLists.txt b/kwin/CMakeLists.txt index 7d5a5f7a..37ff1c6a 100644 --- a/kwin/CMakeLists.txt +++ b/kwin/CMakeLists.txt @@ -2,3 +2,4 @@ # SPDX-License-Identifier: LGPL-2.1-or-later add_subdirectory(scripts) +add_subdirectory(mobiletaskswitcher) \ No newline at end of file diff --git a/kwin/mobiletaskswitcher/CMakeLists.txt b/kwin/mobiletaskswitcher/CMakeLists.txt new file mode 100644 index 00000000..6b6fbb8b --- /dev/null +++ b/kwin/mobiletaskswitcher/CMakeLists.txt @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2022 Devin Lin +# 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) diff --git a/kwin/mobiletaskswitcher/main.cpp b/kwin/mobiletaskswitcher/main.cpp new file mode 100644 index 00000000..2a802b9e --- /dev/null +++ b/kwin/mobiletaskswitcher/main.cpp @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// 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" diff --git a/kwin/mobiletaskswitcher/metadata.json b/kwin/mobiletaskswitcher/metadata.json new file mode 100644 index 00000000..7c3a34c9 --- /dev/null +++ b/kwin/mobiletaskswitcher/metadata.json @@ -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 +} + diff --git a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp new file mode 100644 index 00000000..c420ae26 --- /dev/null +++ b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "mobiletaskswitchereffect.h" + +#include +#include +#include + +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 activateBorders = {ElectricBorder::ElectricBottom}; + for (const int &border : activateBorders) { + m_borderActivate.append(ElectricBorder(border)); + effects->reserveElectricBorder(ElectricBorder(border), this); + } + + const QList 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(); + } +} +} diff --git a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h new file mode 100644 index 00000000..73c705c6 --- /dev/null +++ b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#include +#include +#include + +#include +#include + +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 m_toggleShortcut; + Status m_status = Status::Inactive; + ; + QTimer *m_shutdownTimer; + QList m_borderActivate; + QList m_touchBorderActivate; + + int m_animationDuration = 400; + qreal m_partialActivationFactor = 0; + bool m_gestureInProgress = false; +}; + +} // namespace KWin diff --git a/components/mobileshell/qml/taskswitcher/FlickContainer.qml b/kwin/mobiletaskswitcher/qml/FlickContainer.qml similarity index 82% rename from components/mobileshell/qml/taskswitcher/FlickContainer.qml rename to kwin/mobiletaskswitcher/qml/FlickContainer.qml index 6f2227ea..bb3ac4cc 100644 --- a/components/mobileshell/qml/taskswitcher/FlickContainer.qml +++ b/kwin/mobiletaskswitcher/qml/FlickContainer.qml @@ -1,8 +1,5 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2021-2023 Devin Lin +// 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; diff --git a/components/mobileshell/qml/taskswitcher/Task.qml b/kwin/mobiletaskswitcher/qml/Task.qml similarity index 59% rename from components/mobileshell/qml/taskswitcher/Task.qml rename to kwin/mobiletaskswitcher/qml/Task.qml index 83c63485..ff4e3403 100644 --- a/components/mobileshell/qml/taskswitcher/Task.qml +++ b/kwin/mobiletaskswitcher/qml/Task.qml @@ -1,97 +1,85 @@ -/* - * SPDX-FileCopyrightText: 2015 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2015 Marco Martin +// SPDX-FileCopyrightText: 2021-2023 Devin Lin +// 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 { } } + diff --git a/components/mobileshell/qml/taskswitcher/TaskList.qml b/kwin/mobiletaskswitcher/qml/TaskList.qml similarity index 75% rename from components/mobileshell/qml/taskswitcher/TaskList.qml rename to kwin/mobiletaskswitcher/qml/TaskList.qml index bcace0c6..b105111f 100644 --- a/components/mobileshell/qml/taskswitcher/TaskList.qml +++ b/kwin/mobiletaskswitcher/qml/TaskList.qml @@ -1,8 +1,5 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2021 Devin Lin +// 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 } } } diff --git a/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml b/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml new file mode 100644 index 00000000..fc5f62b0 --- /dev/null +++ b/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml @@ -0,0 +1,269 @@ +// SPDX-FileCopyrightText: 2015 Marco Martin +// SPDX-FileCopyrightText: 2021-2023 Devin Lin +// 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; + } + } + } + } + } + } +} + diff --git a/components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml b/kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml similarity index 88% rename from components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml rename to kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml index 230e8ee1..9123ad9f 100644 --- a/components/mobileshell/qml/taskswitcher/TaskSwitcherState.qml +++ b/kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml @@ -1,31 +1,27 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2021 Devin Lin +// 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;