From f22a1e0b8cf456476c9edcf1da443dc36202722c Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Mon, 3 Jan 2022 00:25:33 -0500 Subject: [PATCH] homescreen: Add search widget --- .../qml/HomeScreenState.qml | 21 +- components/mobileshell/qml/qmldir | 5 +- .../qml/widgets/krunner/KRunnerWidget.qml | 271 ++++++++++++++++++ .../MediaControlsWidget.qml | 1 - .../NotificationsWidget.qml | 2 - .../homescreen/package/contents/ui/main.qml | 40 ++- 6 files changed, 322 insertions(+), 18 deletions(-) create mode 100644 components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml rename components/mobileshell/qml/widgets/{ => mediacontrols}/MediaControlsWidget.qml (99%) rename components/mobileshell/qml/widgets/{ => notifications}/NotificationsWidget.qml (99%) diff --git a/components/mobilehomescreencomponents/qml/HomeScreenState.qml b/components/mobilehomescreencomponents/qml/HomeScreenState.qml index ae94c88d..7a2c779c 100644 --- a/components/mobilehomescreencomponents/qml/HomeScreenState.qml +++ b/components/mobilehomescreencomponents/qml/HomeScreenState.qml @@ -12,6 +12,8 @@ import org.kde.plasma.private.mobileshell 1.0 as MobileShell /** * State object for the homescreen. + * + * We expose the data necessary to make custom "swipe-down" gestures from the page view. */ QtObject { id: root @@ -87,7 +89,7 @@ QtObject { SwipingPages, // horizontal movement between pages SwipingAppDrawerVisibility, // opening/closing app drawer SwipingAppDrawerList, // scrolling app drawer - SwipingActionPanel, // pulling down action panel + SwipingPagesDown, // custom gesture can be implemented for swiping down on the page view DeterminingType } @@ -132,6 +134,11 @@ QtObject { xAnim.stop(); } + // expose signals necessary to implement any behaviour for the "swipe-down" action on the page view + signal swipeDownGestureBegin + signal swipeDownGestureEnd + signal swipeDownGestureOffset(real value) + // be very careful when resetting the swipe state // ensure that we aren't in the middle of a gesture function resetSwipeState() { @@ -191,8 +198,8 @@ QtObject { xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } else if (yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { - currentSwipeState = HomeScreenState.SwipingActionPanel; - MobileShell.TopPanelControls.startSwipe(); + currentSwipeState = HomeScreenState.SwipingPagesDown; + root.swipeDownGestureBegin(); xDetermineSwipePosition = 0; yDetermineSwipePosition = 0; } else if (-yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { @@ -206,10 +213,10 @@ QtObject { xPosition += x; break; - case HomeScreenState.SwipingActionPanel: + case HomeScreenState.SwipingPagesDown: yPosition = pagesYPosition; if (y !== 0) { - MobileShell.TopPanelControls.requestRelativeScroll(y); + root.swipeDownGestureOffset(y); } break; @@ -277,8 +284,8 @@ QtObject { break; } - case HomeScreenState.SwipingActionPanel: { - MobileShell.TopPanelControls.endSwipe(); + case HomeScreenState.SwipingPagesDown: { + root.swipeDownGestureEnd(); root.resetSwipeState(); break; } diff --git a/components/mobileshell/qml/qmldir b/components/mobileshell/qml/qmldir index ac86946f..1d36cdd1 100644 --- a/components/mobileshell/qml/qmldir +++ b/components/mobileshell/qml/qmldir @@ -34,8 +34,9 @@ StatusBar 1.0 statusbar/StatusBar.qml TaskSwitcher 1.0 taskswitcher/TaskSwitcher.qml # /widgets -MediaControlsWidget 1.0 widgets/MediaControlsWidget.qml -NotificationsWidget 1.0 widgets/NotificationsWidget.qml +KRunnerWidget 1.0 widgets/krunner/KRunnerWidget.qml +MediaControlsWidget 1.0 widgets/mediacontrols/MediaControlsWidget.qml +NotificationsWidget 1.0 widgets/notifications/NotificationsWidget.qml # / singleton HomeScreenControls 1.0 HomeScreenControls.qml diff --git a/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml b/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml new file mode 100644 index 00000000..2dcc9a62 --- /dev/null +++ b/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml @@ -0,0 +1,271 @@ +/* + * SPDX-FileCopyrightText: 2014 Aaron Seigo + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import org.kde.milou 0.1 as Milou +import org.kde.kirigami 2.19 as Kirigami + +import "../../components" as Components + +/** + * Search widget that is embedded into the homescreen. The dimensions of + * the root item is assumed to be the available screen area for applications. + */ +Item { + id: root + + function startGesture() { + queryField.text = ""; + flickable.contentY = closedContentY; + } + + function updateGestureOffset(yOffset) { + flickable.contentY = Math.max(0, Math.min(closedContentY, flickable.contentY + yOffset)); + } + + // call when the touch gesture has let go + function endGesture() { + flickable.opening ? open() : close(); + } + + // open the search widget (animated) + function open() { + anim.to = openedContentY; + anim.restart(); + } + + // close the search widget (animated) + function close() { + anim.to = closedContentY; + anim.restart(); + } + + readonly property real closedContentY: PlasmaCore.Units.gridUnit * 5 + readonly property real openedContentY: 0 + readonly property real openFactor: Math.max(0, Math.min(1, 1 - flickable.contentY / closedContentY)) + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.3) + opacity: root.openFactor + } + + Flickable { + id: flickable + + anchors.fill: parent + anchors.topMargin: MobileShell.TaskPanelControls.isPortrait ? MobileShell.TaskPanelControls.panelHeight : 0 + anchors.rightMargin: MobileShell.TaskPanelControls.isPortrait ? 0 : MobileShell.TaskPanelControls.panelWidth + anchors.bottomMargin: MobileShell.TaskPanelControls.panelHeight + + contentHeight: flickable.height + root.closedContentY + 999999 + contentY: root.closedContentY + property real oldContentY: contentY + property bool opening: false + + onContentYChanged: { + opening = contentY < oldContentY; + oldContentY = contentY; + } + + onMovementEnded: root.endGesture() + + onDraggingChanged: { + if (!dragging) { + root.endGesture(); + } + } + + NumberAnimation on contentY { + id: anim + duration: PlasmaCore.Units.longDuration * 2 + easing.type: Easing.OutQuad + onFinished: { + if (root.openedContentY) { + queryField.forceActiveFocus(); + } + } + } + + ColumnLayout { + id: column + height: flickable.height + width: flickable.width + + Controls.Control { + opacity: root.openFactor + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.gridUnit + Layout.rightMargin: PlasmaCore.Units.gridUnit + + leftPadding: PlasmaCore.Units.smallSpacing / 2 + rightPadding: PlasmaCore.Units.smallSpacing / 2 + topPadding: PlasmaCore.Units.smallSpacing / 2 + bottomPadding: PlasmaCore.Units.smallSpacing / 2 + + background: Item { + + // shadow for search window + RectangularGlow { + anchors.topMargin: 1 + anchors.fill: parent + cached: true + glowRadius: 4 + spread: 0.2 + color: Qt.rgba(0, 0, 0, 0.15) + } + + Rectangle { + anchors.fill: parent + color: PlasmaCore.Theme.backgroundColor + radius: PlasmaCore.Units.smallSpacing + } + } + + contentItem: RowLayout { + Item { + implicitHeight: queryField.height + implicitWidth: height + Kirigami.Icon { + anchors.fill: parent + anchors.margins: Math.round(Kirigami.Units.smallSpacing * 1.5) + source: "start-here-symbolic" + } + } + Kirigami.SearchField { + id: queryField + focus: true + Layout.fillWidth: true + } + } + } + + Controls.ScrollView { + opacity: root.openFactor === 1 ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: PlasmaCore.Units.shortDuration } + } + + Layout.fillHeight: true + Layout.fillWidth: true + + Milou.ResultsListView { + id: listView + queryString: queryField.text + highlight: null + PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.NormalColorGroup + + onActivated: queryField.text = ""; + onUpdateQueryString: { + queryField.text = text + queryField.cursorPosition = cursorPosition + } + + delegate: MouseArea { + id: delegate + height: rowLayout.height + width: listView.width + + onClicked: { + listView.currentIndex = model.index; + listView.runCurrentIndex(); + } + hoverEnabled: true + + Rectangle { + anchors.fill: parent + color: delegate.pressed ? Qt.rgba(255, 255, 255, 0.2) : (delegate.containsMouse ? Qt.rgba(255, 255, 255, 0.05) : "transparent") + Behavior on color { + ColorAnimation { duration: PlasmaCore.Units.shortDuration } + } + } + + RowLayout { + id: rowLayout + height: PlasmaCore.Units.gridUnit * 3 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: PlasmaCore.Units.largeSpacing + anchors.rightMargin: PlasmaCore.Units.largeSpacing + + Kirigami.Icon { + Layout.alignment: Qt.AlignVCenter + source: model.decoration + implicitWidth: PlasmaCore.Units.iconSizes.medium + implicitHeight: PlasmaCore.Units.iconSizes.medium + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: PlasmaCore.Units.smallSpacing + + PlasmaComponents.Label { + id: title + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2 + Layout.rightMargin: PlasmaCore.Units.largeSpacing + + maximumLineCount: 1 + elide: Text.ElideRight + text: typeof modelData !== "undefined" ? modelData : model.display + color: "white" + + font.pointSize: PlasmaCore.Theme.defaultFont.pointSize + } + PlasmaComponents.Label { + id: subtitle + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2 + Layout.rightMargin: PlasmaCore.Units.largeSpacing + + maximumLineCount: 1 + elide: Text.ElideRight + text: model.subtext || "" + color: "white" + opacity: 0.8 + + font.pointSize: Math.round(PlasmaCore.Theme.defaultFont.pointSize * 0.8) + } + } + + Repeater { + id: actionsRepeater + model: typeof actions !== "undefined" ? actions : [] + + Controls.ToolButton { + icon.name: modelData.icon || "" + visible: modelData.visible || true + enabled: modelData.enabled || true + + Accessible.role: Accessible.Button + Accessible.name: modelData.text + checkable: checked + checked: delegate.activeAction === index + focus: delegate.activeAction === index + onClicked: delegate.ListView.view.runAction(index) + } + } + } + } + } + } + } + } +} diff --git a/components/mobileshell/qml/widgets/MediaControlsWidget.qml b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml similarity index 99% rename from components/mobileshell/qml/widgets/MediaControlsWidget.qml rename to components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml index 73f4fd61..ded27c73 100644 --- a/components/mobileshell/qml/widgets/MediaControlsWidget.qml +++ b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml @@ -16,7 +16,6 @@ import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras import "../components" as Components -import "mediacontrols" Components.BaseItem { id: root diff --git a/components/mobileshell/qml/widgets/NotificationsWidget.qml b/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml similarity index 99% rename from components/mobileshell/qml/widgets/NotificationsWidget.qml rename to components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml index fc58c42d..079771fd 100644 --- a/components/mobileshell/qml/widgets/NotificationsWidget.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml @@ -19,8 +19,6 @@ import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.notificationmanager 1.0 as NotificationManager -import "notifications" - /** * Embeddable notification list widget optimized for mobile and touch. * Used on the lockscreen and action drawer. diff --git a/containments/homescreen/package/contents/ui/main.qml b/containments/homescreen/package/contents/ui/main.qml index 66540a4c..24bdc2bc 100644 --- a/containments/homescreen/package/contents/ui/main.qml +++ b/containments/homescreen/package/contents/ui/main.qml @@ -32,9 +32,10 @@ FocusScope { } function triggerHomeScreen() { - taskSwitcher.minimizeAll(); MobileShell.HomeScreenControls.resetHomeScreenPosition(); taskSwitcher.visible = false; // will trigger homescreen open + searchWidget.close(); + taskSwitcher.minimizeAll(); } //END functions @@ -126,18 +127,45 @@ FocusScope { } } + // control the opacity of both the search and homescreen components + property real homeScreenOpacity: 1 + NumberAnimation on homeScreenOpacity { + id: opacityAnimation + duration: PlasmaCore.Units.longDuration + } + // homescreen component HomeScreenComponents.HomeScreen { id: homescreen anchors.fill: parent + opacity: root.homeScreenOpacity * (1 - searchWidget.openFactor) // make the homescreen not interactable when task switcher or startup feedback is on interactive: !taskSwitcher.visible && !startupFeedback.visible - opacity: 1 - NumberAnimation on opacity { - id: opacityAnimation - duration: PlasmaCore.Units.longDuration + } + + // search component + MobileShell.KRunnerWidget { + id: searchWidget + anchors.fill: parent + + opacity: root.homeScreenOpacity + visible: openFactor > 0 + onOpenFactorChanged: homescreen.opacity = 1 - openFactor; + } + + Connections { + target: homescreen.homeScreenState + + function onSwipeDownGestureBegin() { + searchWidget.startGesture(); + } + function onSwipeDownGestureEnd() { + searchWidget.endGesture(); + } + function onSwipeDownGestureOffset(offset) { + searchWidget.updateGestureOffset(-offset); } } @@ -177,7 +205,7 @@ FocusScope { opacityAnimation.to = 0; opacityAnimation.restart(); } else { - homescreen.opacity = 0; + root.homeScreenOpacity = 0; } } else {