mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
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.
This commit is contained in:
parent
be90b3f74b
commit
28f7224a5b
9 changed files with 248 additions and 62 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
48
components/mobileshell/qml/navigationpanel/GesturePanel.qml
Normal file
48
components/mobileshell/qml/navigationpanel/GesturePanel.qml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue