shift-shell/kcms/navigation/ui/main.qml
Devin Lin 28f7224a5b 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.
2025-12-14 18:36:42 -05:00

259 lines
10 KiB
QML

/*
* SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2025 Luis Büchi <luis.buechi@kdemail.net>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as Controls
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kcmutils as KCM
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
KCM.SimpleKCM {
id: root
title: i18n("Navigation")
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
ColumnLayout {
FormCard.FormHeader {
title: i18nc("@label", "Navigation Mode")
}
FormCard.FormCard {
id: gestureSelectionCard
Controls.ButtonGroup {
id: positionGroup
buttons: [navPanelRadio, gesturesRadio]
}
FormCard.FormRadioDelegate {
id: navPanelRadio
text: i18nc("Nav Panel Navigation Mode", "Panel")
checked: ShellSettings.Settings.navigationPanelEnabled
onClicked: ShellSettings.Settings.navigationPanelEnabled = true;
}
FormCard.FormRadioDelegate {
id: gesturesRadio
text: i18nc("Gestures Navigation Mode", "Gestures")
checked: !ShellSettings.Settings.navigationPanelEnabled
onClicked: ShellSettings.Settings.navigationPanelEnabled = false;
}
FormCard.FormDelegateSeparator {
visible: !ShellSettings.Settings.navigationPanelEnabled
above: gesturesRadio
below: tutorialContainer
}
Item {
id: tutorialContainer
visible: !ShellSettings.Settings.navigationPanelEnabled
property int phoneWidth: Math.min(Window.width * 0.4, gestureSelectionCard.maximumWidth * 0.9)
property int phoneHeight: phoneWidth * Window.height / Window.width
property int count: 3
property int fingerSize: 20
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: phoneWidth
Layout.preferredHeight: phoneHeight + fingerSize / 2
Layout.topMargin: Kirigami.Units.largeSpacing * 2
clip: true
MouseArea {
id: tutorialSwitcherInput
anchors.fill: tutorialContainer
preventStealing: true
property int activeTutorialIndex: 0
property real lastX
property real lastDelta
onPressedChanged: {
if (pressed) {
lastX = mouseX;
}
else {
let moveOffset = lastDelta > 0 ? -0.45 : 0.45
let candidateIndex = Math.round(-tutorialLayout.offset / (tutorialLayout.spacing + tutorialContainer.phoneWidth) + moveOffset);
activeTutorialIndex = Math.max(0, Math.min(tutorialContainer.count - 1, candidateIndex));
updateActiveTutorial();
releaseAnim.start()
}
}
function updateActiveTutorial(): void {
switch (activeTutorialIndex) {
case 0:
switchTutorial.loopSwitcherAnimation();
flickTutorial.stopAnimation();
scrubTutorial.stopAnimation();
break;
case 1:
switchTutorial.stopAnimation();
flickTutorial.loopFlickAnimation();
scrubTutorial.stopAnimation();
break;
case 2:
switchTutorial.stopAnimation();
flickTutorial.stopAnimation();
scrubTutorial.loopScrubAnimation();
break;
}
}
onPositionChanged: (mouse) => {
let delta = mouse.x - lastX;
let endOffset = -(tutorialContainer.count - 1) * (tutorialContainer.phoneWidth + tutorialLayout.spacing);
if (activeTutorialIndex == 0 && tutorialLayout.offset > 0 && delta > 0) {
// handle overshoot on the left
delta /= tutorialLayout.x;
}
else if (activeTutorialIndex == tutorialContainer.count - 1 && tutorialLayout.offset < endOffset) {
// handle overhoot on the right
delta /= Math.abs(tutorialLayout.x - endOffset);
}
tutorialLayout.offset += delta
lastDelta = delta;
lastX = mouse.x
}
NumberAnimation {
id: releaseAnim
target: tutorialLayout
property: "offset"
to: -tutorialSwitcherInput.activeTutorialIndex * (tutorialContainer.phoneWidth + tutorialLayout.spacing)
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
Item {
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: tutorialContainer.phoneWidth
Layout.preferredHeight: tutorialContainer.phoneHeight + tutorialContainer.fingerSize / 2
RowLayout {
id: tutorialLayout
property int offset: 0
x: offset
width: tutorialContainer.phoneWidth * tutorialContainer.count + Kirigami.Units.largeSpacing * (tutorialContainer.count - 1)
height: tutorialContainer.phoneHeight
spacing: Kirigami.Units.largeSpacing
TutorialPhone {
id: switchTutorial
phoneWidth: tutorialContainer.phoneWidth
phoneHeight: tutorialContainer.phoneHeight
fingerSize: tutorialContainer.fingerSize
showBackground: false
Layout.alignment: Qt.AlignCenter
Component.onCompleted: {
loopSwitcherAnimation();
}
}
TutorialPhone {
id: flickTutorial
phoneWidth: tutorialContainer.phoneWidth
phoneHeight: tutorialContainer.phoneHeight
fingerSize: tutorialContainer.fingerSize
Layout.alignment: Qt.AlignCenter
}
TutorialPhone {
id: scrubTutorial
phoneWidth: tutorialContainer.phoneWidth
phoneHeight: tutorialContainer.phoneHeight
fingerSize: tutorialContainer.fingerSize
showBackground: false
Layout.alignment: Qt.AlignCenter
}
}
}
}
Controls.PageIndicator {
visible: !ShellSettings.Settings.navigationPanelEnabled
currentIndex: tutorialSwitcherInput.activeTutorialIndex
count: tutorialContainer.count
Layout.alignment: Qt.AlignCenter
}
FormCard.FormSectionText {
visible: !ShellSettings.Settings.navigationPanelEnabled && tutorialSwitcherInput.activeTutorialIndex == 0
text: i18n("Swipe up, hold then release to enter Task Switcher")
}
FormCard.FormSectionText {
visible: !ShellSettings.Settings.navigationPanelEnabled && tutorialSwitcherInput.activeTutorialIndex == 1
text: i18n("Swipe up and release to go to Home Screen")
}
FormCard.FormSectionText {
visible: !ShellSettings.Settings.navigationPanelEnabled && tutorialSwitcherInput.activeTutorialIndex == 2
text: i18n("Swipe left and right near the bottom screen edge to scrub through open tasks. Release to select a task to focus")
}
FormCard.FormDelegateSeparator { visible: keyboardToggleDelegate.visible; above: gesturesRadio; below: keyboardToggleDelegate }
FormCard.FormSwitchDelegate {
id: keyboardToggleDelegate
visible: ShellSettings.Settings.navigationPanelEnabled
text: i18n("Always show keyboard toggle")
description: i18n("Whether to always show the keyboard toggle button on the navigation panel.")
checked: ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel
onCheckedChanged: {
if (checked != ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel) {
ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel = checked;
}
}
}
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;
}
}
}
}
}
}