pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import QtQuick.Controls import qs.Components import qs.Config import "../../scripts/fuzzysort.js" as Fuzzy import "./SettingsIndex.mjs" as SettingsIndex Item { id: root property alias text: searchField.text signal settingSelected(string category, string section, string settingName) function close() { searchField.text = ""; searchField.focus = false; popup.close(); } function search(query) { resultsModel.clear(); if (!query || query.trim() === "") { popup.close(); return; } const results = SettingsIndex.searchSettings(query, Fuzzy); for (const result of results.slice(0, 10)) { resultsModel.append({ name: result.name, category: result.category, categoryName: result.categoryName, section: result.section, matchType: result.matchType }); } if (resultsModel.count > 0) { popup.open(); } else { popup.close(); } } implicitHeight: searchContainer.implicitHeight implicitWidth: 200 ListModel { id: resultsModel } CustomRect { id: searchContainer anchors.fill: parent color: DynamicColors.tPalette.m3surfaceContainerHigh implicitHeight: searchRow.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full RowLayout { id: searchRow anchors.fill: parent anchors.leftMargin: Appearance.padding.normal anchors.rightMargin: Appearance.padding.small spacing: Appearance.spacing.small MaterialIcon { Layout.alignment: Qt.AlignVCenter color: DynamicColors.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.larger text: "search" } CustomTextField { id: searchField Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true font.pointSize: Appearance.font.size.small placeholderText: qsTr("Search settings...") Keys.onDownPressed: { if (popup.visible && resultsList.count > 0) { resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1); } } Keys.onEscapePressed: { root.close(); } Keys.onReturnPressed: { if (popup.visible && resultsList.currentIndex >= 0) { const item = resultsModel.get(resultsList.currentIndex); root.settingSelected(item.category, item.section, item.name); root.close(); } } Keys.onUpPressed: { if (popup.visible && resultsList.count > 0) { resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0); } } onTextChanged: { searchTimer.restart(); } } // Clear button IconButton { Layout.alignment: Qt.AlignVCenter font.pointSize: Appearance.font.size.larger icon: "close" opacity: searchField.text.length > 0 ? 1 : 0 Behavior on opacity { Anim { duration: Appearance.anim.durations.small } } onClicked: { root.close(); } } } } // Debounce timer for search Timer { id: searchTimer interval: 150 onTriggered: root.search(searchField.text) } // Results dropdown Popup { id: popup closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside implicitHeight: Math.min(resultsList.contentHeight + Appearance.padding.small * 2 + 10, (contentItem.view.delegate.implicitHeight + Appearance.spacing.smaller) * 5 - Appearance.spacing.smaller + 10) implicitWidth: root.width + Appearance.padding.small * 2 modal: false padding: 0 x: root.width / 2 - popup.width / 2 y: searchContainer.height + Appearance.spacing.small background: Item { } contentItem: Item { property alias view: resultsList Elevation { id: popupShadow anchors.centerIn: parent height: popup.implicitHeight - 10 level: 2 radius: Appearance.rounding.normal width: popup.implicitWidth - 10 CustomRect { anchors.fill: parent color: DynamicColors.palette.m3surfaceContainer radius: Appearance.rounding.normal CustomListView { id: resultsList anchors.fill: parent anchors.margins: Appearance.padding.small clip: true currentIndex: 0 highlightFollowsCurrentItem: false highlightRangeMode: ListView.ApplyRange model: resultsModel preferredHighlightBegin: 0 preferredHighlightEnd: height spacing: Appearance.spacing.smaller delegate: SearchResultItem { required property string category required property string categoryName required property int index required property string matchType required property string name required property string section highlighted: index === resultsList.currentIndex width: resultsList.width onClicked: { root.settingSelected(category, section, name); root.close(); } } highlight: CustomRect { color: DynamicColors.palette.m3primary implicitHeight: resultsList.currentItem?.implicitHeight ?? 0 implicitWidth: resultsList.width radius: Appearance.rounding.normal - Appearance.padding.smaller y: resultsList.currentItem?.y ?? 0 Behavior on y { Anim { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.expressiveEffects } } } } } } } enter: Transition { Anim { duration: Appearance.anim.durations.small from: 0 property: "opacity" to: 1 } Anim { duration: Appearance.anim.durations.small from: 0.95 property: "scale" to: 1.0 } } exit: Transition { Anim { duration: Appearance.anim.durations.smaller from: 1 property: "opacity" to: 0 } } } // Search result item component component SearchResultItem: CustomRect { id: resultItem property bool highlighted: false signal clicked implicitHeight: resultLayout.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.small ColumnLayout { id: resultLayout anchors.fill: parent anchors.margins: Appearance.padding.small spacing: 2 // Setting name CustomText { Layout.fillWidth: true color: highlighted ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface elide: Text.ElideRight font.pointSize: Appearance.font.size.small font.weight: Font.Medium text: resultItem.name } // Category and section path RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller CustomText { color: highlighted ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.smaller opacity: 0.8 text: resultItem.categoryName } CustomText { color: highlighted ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.smaller opacity: 0.6 text: "›" } CustomText { Layout.fillWidth: true color: highlighted ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurfaceVariant elide: Text.ElideRight font.pointSize: Appearance.font.size.smaller opacity: 0.8 text: resultItem.section } } } StateLayer { onClicked: resultItem.clicked() } } }