From 28f7224a5b0185606d0281802473f1f40ed72c4c Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Sat, 13 Dec 2025 23:38:13 -0500 Subject: [PATCH] gestures: Add optional gesture guide panel This adds a gesture handle mode to the navigation panel, which can be enabled during gesture-only mode. This reserves space for the system gesture to be able to be used, allowing us to extend the height in KWin of the gesture recognition area (which is currently far too short for devices such as Pixel 3a). This also allows for navigation with a mouse; clicking on the handle triggers the task switcher, holding it triggers the "home" action. --- components/mobileshell/CMakeLists.txt | 1 + .../mobileshell/qml/components/Constants.qml | 6 +- .../qml/navigationpanel/GesturePanel.qml | 48 +++++++ .../mobileshellsettings.cpp | 35 ++++- .../shellsettingsplugin/mobileshellsettings.h | 18 ++- containments/taskpanel/qml/main.qml | 52 +++++-- kcms/navigation/ui/main.qml | 15 ++ .../package/contents/ui/TaskSwitcher.qml | 130 ++++++++++++------ .../package/contents/ui/main.qml | 5 +- 9 files changed, 248 insertions(+), 62 deletions(-) create mode 100644 components/mobileshell/qml/navigationpanel/GesturePanel.qml diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt index de291ae2..f3466673 100644 --- a/components/mobileshell/CMakeLists.txt +++ b/components/mobileshell/CMakeLists.txt @@ -67,6 +67,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES qml/homescreen/HomeScreen.qml qml/homescreen/WallpaperSelector.qml + qml/navigationpanel/GesturePanel.qml qml/navigationpanel/NavigationPanel.qml qml/navigationpanel/NavigationPanelAction.qml qml/navigationpanel/NavigationPanelButton.qml diff --git a/components/mobileshell/qml/components/Constants.qml b/components/mobileshell/qml/components/Constants.qml index 1255f305..4ab68605 100644 --- a/components/mobileshell/qml/components/Constants.qml +++ b/components/mobileshell/qml/components/Constants.qml @@ -28,11 +28,13 @@ QtObject { } readonly property real defaultNavigationPanelThickness: Kirigami.Units.gridUnit * 2 + readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit readonly property real navigationPanelThickness: { if (!ShellSettings.Settings.navigationPanelEnabled) { - return 0; - } else if (root.panelSettings.navigationPanelHeight <= 0) { + return ShellSettings.Settings.gesturePanelEnabled ? defaultGesturePanelThickness : 0; + } + if (root.panelSettings.navigationPanelHeight <= 0) { return defaultNavigationPanelThickness; } return root.panelSettings.navigationPanelHeight; diff --git a/components/mobileshell/qml/navigationpanel/GesturePanel.qml b/components/mobileshell/qml/navigationpanel/GesturePanel.qml new file mode 100644 index 00000000..76e84ebd --- /dev/null +++ b/components/mobileshell/qml/navigationpanel/GesturePanel.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2025 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Window + +import org.kde.plasma.plasmoid + +import org.kde.plasma.private.mobileshell as MobileShell +import org.kde.plasma.private.mobileshell.state as MobileShellState +import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings + +import org.kde.kirigami as Kirigami + +Rectangle { + id: root + + // Whether the bar background should be opaque + required property bool opaqueBar + + signal handleClicked() + signal handlePressedAndHeld() + + color: opaqueBar ? Kirigami.Theme.backgroundColor : "transparent" + + // Handle + MouseArea { + anchors.centerIn: parent + width: Math.min(root.width * 0.2, Kirigami.Units.gridUnit * 12) + height: parent.height + + cursorShape: Qt.PointingHandCursor + + onClicked: root.handleClicked() + onPressAndHold: root.handlePressedAndHeld() + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: 4 + radius: height / 2 + + opacity: 0.8 + color: Kirigami.Theme.textColor + } + } +} diff --git a/components/shellsettingsplugin/mobileshellsettings.cpp b/components/shellsettingsplugin/mobileshellsettings.cpp index 8ce0ad5e..88beb01e 100644 --- a/components/shellsettingsplugin/mobileshellsettings.cpp +++ b/components/shellsettingsplugin/mobileshellsettings.cpp @@ -36,6 +36,7 @@ MobileShellSettings::MobileShellSettings(QObject *parent) Q_EMIT statusBarScaleFactorChanged(); Q_EMIT showBatteryPercentageChanged(); Q_EMIT navigationPanelEnabledChanged(); + Q_EMIT gesturePanelEnabledChanged(); Q_EMIT alwaysShowKeyboardToggleOnNavigationPanelChanged(); Q_EMIT keyboardButtonEnabledChanged(); Q_EMIT taskSwitcherPreviewsEnabledChanged(); @@ -145,7 +146,22 @@ void MobileShellSettings::setNavigationPanelEnabled(bool navigationPanelEnabled) group.writeEntry("navigationPanelEnabled", navigationPanelEnabled, KConfigGroup::Notify); m_config->sync(); - updateNavigationBarsInPlasma(navigationPanelEnabled); + updateNavigationBarsInPlasma(); +} + +bool MobileShellSettings::gesturePanelEnabled() const +{ + auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP}; + return group.readEntry("gesturePanelEnabled", true); +} + +void MobileShellSettings::setGesturePanelEnabled(bool gesturePanelEnabled) +{ + auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP}; + group.writeEntry("gesturePanelEnabled", gesturePanelEnabled, KConfigGroup::Notify); + m_config->sync(); + + updateNavigationBarsInPlasma(); } bool MobileShellSettings::alwaysShowKeyboardToggleOnNavigationPanel() const @@ -232,7 +248,7 @@ void MobileShellSettings::setAutoHidePanelsEnabled(bool enabled) m_config->sync(); } -void MobileShellSettings::updateNavigationBarsInPlasma(bool navigationPanelEnabled) +void MobileShellSettings::updateNavigationBarsInPlasma() { // Do not update panels when not in Plasma Mobile bool isMobilePlatform = KRuntimePlatform::runtimePlatform().contains("phone"); @@ -245,13 +261,22 @@ void MobileShellSettings::updateNavigationBarsInPlasma(bool navigationPanelEnabl QLatin1String("org.kde.PlasmaShell"), QLatin1String("evaluateScript")); - if (navigationPanelEnabled) { + if (navigationPanelEnabled() || gesturePanelEnabled()) { QString createNavigationPanelScript = R"( - loadTemplate("org.kde.plasma.mobile.defaultNavigationPanel"); + let allPanels = panels(); + let foundPanel = false; + for (var i = 0; i < allPanels.length; i++) { + if (allPanels[i].type === "org.kde.plasma.mobile.taskpanel") { + foundPanel = true; + } + } + + if (!foundPanel) { + loadTemplate("org.kde.plasma.mobile.defaultNavigationPanel"); + } )"; message << createNavigationPanelScript; - } else { QString deleteNavigationPanelScript = R"( let allPanels = panels(); diff --git a/components/shellsettingsplugin/mobileshellsettings.h b/components/shellsettingsplugin/mobileshellsettings.h index 4b3f5fc0..64be056b 100644 --- a/components/shellsettingsplugin/mobileshellsettings.h +++ b/components/shellsettingsplugin/mobileshellsettings.h @@ -37,6 +37,7 @@ class MobileShellSettings : public QObject // navigation panel Q_PROPERTY(bool navigationPanelEnabled READ navigationPanelEnabled WRITE setNavigationPanelEnabled NOTIFY navigationPanelEnabledChanged) Q_PROPERTY(bool alwaysShowKeyboardToggleOnNavigationPanel READ alwaysShowKeyboardToggleOnNavigationPanel WRITE setAlwaysShowKeyboardToggleOnNavigationPanel NOTIFY alwaysShowKeyboardToggleOnNavigationPanelChanged) + Q_PROPERTY(bool gesturePanelEnabled READ gesturePanelEnabled WRITE setGesturePanelEnabled NOTIFY gesturePanelEnabledChanged) // action drawer Q_PROPERTY(ActionDrawerMode actionDrawerTopLeftMode READ actionDrawerTopLeftMode WRITE setActionDrawerTopLeftMode NOTIFY actionDrawerTopLeftModeChanged) @@ -172,6 +173,20 @@ public: */ void setNavigationPanelEnabled(bool navigationPanelEnabled); + /** + * Whether the gesture panel is enabled. + * + * This is only applicable when navigationPanelEnabled() = false (gesture mode). + */ + bool gesturePanelEnabled() const; + + /** + * Set whether the gesture panel is enabled. + * + * @param gesturePanelEnabled Whether the gesture panel should be enabled. + */ + void setGesturePanelEnabled(bool gesturePanelEnabled); + /** * Set whether the keyboard toggle button should always show on the navigation panel, regardless of * whether the app properly supports virtual keyboards. @@ -282,6 +297,7 @@ Q_SIGNALS: void vibrationsEnabledChanged(); void vibrationDurationChanged(); void navigationPanelEnabledChanged(); + void gesturePanelEnabledChanged(); void alwaysShowKeyboardToggleOnNavigationPanelChanged(); void keyboardButtonEnabledChanged(); void animationsEnabledChanged(); @@ -299,7 +315,7 @@ Q_SIGNALS: void lockscreenRightButtonActionChanged(); private: - void updateNavigationBarsInPlasma(bool navigationPanelEnabled); + void updateNavigationBarsInPlasma(); KConfigWatcher::Ptr m_configWatcher; KSharedConfig::Ptr m_config; diff --git a/containments/taskpanel/qml/main.qml b/containments/taskpanel/qml/main.qml index 43a7c555..106e0105 100644 --- a/containments/taskpanel/qml/main.qml +++ b/containments/taskpanel/qml/main.qml @@ -177,20 +177,48 @@ ContainmentItem { property real offset: 0 + Component { + id: navigationPanelComponent + + NavigationPanelComponent { + isVertical: root.inLandscape + opaqueBar: root.opaqueBar + forcedComplementary: !opaqueBar && !startupFeedbackColorAnimation.isShowing + + transform: [ + Translate { + y: inLandscape ? 0 : navigationPanel.offset + x: inLandscape ? navigationPanel.offset : 0 + } + ] + } + } + + Component { + id: gesturePanelComponent + + MobileShell.GesturePanel { + opaqueBar: root.opaqueBar + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: (!opaqueBar && !startupFeedbackColorAnimation.isShowing) ? Kirigami.Theme.Complementary : Kirigami.Theme.Window + + onHandlePressedAndHeld: MobileShellState.ShellDBusClient.openHomeScreen() + onHandleClicked: Plasmoid.triggerTaskSwitcher() + + transform: [ + Translate { + y: inLandscape ? 0 : navigationPanel.offset + x: inLandscape ? navigationPanel.offset : 0 + } + ] + } + } + // load appropriate system navigation component - NavigationPanelComponent { + Loader { anchors.fill: parent - - isVertical: root.inLandscape - opaqueBar: root.opaqueBar - forcedComplementary: !opaqueBar && !startupFeedbackColorAnimation.isShowing - - transform: [ - Translate { - y: inLandscape ? 0 : navigationPanel.offset - x: inLandscape ? navigationPanel.offset : 0 - } - ] + sourceComponent: ShellSettings.Settings.navigationPanelEnabled ? navigationPanelComponent : gesturePanelComponent } state: MobileShellState.ShellDBusClient.panelState diff --git a/kcms/navigation/ui/main.qml b/kcms/navigation/ui/main.qml index da4e3c5b..0c2dd39a 100644 --- a/kcms/navigation/ui/main.qml +++ b/kcms/navigation/ui/main.qml @@ -239,6 +239,21 @@ KCM.SimpleKCM { } } } + + FormCard.FormDelegateSeparator { visible: gesturePanelToggleDelegate.visible } + + FormCard.FormSwitchDelegate { + id: gesturePanelToggleDelegate + visible: !ShellSettings.Settings.navigationPanelEnabled + text: i18n("Show gesture handle") + description: i18n("Whether to add a panel on the bottom with a gesture handle.") + checked: ShellSettings.Settings.gesturePanelEnabled + onCheckedChanged: { + if (checked != ShellSettings.Settings.gesturePanelEnabled) { + ShellSettings.Settings.gesturePanelEnabled = checked; + } + } + } } } } diff --git a/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml b/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml index 1abaa633..9188e9df 100644 --- a/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml +++ b/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml @@ -482,31 +482,21 @@ FocusScope { // anchors.right: parent.right // } - // navigation panel - MobileShell.NavigationPanel { - id: navigationPanel - z: !root.taskSwitcherHelpers.currentlyBeingClosed ? 1 : 0 - visible: ShellSettings.Settings.navigationPanelEnabled - backgroundColor: Qt.rgba(0, 0, 0, 0.1) - foregroundColorGroup: Kirigami.Theme.Complementary - shadow: false + // Gesture panel + Component { + id: gesturePanelComponent - isVertical: MobileShell.Constants.navigationPanelOnSide(root.width, root.height) + MobileShell.GesturePanel { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom - MobileShellState.PanelSettingsDBusClient { - id: panelSettings - screenName: Screen.name - } + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + opaqueBar: false - leftPadding: panelSettings.navigationPanelLeftPadding - rightPadding: panelSettings.navigationPanelRightPadding - - leftAction: MobileShell.NavigationPanelAction { - enabled: true - iconSource: "mobile-task-switcher" - shrinkSize: 4 - - onTriggered: { + // Trigger home action on tap + onHandleClicked: { if (taskList.count === 0) { root.hide(); } else { @@ -522,27 +512,85 @@ FocusScope { } } } + } - // home button - middleAction: MobileShell.NavigationPanelAction { - enabled: true - iconSource: "start-here-kde" - onTriggered: root.hide() - } + // Navigation panel + Component { + id: navigationPanelComponent - // close app/keyboard button - rightAction: MobileShell.NavigationPanelAction { - enabled: true - iconSource: "mobile-close-app" - shrinkSize: 4 + MobileShell.NavigationPanel { + id: navigationPanel + backgroundColor: Qt.rgba(0, 0, 0, 0.1) + foregroundColorGroup: Kirigami.Theme.Complementary + shadow: false - onTriggered: { - taskList.getTaskAt(root.state.currentTaskIndex).closeApp(); + isVertical: MobileShell.Constants.navigationPanelOnSide(root.width, root.height) + + MobileShellState.PanelSettingsDBusClient { + id: panelSettings + screenName: Screen.name + } + + leftPadding: panelSettings.navigationPanelLeftPadding + rightPadding: panelSettings.navigationPanelRightPadding + + leftAction: MobileShell.NavigationPanelAction { + enabled: true + iconSource: "mobile-task-switcher" + shrinkSize: 4 + + onTriggered: { + if (taskList.count === 0) { + root.hide(); + } else { + if (taskList.count > 1 && + root.state.elapsedTimeSinceStart != -1 && + root.state.elapsedTimeSinceStart < root.state.doubleClickInterval) { + root.taskSwitcherHelpers.openApp(1); + return; + } + + const currentIndex = root.state.currentTaskIndex; + root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex); + } + } + } + + // home button + middleAction: MobileShell.NavigationPanelAction { + enabled: true + iconSource: "start-here-kde" + onTriggered: root.hide() + } + + // close app/keyboard button + rightAction: MobileShell.NavigationPanelAction { + enabled: true + iconSource: "mobile-close-app" + shrinkSize: 4 + + onTriggered: { + taskList.getTaskAt(root.state.currentTaskIndex).closeApp(); + } + } + + rightCornerAction: MobileShell.NavigationPanelAction { + visible: false } } + } - rightCornerAction: MobileShell.NavigationPanelAction { - visible: false + Loader { + id: panelLoader + z: !root.taskSwitcherHelpers.currentlyBeingClosed ? 1 : 0 + sourceComponent: { + if (ShellSettings.Settings.navigationPanelEnabled) { + return navigationPanelComponent; + } else if (ShellSettings.Settings.gesturePanelEnabled) { + return gesturePanelComponent; + } else { + return null; + } } } @@ -551,7 +599,7 @@ FocusScope { name: "landscape" when: MobileShell.Constants.navigationPanelOnSide(root.width, root.height) AnchorChanges { - target: navigationPanel + target: panelLoader anchors { right: root.right top: root.top @@ -560,7 +608,7 @@ FocusScope { } } PropertyChanges { - target: navigationPanel + target: panelLoader width: navRightMargin anchors.topMargin: root.topMargin } @@ -569,7 +617,7 @@ FocusScope { name: "portrait" when: !MobileShell.Constants.navigationPanelOnSide(root.width, root.height) AnchorChanges { - target: navigationPanel + target: panelLoader anchors { top: undefined right: root.right @@ -578,7 +626,7 @@ FocusScope { } } PropertyChanges { - target: navigationPanel + target: panelLoader height: navBottomMargin } } diff --git a/kwin/mobiletaskswitcher/package/contents/ui/main.qml b/kwin/mobiletaskswitcher/package/contents/ui/main.qml index 700ee471..fe773c6d 100644 --- a/kwin/mobiletaskswitcher/package/contents/ui/main.qml +++ b/kwin/mobiletaskswitcher/package/contents/ui/main.qml @@ -29,7 +29,10 @@ SceneEffect { TaskSwitcherPlugin.MobileTaskSwitcherState { id: taskSwitcherState - gestureEnabled: !ShellSettings.Settings.navigationPanelEnabled && !MobileShellState.ShellDBusClient.isActionDrawerOpen && !MobileShellState.ShellDBusClient.isVolumeOSDOpen && !MobileShellState.ShellDBusClient.isNotificationPopupDrawerOpen + gestureEnabled: !ShellSettings.Settings.navigationPanelEnabled + && !MobileShellState.ShellDBusClient.isActionDrawerOpen + && !MobileShellState.ShellDBusClient.isVolumeOSDOpen + && !MobileShellState.ShellDBusClient.isNotificationPopupDrawerOpen Component.onCompleted: { // Initialize with effect