From 0bcab0ae3af2dcc07335d604ac752161d3bcfc11 Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Thu, 10 Jul 2025 12:54:01 -0400 Subject: [PATCH] homescreens/folio: Add keyboard navigation to app drawer and fix on search This commit is a subset of https://invent.kde.org/plasma/plasma-mobile/-/merge_requests/694 to add keyboard navigation to the app drawer. Pressing the arrow keys while in the app drawer will allow you to navigate between the apps and the search bar, and going up will exit the app drawer. Escape/Back is also supported for unfocusing the search bar and exiting the view. This also fixes an issue in the keyboard navigation on the search screen where it wouldn't close when there are no search results. --- .../qml/widgets/krunner/KRunnerScreen.qml | 16 ++++++- .../folio/package/contents/ui/AppDrawer.qml | 44 +++++++++++++++++-- .../package/contents/ui/AppDrawerGrid.qml | 11 +++++ .../package/contents/ui/AppDrawerHeader.qml | 21 +++++++++ .../folio/package/contents/ui/HomeScreen.qml | 25 ++++++++++- .../contents/ui/delegate/AbstractDelegate.qml | 31 +++++++++++-- .../ui/delegate/KeyboardHighlight.qml | 18 ++++++++ 7 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/KeyboardHighlight.qml diff --git a/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml b/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml index 86ad71bc..7420d590 100644 --- a/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml +++ b/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml @@ -72,7 +72,21 @@ MouseArea { font.weight: Font.Bold - KeyNavigation.down: listView + // Keyboard navigation + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Down) { + if (listView.count === 0) { + // Close if listview has no elements + root.requestedClose(); + } else { + // Focus on listview if there are elements + listView.forceActiveFocus(); + listView.currentIndex = 0; + } + event.accepted = true; + } + } + } QQC2.ScrollView { diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawer.qml b/containments/homescreens/folio/package/contents/ui/AppDrawer.qml index d479b505..0f016142 100644 --- a/containments/homescreens/folio/package/contents/ui/AppDrawer.qml +++ b/containments/homescreens/folio/package/contents/ui/AppDrawer.qml @@ -28,11 +28,21 @@ Item { required property int headerHeight required property var headerItem - // height from top of screen that the drawer starts + // Height from top of screen that the drawer starts readonly property real drawerTopMargin: height - topPadding - bottomPadding property alias flickable: appDrawerGrid + // Keyboard navigation + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) { + // Close drawer if "back" action + folio.HomeScreenState.closeAppDrawer(); + event.accepted = true; + } + } + + // App drawer container Item { anchors.fill: parent @@ -41,7 +51,7 @@ Item { anchors.rightMargin: root.rightPadding anchors.bottomMargin: root.bottomPadding - // drawer header + // Drawer header MobileShell.BaseItem { id: drawerHeader z: 1 @@ -52,8 +62,23 @@ Item { anchors.right: parent.right contentItem: root.headerItem + + // Keyboard navigation for header (search bar) + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) { + // Go from search bar to app grid + appDrawerGrid.forceActiveFocus(); + appDrawerGrid.currentIndex = 0; + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + // Go to homescreen pages + folio.HomeScreenState.closeAppDrawer(); + event.accepted = true; + } + } } + // App list AppDrawerGrid { id: appDrawerGrid folio: root.folio @@ -64,9 +89,22 @@ Item { anchors.bottom: parent.bottom opacity: 0 // we display with the opacity gradient below headerHeight: root.headerHeight + + // Keyboard navigation + topEdgeCallback: () => { + drawerHeader.contentItem.forceActiveFocus(); + currentIndex = -1; + } + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) { + topEdgeCallback(); + event.accepted = true; + } + } } - // opacity gradient at grid edges + // Opacity gradient at grid edges MobileShell.FlickableOpacityGradient { anchors.fill: appDrawerGrid flickable: appDrawerGrid diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml b/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml index d526222b..54632b5d 100644 --- a/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml +++ b/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml @@ -23,6 +23,10 @@ MobileShell.GridView { reuseItems: true layer.enabled: true + keyNavigationEnabled: true + highlightMoveDuration: 0 + highlight: null // We supply our own highlight from the delegate + property var homeScreen property real headerHeight @@ -84,6 +88,13 @@ MobileShell.GridView { model: folio.ApplicationListSearchModel + // Keyboard focus on app delegate when it is the selected item + onCurrentItemChanged: { + if (currentItem) { + currentItem.keyboardFocus(); + } + } + delegate: AppDelegate { id: appDelegate diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml b/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml index abf9f707..80921fd1 100644 --- a/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml +++ b/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml @@ -18,10 +18,31 @@ Item { Kirigami.Theme.colorSet: Kirigami.Theme.Complementary Kirigami.Theme.inherit: false + function addSearchText(text: string) { + searchField.text += text; + } + function clearSearchText(): void { searchField.text = ''; } + // Request to not focus on the search bar + signal releaseFocusRequested() + + onFocusChanged: { + if (focus) { + searchField.focus = true; + } + } + + // Keyboard navigation + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) { + root.releaseFocusRequested(); + event.accepted = true; + } + } + RowLayout { anchors.topMargin: Kirigami.Units.largeSpacing anchors.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreen.qml b/containments/homescreens/folio/package/contents/ui/HomeScreen.qml index 490d79f4..3bddeeef 100644 --- a/containments/homescreens/folio/package/contents/ui/HomeScreen.qml +++ b/containments/homescreens/folio/package/contents/ui/HomeScreen.qml @@ -437,17 +437,38 @@ Item { y: (opacity > 0) ? animationY : parent.height headerHeight: Math.round(Kirigami.Units.gridUnit * 4) - headerItem: AppDrawerHeader { folio: root.folio } + headerItem: AppDrawerHeader { + id: appDrawerHeader + folio: root.folio - // account for panels + onReleaseFocusRequested: appDrawer.forceActiveFocus() + } + + // Account for panels topPadding: root.topMargin bottomPadding: root.bottomMargin leftPadding: root.leftMargin rightPadding: root.rightMargin + // Forward keyboard text to the search bar + Keys.onPressed: (event) => { + if (event.text.trim().length > 0) { + appDrawerHeader.addSearchText(event.text); + appDrawerHeader.forceActiveFocus(); + event.accepted = true; + } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) { + appDrawerHeader.forceActiveFocus(); + event.accepted = true; + } + } + Connections { target: folio.HomeScreenState + function onAppDrawerOpened() { + appDrawer.forceActiveFocus(); + } + function onAppDrawerClosed() { // reset app drawer position when closed appDrawer.flickable.contentY = 0; diff --git a/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml index 593499f4..27fecab5 100644 --- a/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml +++ b/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml @@ -33,6 +33,10 @@ Folio.DelegateTouchArea { property real scaleAmount: 1 property bool clickRequested: false + function keyboardFocus() { + delegateWrapper.forceActiveFocus(); + } + NumberAnimation on scaleAmount { id: shrinkAnim running: false @@ -70,17 +74,36 @@ Folio.DelegateTouchArea { // trigger handled by press animation onClicked: clickRequested = true; - layer.enabled: root.shadow - layer.effect: DelegateShadow {} - - Item { + FocusScope { id: delegateWrapper anchors.fill: parent + // Select keyboard navigation + Keys.onPressed: (event) => { + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Space: + root.afterClickAnimation(); + event.accepted = true; + break; + default: + break; + } + } + + KeyboardHighlight { + anchors.fill: parent + visible: delegateWrapper.activeFocus + } + ColumnLayout { anchors.fill: parent spacing: 0 + layer.enabled: root.shadow + layer.effect: DelegateShadow {} + // transform is not on delegateWrapper because when it's zoomed in, it apparently // affects the delegate's x and y position, which messes up the starting drag and drop // position (for mapFromItem in HomeScreen.qml) diff --git a/containments/homescreens/folio/package/contents/ui/delegate/KeyboardHighlight.qml b/containments/homescreens/folio/package/contents/ui/delegate/KeyboardHighlight.qml new file mode 100644 index 00000000..2e0ebb6f --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/KeyboardHighlight.qml @@ -0,0 +1,18 @@ + +// SPDX-FileCopyrightText: 2025 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import org.kde.ksvg as KSvg +import org.kde.kirigami as Kirigami + +Rectangle { + id: background + radius: Kirigami.Units.cornerRadius + + border.width: 1 + border.color: Kirigami.Theme.highlightColor + border.pixelAligned: false + + color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.2) +}