import QtQuick import qs.Common import qs.Widgets Item { id: root activeFocusOnTab: true KeyNavigation.tab: keyNavigationTab KeyNavigation.backtab: keyNavigationBacktab onActiveFocusChanged: { if (activeFocus) { locationInput.forceActiveFocus(); } } property string currentLocation: "" property string placeholderText: I18n.tr("Search for a location...") property bool _internalChange: false property bool isLoading: false property string currentSearchText: "" property Item keyNavigationTab: null property Item keyNavigationBacktab: null signal locationSelected(string displayName, string coordinates) function resetSearchState() { locationSearchTimer.stop(); dropdownHideTimer.stop(); isLoading = false; searchResultsModel.clear(); } width: parent.width height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0) ListModel { id: searchResultsModel } Timer { id: locationSearchTimer interval: 500 running: false repeat: false onTriggered: { if (locationInput.text.length > 2) { searchResultsModel.clear(); root.isLoading = true; const searchLocation = locationInput.text; root.currentSearchText = searchLocation; const encodedLocation = encodeURIComponent(searchLocation); const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1"; Proc.runCommand("locationSearch", ["dms", "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => { root.isLoading = false; if (exitCode !== 0) { searchResultsModel.clear(); return; } if (root.currentSearchText !== locationInput.text) return; const raw = output.trim(); searchResultsModel.clear(); if (!raw || raw[0] !== "[") { return; } try { const data = JSON.parse(raw); if (data.length === 0) { return; } for (var i = 0; i < Math.min(data.length, 5); i++) { const location = data[i]; if (location.display_name && location.lat && location.lon) { const parts = location.display_name.split(', '); let cleanName = parts[0]; if (parts.length > 1) { const state = parts[parts.length - 2]; if (state && state !== cleanName) cleanName += `, ${state}`; } const query = `${location.lat},${location.lon}`; searchResultsModel.append({ "name": cleanName, "query": query }); } } } catch (e) {} }); } } } Timer { id: dropdownHideTimer interval: 200 running: false repeat: false onTriggered: { if (!locationInput.getActiveFocus() && !searchDropdown.hovered) root.resetSearchState(); } } Item { id: searchInputField width: parent.width height: 48 DankTextField { id: locationInput width: parent.width height: parent.height leftIconName: "search" placeholderText: root.placeholderText text: "" backgroundColor: Theme.surfaceVariant normalBorderColor: Theme.primarySelected focusedBorderColor: Theme.primary keyNavigationTab: root.keyNavigationTab keyNavigationBacktab: root.keyNavigationBacktab onTextEdited: { if (root._internalChange) return; if (getActiveFocus()) { if (text.length > 2) { root.isLoading = true; locationSearchTimer.restart(); } else { root.resetSearchState(); } } } onFocusStateChanged: hasFocus => { if (hasFocus) { dropdownHideTimer.stop(); } else { dropdownHideTimer.start(); } } } DankIcon { name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error") size: Theme.iconSize - 4 color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error) anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } } } StyledRect { id: searchDropdown property bool hovered: false width: parent.width height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200) y: searchInputField.height radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) border.color: Theme.primarySelected border.width: 1 visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading) MouseArea { anchors.fill: parent hoverEnabled: true onEntered: { parent.hovered = true; dropdownHideTimer.stop(); } onExited: { parent.hovered = false; if (!locationInput.getActiveFocus()) dropdownHideTimer.start(); } acceptedButtons: Qt.NoButton } Item { anchors.fill: parent anchors.margins: Theme.spacingS DankListView { id: searchResultsList anchors.fill: parent clip: true model: searchResultsModel spacing: 2 delegate: StyledRect { width: searchResultsList.width height: 36 radius: Theme.cornerRadius color: resultMouseArea.containsMouse ? Theme.surfaceLight : "transparent" Row { anchors.fill: parent anchors.margins: Theme.spacingM spacing: Theme.spacingS DankIcon { name: "place" size: Theme.iconSize - 6 color: Theme.surfaceVariantText anchors.verticalCenter: parent.verticalCenter } StyledText { text: model.name || "Unknown" font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter elide: Text.ElideRight width: parent.width - 30 } } MouseArea { id: resultMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { root._internalChange = true; const selectedName = model.name; const selectedQuery = model.query; locationInput.text = selectedName; root.locationSelected(selectedName, selectedQuery); root.resetSearchState(); locationInput.setFocus(false); root._internalChange = false; } } } } StyledText { anchors.centerIn: parent text: root.isLoading ? "Searching..." : "No locations found" font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText visible: searchResultsList.count === 0 && locationInput.text.length > 2 } } } }