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