homescreens/halcyon: Unify search widget with folio

This commit drops halcyon's search widget in favour of using the same
widget as folio to share code. This also brings over its keyboard
navigation features.
This commit is contained in:
Devin Lin 2025-07-10 13:28:54 -04:00
parent 0bcab0ae3a
commit d0b1df97a6
4 changed files with 149 additions and 330 deletions

View file

@ -1,324 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 Aaron Seigo <aseigo@kde.org>
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Effects
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.milou as Milou
import org.kde.kirigami 2.19 as Kirigami
/**
* 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
// content margins (background ignores this)
property real topMargin: 0
property real bottomMargin: 0
property real leftMargin: 0
property real rightMargin: 0
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();
}
// emitted when an item on the ListView is triggered
signal actionTriggered()
readonly property real closedContentY: Kirigami.Units.gridUnit * 5
readonly property real openedContentY: 0
readonly property real openFactor: Math.max(0, Math.min(1, 1 - flickable.contentY / closedContentY))
readonly property bool isOpen: openFactor != 0
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: root.openFactor
}
onOpacityChanged: {
if (opacity === 0) {
close();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Down) {
listView.forceActiveFocus();
}
}
Flickable {
id: flickable
anchors.fill: parent
anchors.topMargin: root.topMargin
anchors.bottomMargin: root.bottomMargin
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
contentHeight: flickable.height + root.closedContentY
contentY: root.closedContentY
property real oldContentY: contentY
property bool opening: false
onContentYChanged: {
opening = contentY < oldContentY;
oldContentY = contentY;
if (contentY !== root.openedContentY) {
queryField.focus = false;
}
}
onMovementEnded: root.endGesture()
onDraggingChanged: {
if (!dragging) {
root.endGesture();
}
}
NumberAnimation on contentY {
id: anim
duration: Kirigami.Units.longDuration * 2
easing.type: Easing.OutQuad
running: false
onFinished: {
if (anim.to === root.openedContentY) {
queryField.forceActiveFocus();
}
}
}
ColumnLayout {
id: column
height: flickable.height
width: flickable.width
Controls.Control {
opacity: root.openFactor
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.gridUnit
Layout.leftMargin: Kirigami.Units.gridUnit
Layout.rightMargin: Kirigami.Units.gridUnit
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
background: Item {
// shadow for search window
MultiEffect {
anchors.fill: parent
source: rectBackground
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.15
}
Rectangle {
id: rectBackground
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
}
}
contentItem: RowLayout {
Item {
implicitHeight: queryField.height
implicitWidth: height
Kirigami.Icon {
anchors.fill: parent
anchors.margins: Math.round(Kirigami.Units.smallSpacing)
source: "start-here-symbolic"
}
}
PlasmaComponents.TextField {
id: queryField
Layout.fillWidth: true
placeholderText: i18n("Search…")
inputMethodHints: Qt.ImhNoPredictiveText // don't need to press "enter" to update text
}
}
}
Controls.ScrollView {
opacity: root.openFactor === 1 ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration }
}
Layout.fillWidth: true
Layout.fillHeight: listView.contentHeight > availableHeight
Milou.ResultsListView {
id: listView
queryString: queryField.text
clip: true
Kirigami.Theme.colorSet: Kirigami.Theme.Window
highlight: activeFocus ? highlightComponent : null
Component {
id: highlightComponent
PlasmaExtras.Highlight {}
}
onActivated: {
root.close();
}
onUpdateQueryString: {
queryField.text = text
queryField.cursorPosition = cursorPosition
}
delegate: MouseArea {
id: delegate
height: rowLayout.height
width: listView.width
onClicked: {
listView.currentIndex = model.index;
listView.runCurrentIndex();
root.actionTriggered();
}
hoverEnabled: true
function activateNextAction() {
queryField.forceActiveFocus();
queryField.selectAll();
listView.currentIndex = -1;
}
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: Kirigami.Units.shortDuration }
}
}
RowLayout {
id: rowLayout
height: Kirigami.Units.gridUnit * 3
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.gridUnit
anchors.rightMargin: Kirigami.Units.gridUnit
Kirigami.Icon {
Layout.alignment: Qt.AlignVCenter
source: model.decoration
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
id: title
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
Layout.rightMargin: Kirigami.Units.gridUnit
maximumLineCount: 1
elide: Text.ElideRight
text: typeof modelData !== "undefined" ? modelData : model.display
color: "white"
font.pointSize: Kirigami.Theme.defaultFont.pointSize
}
PlasmaComponents.Label {
id: subtitle
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
Layout.rightMargin: Kirigami.Units.gridUnit
maximumLineCount: 1
elide: Text.ElideRight
text: model.subtext || ""
color: "white"
opacity: 0.8
font.pointSize: Math.round(Kirigami.Theme.defaultFont.pointSize * 0.8)
}
}
Repeater {
id: actionsRepeater
model: typeof actions !== "undefined" ? actions : []
Controls.ToolButton {
icon: 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)
}
}
}
}
}
}
MouseArea {
Layout.fillWidth: true
Layout.fillHeight: true
onClicked: close()
}
}
}
}

View file

@ -57,6 +57,13 @@ Item {
Plasmoid.editMode = false;
}
// Pass focus to swipe view child
onFocusChanged: {
if (focus) {
swipeView.focusChild();
}
}
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry

View file

@ -0,0 +1,136 @@
/*
* SPDX-FileCopyrightText: 2014 Aaron Seigo <aseigo@kde.org>
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2021-2025 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.kirigami as Kirigami
Item {
id: root
// Content margins
property real topPadding: 0
property real bottomPadding: 0
property real leftPadding: 0
property real rightPadding: 0
// Call when the gesture has started
function startGesture() {
krunnerScreen.clearField();
flickable.contentY = flickable.closedContentY;
}
// Call when the touch gesture has been updated
function updateGestureOffset(yOffset) {
flickable.contentY = Math.max(0, Math.min(flickable.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 = flickable.openedContentY;
anim.restart();
}
// Close the search widget (animated)
function close() {
anim.to = flickable.closedContentY;
anim.restart();
}
// Emitted when it is requested to force active focus on the parent and release focus on the widget
signal releaseFocusRequested()
readonly property real openFactor: Math.max(0, Math.min(1, 1 - flickable.contentY / flickable.closedContentY))
readonly property bool isOpen: openFactor != 0
// Pass focus to search screen
onFocusChanged: {
if (focus) {
krunnerScreen.requestFocus();
}
}
Rectangle {
id: background
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: root.openFactor
}
Flickable {
id: flickable
anchors.fill: parent
anchors.topMargin: root.topPadding
anchors.bottomMargin: root.bottomPadding
anchors.leftMargin: root.leftPadding
anchors.rightMargin: root.rightPadding
opacity: root.openFactor
contentHeight: flickable.height + flickable.closedContentY
contentY: flickable.closedContentY
property real oldContentY: contentY
property bool opening: false
// The y at which the flickable is fully open
readonly property real closedContentY: Kirigami.Units.gridUnit * 5
// The y at which the flickable is fully closed
readonly property real openedContentY: 0
onContentYChanged: {
opening = contentY < oldContentY;
oldContentY = contentY;
if (krunnerScreen.focus) {
// Unfocus from search
root.releaseFocusRequested();
}
}
onMovementEnded: root.endGesture()
onDraggingChanged: {
if (!dragging) {
root.endGesture();
}
}
NumberAnimation on contentY {
id: anim
duration: Kirigami.Units.longDuration * 2
easing.type: Easing.OutQuad
running: false
onFinished: {
if (anim.to === flickable.openedContentY) {
krunnerScreen.requestFocus();
} else {
// Unfocus from search
root.releaseFocusRequested();
}
}
}
MobileShell.KRunnerScreen {
id: krunnerScreen
width: parent.width
height: parent.height
onRequestedClose: root.close();
}
}
}

View file

@ -136,17 +136,17 @@ ContainmentItem {
}
// search component
MobileShell.KRunnerWidget {
SearchWidget {
id: search
anchors.fill: parent
visible: openFactor > 0
onActionTriggered: search.close()
topPadding: homeScreen.topMargin
bottomPadding: homeScreen.bottomMargin
leftPadding: homeScreen.leftMargin
rightPadding: homeScreen.rightMargin
topMargin: homeScreen.topMargin
bottomMargin: homeScreen.bottomMargin
leftMargin: homeScreen.leftMargin
rightMargin: homeScreen.rightMargin
onReleaseFocusRequested: halcyonHomeScreen.forceActiveFocus()
}
}
}