aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--examples/quickcontrols/gallery/CMakeLists.txt2
-rw-r--r--examples/quickcontrols/gallery/gallery.pro1
-rw-r--r--examples/quickcontrols/gallery/gallery.qml1
-rw-r--r--examples/quickcontrols/gallery/pages/SearchFieldPage.qml44
-rw-r--r--examples/quickcontrols/gallery/qmlsortfilterproxymodel.h16
-rw-r--r--src/quickcontrols/basic/CMakeLists.txt3
-rw-r--r--src/quickcontrols/basic/SearchField.qml126
-rw-r--r--src/quickcontrols/basic/images/close_circle.pngbin0 -> 607 bytes
-rw-r--r--src/quickcontrols/basic/images/search-magnifier.pngbin0 -> 489 bytes
-rw-r--r--src/quickcontrols/doc/images/qtquickcontrols-searchfield.gifbin0 -> 48361 bytes
-rw-r--r--src/quicktemplates/CMakeLists.txt1
-rw-r--r--src/quicktemplates/qquicksearchfield.cpp1045
-rw-r--r--src/quicktemplates/qquicksearchfield_p.h115
-rw-r--r--tests/auto/quickcontrols/controls/data/tst_searchfield.qml230
-rw-r--r--tests/baseline/controls/data/searchfield/searchfield.qml84
-rw-r--r--tests/baseline/controls/tst_baseline_controls.cpp5
16 files changed, 1673 insertions, 0 deletions
diff --git a/examples/quickcontrols/gallery/CMakeLists.txt b/examples/quickcontrols/gallery/CMakeLists.txt
index a9bc9007bd..ca727ef18f 100644
--- a/examples/quickcontrols/gallery/CMakeLists.txt
+++ b/examples/quickcontrols/gallery/CMakeLists.txt
@@ -10,6 +10,7 @@ qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(galleryexample WIN32 MACOSX_BUNDLE
gallery.cpp
+ qmlsortfilterproxymodel.h
)
qt_add_qml_module(galleryexample
@@ -36,6 +37,7 @@ qt_add_qml_module(galleryexample
"pages/ScrollBarPage.qml"
"pages/ScrollIndicatorPage.qml"
"pages/ScrollablePage.qml"
+ "pages/SearchFieldPage.qml"
"pages/SliderPage.qml"
"pages/SpinBoxPage.qml"
"pages/StackViewPage.qml"
diff --git a/examples/quickcontrols/gallery/gallery.pro b/examples/quickcontrols/gallery/gallery.pro
index 42dc098425..c480fc710d 100644
--- a/examples/quickcontrols/gallery/gallery.pro
+++ b/examples/quickcontrols/gallery/gallery.pro
@@ -24,6 +24,7 @@ RESOURCES += \
pages/ScrollablePage.qml \
pages/ScrollBarPage.qml \
pages/ScrollIndicatorPage.qml \
+ pages/SearchFieldPage.qml \
pages/SliderPage.qml \
pages/SpinBoxPage.qml \
pages/StackViewPage.qml \
diff --git a/examples/quickcontrols/gallery/gallery.qml b/examples/quickcontrols/gallery/gallery.qml
index c06d1eb847..8370f88aba 100644
--- a/examples/quickcontrols/gallery/gallery.qml
+++ b/examples/quickcontrols/gallery/gallery.qml
@@ -149,6 +149,7 @@ ApplicationWindow {
ListElement { title: qsTr("RangeSlider"); source: "qrc:/pages/RangeSliderPage.qml" }
ListElement { title: qsTr("ScrollBar"); source: "qrc:/pages/ScrollBarPage.qml" }
ListElement { title: qsTr("ScrollIndicator"); source: "qrc:/pages/ScrollIndicatorPage.qml" }
+ ListElement { title: qsTr("SearchField"); source: "qrc:/pages/SearchFieldPage.qml" }
ListElement { title: qsTr("Slider"); source: "qrc:/pages/SliderPage.qml" }
ListElement { title: qsTr("SpinBox"); source: "qrc:/pages/SpinBoxPage.qml" }
ListElement { title: qsTr("StackView"); source: "qrc:/pages/StackViewPage.qml" }
diff --git a/examples/quickcontrols/gallery/pages/SearchFieldPage.qml b/examples/quickcontrols/gallery/pages/SearchFieldPage.qml
new file mode 100644
index 0000000000..1c0c21dadd
--- /dev/null
+++ b/examples/quickcontrols/gallery/pages/SearchFieldPage.qml
@@ -0,0 +1,44 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+
+ScrollablePage {
+ id: page
+
+ Column {
+ spacing: 40
+ width: parent.width
+
+ Label {
+ width: parent.width
+ wrapMode: Label.Wrap
+ horizontalAlignment: Qt.AlignHCenter
+ text: qsTr("SearchField is a styled text input for searching, typically "
+ + "with a magnifier and clear icon.")
+ }
+
+ ListModel {
+ id: colorModel
+ ListElement { color: "blue" }
+ ListElement { color: "green" }
+ ListElement { color: "red" }
+ ListElement { color: "yellow" }
+ ListElement { color: "orange" }
+ ListElement { color: "purple" }
+ }
+
+ SortFilterProxyModel {
+ id: colorFilter
+ sourceModel: colorModel
+ filterRegularExpression: RegExp(colorSearch.text, "i")
+ }
+
+ SearchField {
+ id: colorSearch
+ suggestionModel: colorFilter
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+}
diff --git a/examples/quickcontrols/gallery/qmlsortfilterproxymodel.h b/examples/quickcontrols/gallery/qmlsortfilterproxymodel.h
new file mode 100644
index 0000000000..5d398c2edb
--- /dev/null
+++ b/examples/quickcontrols/gallery/qmlsortfilterproxymodel.h
@@ -0,0 +1,16 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef QMLSORTFILTERPROXYMODEL_H
+#define QMLSORTFILTERPROXYMODEL_H
+
+#include <QtQml/qqmlregistration.h>
+#include <QSortFilterProxyModel>
+
+class QmlSortFilterProxyModel {
+ Q_GADGET
+ QML_FOREIGN(QSortFilterProxyModel)
+ QML_NAMED_ELEMENT(SortFilterProxyModel)
+};
+
+#endif // QMLSORTFILTERPROXYMODEL_H
diff --git a/src/quickcontrols/basic/CMakeLists.txt b/src/quickcontrols/basic/CMakeLists.txt
index 265be49319..e2925d92dd 100644
--- a/src/quickcontrols/basic/CMakeLists.txt
+++ b/src/quickcontrols/basic/CMakeLists.txt
@@ -46,6 +46,7 @@ set(qml_files
"ScrollBar.qml"
"ScrollIndicator.qml"
"ScrollView.qml"
+ "SearchField.qml"
"SelectionRectangle.qml"
"Slider.qml"
"SpinBox.qml"
@@ -210,6 +211,7 @@ set(qtquickcontrols2basicstyle_resource_files
+ "images/close_circle.png"
"images/dial-indicator.png"
@@ -222,6 +224,7 @@ set(qtquickcontrols2basicstyle_resource_files
+ "images/search-magnifier.png"
)
qt_internal_add_resource(QuickControls2Basic "qtquickcontrols2basicstyle"
diff --git a/src/quickcontrols/basic/SearchField.qml b/src/quickcontrols/basic/SearchField.qml
new file mode 100644
index 0000000000..4fff8878c0
--- /dev/null
+++ b/src/quickcontrols/basic/SearchField.qml
@@ -0,0 +1,126 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls.impl
+import QtQuick.Templates as T
+
+T.SearchField {
+ id: control
+
+ implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+ implicitContentWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+ implicitContentHeight + topPadding + bottomPadding,
+ searchIndicator.implicitIndicatorHeight + topPadding + bottomPadding,
+ clearIndicator.implicitIndicatorHeight + topPadding + bottomPadding)
+
+ leftPadding: padding + (control.mirrored || !searchIndicator.indicator || !searchIndicator.indicator.visible ? 0 : searchIndicator.indicator.width + spacing)
+ rightPadding: padding + (control.mirrored || !clearIndicator.indicator || !clearIndicator.indicator.visible ? 0 : clearIndicator.indicator.width + spacing)
+
+ delegate: ItemDelegate {
+ width: ListView.view.width
+ text: model[control.textRole]
+ palette.text: control.palette.text
+ palette.highlightedText: control.palette.highlightedText
+ font.weight: control.currentIndex === index ? Font.DemiBold : Font.Normal
+ highlighted: control.currentIndex === index
+ hoverEnabled: control.hoverEnabled
+
+ required property var model
+ required property int index
+ }
+
+ searchIndicator.indicator: Rectangle {
+ implicitWidth: 28
+ implicitHeight: 28
+
+ x: !control.mirrored ? 3 : control.width - width - 3
+ y: control.topPadding + (control.availableHeight - height) / 2
+ color: control.palette.button
+
+ ColorImage {
+ x: (parent.width - width) / 2
+ y: (parent.height - height) / 2
+ color: control.palette.dark
+ defaultColor: "#353637"
+ source: "qrc:/qt-project.org/imports/QtQuick/Controls/Basic/images/search-magnifier.png"
+ opacity: enabled ? 1 : 0.3
+ }
+ }
+
+ clearIndicator.indicator: Rectangle {
+ implicitWidth: 28
+ implicitHeight: 28
+
+ x: control.mirrored ? 3 : control.width - width - 3
+ y: control.topPadding + (control.availableHeight - height) / 2
+ visible: control.text.length > 0
+ color: control.palette.button
+
+ ColorImage {
+ x: (parent.width - width) / 2
+ y: (parent.height - height) / 2
+ color: control.palette.dark
+ defaultColor: "#353637"
+ source: "qrc:/qt-project.org/imports/QtQuick/Controls/Basic/images/close_circle.png"
+ opacity: enabled ? 1 : 0.3
+ }
+ }
+
+ contentItem: T.TextField {
+ leftPadding: control.searchIndicator.indicator && !control.mirrored ? 6 : 0
+ rightPadding: control.clearIndicator.indicator && !control.mirrored ? 6 : 0
+ topPadding: 6 - control.padding
+ bottomPadding: 6 - control.padding
+
+ text: control.text
+
+ color: control.palette.text
+ selectionColor: control.palette.highlight
+ selectedTextColor: control.palette.highlightedText
+ verticalAlignment: TextInput.AlignVCenter
+ }
+
+ background: Rectangle {
+ implicitWidth: 200
+ implicitHeight: 40
+
+ color: control.palette.button
+ border.width: (control.activeFocus || control.contentItem.activeFocus) ? 2 : 1
+ border.color: (control.activeFocus || control.contentItem.activeFocus) ? control.palette.highlight : control.palette.mid
+ }
+
+ popup: T.Popup {
+ y: control.height
+ width: control.width
+ height: Math.min(contentItem.implicitHeight, control.Window.height - control.y - control.height - control.padding)
+ topMargin: 6
+ bottomMargin: 6
+ palette: control.palette
+
+ contentItem: ListView {
+ clip: true
+ implicitHeight: contentHeight
+ model: control.delegateModel
+ currentIndex: control.currentIndex
+ highlightMoveDuration: 0
+
+ Rectangle {
+ z: 10
+ width: parent.width
+ height: parent.height
+ color: "transparent"
+ border.color: control.palette.mid
+ }
+
+ T.ScrollIndicator.vertical: ScrollIndicator { }
+ }
+
+ background: Rectangle {
+ color: control.palette.window
+ }
+ }
+}
diff --git a/src/quickcontrols/basic/images/close_circle.png b/src/quickcontrols/basic/images/close_circle.png
new file mode 100644
index 0000000000..4b2644d4f5
--- /dev/null
+++ b/src/quickcontrols/basic/images/close_circle.png
Binary files differ
diff --git a/src/quickcontrols/basic/images/search-magnifier.png b/src/quickcontrols/basic/images/search-magnifier.png
new file mode 100644
index 0000000000..1bd4da97a0
--- /dev/null
+++ b/src/quickcontrols/basic/images/search-magnifier.png
Binary files differ
diff --git a/src/quickcontrols/doc/images/qtquickcontrols-searchfield.gif b/src/quickcontrols/doc/images/qtquickcontrols-searchfield.gif
new file mode 100644
index 0000000000..c323767bd0
--- /dev/null
+++ b/src/quickcontrols/doc/images/qtquickcontrols-searchfield.gif
Binary files differ
diff --git a/src/quicktemplates/CMakeLists.txt b/src/quicktemplates/CMakeLists.txt
index cf89913679..e56b0dc7c1 100644
--- a/src/quicktemplates/CMakeLists.txt
+++ b/src/quicktemplates/CMakeLists.txt
@@ -83,6 +83,7 @@ qt_internal_add_qml_module(QuickTemplates2
qquickscrollbar_p_p.h
qquickscrollindicator.cpp qquickscrollindicator_p.h
qquickscrollview.cpp qquickscrollview_p.h
+ qquicksearchfield.cpp qquicksearchfield_p.h
qquickshortcutcontext.cpp
qquickshortcutcontext_p_p.h
qquickslider.cpp qquickslider_p.h
diff --git a/src/quicktemplates/qquicksearchfield.cpp b/src/quicktemplates/qquicksearchfield.cpp
new file mode 100644
index 0000000000..c7d6d4f1cb
--- /dev/null
+++ b/src/quicktemplates/qquicksearchfield.cpp
@@ -0,0 +1,1045 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquicksearchfield_p.h"
+#include "qquickcontrol_p_p.h"
+#include <private/qquickindicatorbutton_p.h>
+#include <QtQuickTemplates2/private/qquicktextfield_p.h>
+#include "qquickpopup_p_p.h"
+#include "qquickdeferredexecute_p_p.h"
+#include <private/qqmldelegatemodel_p.h>
+#include "qquickabstractbutton_p.h"
+#include "qquickabstractbutton_p_p.h"
+#include <QtQuick/private/qquickaccessibleattached_p.h>
+#if QT_CONFIG(quick_itemview)
+# include <QtQuick/private/qquickitemview_p.h>
+#endif
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype SearchField
+ \inherits Control
+ //! \nativetype QQuickSearchField
+ \inqmlmodule QtQuick.Controls
+ \since 6.10
+ \ingroup qtquickcontrols-input
+ \ingroup qtquickcontrols-focusscopes
+ \brief A specialized input field designed to use for search functionality.
+
+ SearchField is a specialized input field designed to use for search functionality.
+ The control includes a text field, search and clear icons, and a popup that
+ displays suggestions or search results.
+
+ \image qtquickcontrols-searchfield.gif
+
+ \section1 SearchField Model Roles
+
+ SearchField is able to visualize standard \l {qml-data-models}{data models}
+ that provide the \c modelData role:
+ \list
+ \li models that have only one role
+ \li models that do not have named roles (JavaScript array, integer)
+ \endlist
+
+ When using models that have multiple named roles, SearchField must be configured
+ to use a specific \l {textRole}{text role} for its \l {text}{text}
+ and \l delegate instances.
+
+ \code
+ ListModel {
+ id : fruitModel
+ ListElement { name: "Apple"; color: "green" }
+ ListElement { name: "Cherry"; color: "red" }
+ ListElement { name: "Banana"; color: "yellow" }
+ ListElement { name: "Orange"; color: "orange" }
+ ListElement { name: "WaterMelon"; color: "pink" }
+ }
+
+ QSortFilterProxyModel {
+ id: fruitFilter
+ sourceModel: fruitModel
+ filterRegularExpression: RegExp(fruitSearch.text, "i")
+ filterRole: 0 // needs to be set explicitly
+ }
+
+ SearchField {
+ id: fruitSearch
+ suggestionModel: fruitFilter
+ textRole: "name"
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ \endcode
+ */
+
+/*!
+ \qmlsignal void QtQuick.Controls::SearchField::activated(int index)
+
+ This signal is emitted when the item at \a index is activated by the user.
+
+ An item is activated when it is selected while the popup is open,
+ causing the popup to close (and \l currentIndex to change).
+ The \l currentIndex property is set to \a index.
+
+ \sa currentIndex
+*/
+
+/*!
+ \qmlsignal void QtQuick.Controls::SearchField::accepted()
+
+ This signal is emitted when the user confirms their input by pressing
+ the Enter or Return key.
+
+ This signal is typically used to trigger a search or action based on
+ the final text input, and it indicates the user's intention to complete
+ or submit the query.
+
+ \sa searchTriggered()
+ */
+
+/*!
+ \qmlsignal void QtQuick.Controls::SearchField::searchTriggered()
+
+ This signal is emitted when a search action is initiated.
+
+ It occurs in two cases:
+ 1. When the Enter or Return key is pressed, it will be emitted together
+ with accepted() signal
+ 2. When the text is edited and if the \l live property is set to \c true,
+ this signal will be emitted.
+
+ This signal is ideal for initiating searches both on-demand and in real-time as
+ the user types, depending on the desired interaction model.
+
+ \sa accepted(), textEdited()
+ */
+
+/*!
+ \qmlsignal void QtQuick.Controls::SearchField::textEdited()
+
+ This signal is emitted every time the user modifies the text in the
+ search field, typically with each keystroke.
+
+ \sa searchTriggered()
+ */
+
+class QQuickSearchFieldPrivate : public QQuickControlPrivate
+{
+public:
+ Q_DECLARE_PUBLIC(QQuickSearchField)
+
+ bool isPopupVisible() const;
+ void showPopup();
+ void hidePopup();
+ static void hideOldPopup(QQuickPopup *popup);
+ void popupVisibleChanged();
+ void popupDestroyed();
+
+ void itemClicked();
+ void itemHovered();
+
+ void createdItem(int index, QObject *object);
+ void suggestionCountChanged();
+
+ void increaseCurrentIndex();
+ void decreaseCurrentIndex();
+ void setCurrentIndex(int index);
+
+ void createDelegateModel();
+
+ QString currentTextRole() const;
+ void selectAll();
+ void updateText();
+ void updateDisplayText();
+ QString textAt(int index) const;
+ bool isValidIndex(int index) const;
+
+ void cancelPopup();
+ void executePopup(bool complete = false);
+
+ bool handlePress(const QPointF &point, ulong timestamp) override;
+ bool handleRelease(const QPointF &point, ulong timestamp) override;
+
+ void startSearch();
+ void startClear();
+
+ void itemImplicitWidthChanged(QQuickItem *item) override;
+ void itemImplicitHeightChanged(QQuickItem *item) override;
+ void itemDestroyed(QQuickItem *item) override;
+
+ static inline QString popupName() { return QStringLiteral("popup"); }
+
+ QVariant suggestionModel;
+ bool hasCurrentIndex = false;
+ int currentIndex = -1;
+ QString text;
+ QString textRole;
+ bool live = true;
+ bool searchPressed = false;
+ bool clearPressed = false;
+ bool searchFlat = false;
+ bool clearFlat = false;
+ bool searchDown = false;
+ bool clearDown = false;
+ bool hasSearchDown = false;
+ bool hasClearDown = false;
+ bool ownModel = false;
+ QQmlInstanceModel *delegateModel = nullptr;
+ QQmlComponent *delegate = nullptr;
+ QQuickIndicatorButton *searchIndicator = nullptr;
+ QQuickIndicatorButton *clearIndicator = nullptr;
+ QQuickDeferredPointer<QQuickPopup> popup;
+};
+
+bool QQuickSearchFieldPrivate::isPopupVisible() const
+{
+ return popup && popup->isVisible();
+}
+
+void QQuickSearchFieldPrivate::showPopup()
+{
+ if (!popup)
+ executePopup(true);
+
+ if (popup && !popup->isVisible())
+ popup->open();
+}
+
+void QQuickSearchFieldPrivate::hidePopup()
+{
+ if (popup && popup->isVisible())
+ popup->close();
+}
+
+void QQuickSearchFieldPrivate::hideOldPopup(QQuickPopup *popup)
+{
+ if (!popup)
+ return;
+
+ qCDebug(lcItemManagement) << "hiding old popup" << popup;
+
+ popup->setVisible(false);
+ popup->setParentItem(nullptr);
+#if QT_CONFIG(accessibility)
+ // Remove the item from the accessibility tree.
+ QQuickAccessibleAttached *accessible = accessibleAttached(popup);
+ if (accessible)
+ accessible->setIgnored(true);
+#endif
+}
+
+void QQuickSearchFieldPrivate::popupVisibleChanged()
+{
+ if (isPopupVisible())
+ QGuiApplication::inputMethod()->reset();
+
+#if QT_CONFIG(quick_itemview)
+ QQuickItemView *itemView = popup->findChild<QQuickItemView *>();
+ if (itemView)
+ itemView->setHighlightRangeMode(QQuickItemView::NoHighlightRange);
+#endif
+
+ if (popup->isVisible())
+ setCurrentIndex(currentIndex);
+ else
+ setCurrentIndex(0);
+
+#if QT_CONFIG(quick_itemview)
+ if (itemView)
+ itemView->positionViewAtIndex(currentIndex, QQuickItemView::Beginning);
+#endif
+}
+
+void QQuickSearchFieldPrivate::popupDestroyed()
+{
+ Q_Q(QQuickSearchField);
+ popup = nullptr;
+ emit q->popupChanged();
+}
+
+void QQuickSearchFieldPrivate::itemClicked()
+{
+ Q_Q(QQuickSearchField);
+ int index = delegateModel->indexOf(q->sender(), nullptr);
+ if (index != -1) {
+ setCurrentIndex(index);
+ updateDisplayText();
+ hidePopup();
+
+ emit q->activated(index);
+ }
+}
+
+void QQuickSearchFieldPrivate::itemHovered()
+{
+ Q_Q(QQuickSearchField);
+
+ QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(q->sender());
+ if (!button || !button->isHovered() || !button->isEnabled()
+ || QQuickAbstractButtonPrivate::get(button)->touchId != -1)
+ return;
+
+ int index = delegateModel->indexOf(button, nullptr);
+ if (index != -1) {
+ setCurrentIndex(index);
+
+#if QT_CONFIG(quick_itemview)
+ if (QQuickItemView *itemView = popup->findChild<QQuickItemView *>())
+ itemView->positionViewAtIndex(index, QQuickItemView::Contain);
+#endif
+ }
+}
+
+void QQuickSearchFieldPrivate::createdItem(int index, QObject *object)
+{
+ Q_UNUSED(index);
+ Q_Q(QQuickSearchField);
+ QQuickItem *item = qobject_cast<QQuickItem *>(object);
+ if (item && !item->parentItem()) {
+ if (popup)
+ item->setParentItem(popup->contentItem());
+ else
+ item->setParentItem(q);
+ QQuickItemPrivate::get(item)->setCulled(true);
+ }
+
+ QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(object);
+ if (button) {
+ button->setFocusPolicy(Qt::NoFocus);
+ connect(button, &QQuickAbstractButton::clicked, this,
+ &QQuickSearchFieldPrivate::itemClicked);
+ connect(button, &QQuickAbstractButton::hoveredChanged, this,
+ &QQuickSearchFieldPrivate::itemHovered);
+ }
+}
+
+void QQuickSearchFieldPrivate::suggestionCountChanged()
+{
+ Q_Q(QQuickSearchField);
+ if (q->suggestionCount() == 0)
+ q->setCurrentIndex(-1);
+ emit q->suggestionCountChanged();
+}
+
+void QQuickSearchFieldPrivate::increaseCurrentIndex()
+{
+ Q_Q(QQuickSearchField);
+ if (currentIndex < q->suggestionCount() - 1)
+ setCurrentIndex(currentIndex + 1);
+ else if (currentIndex == q->suggestionCount() - 1)
+ setCurrentIndex(0);
+}
+
+void QQuickSearchFieldPrivate::decreaseCurrentIndex()
+{
+ if (currentIndex > 0)
+ setCurrentIndex(currentIndex - 1);
+}
+
+void QQuickSearchFieldPrivate::setCurrentIndex(int index)
+{
+ Q_Q(QQuickSearchField);
+ if (currentIndex == index)
+ return;
+
+ currentIndex = index;
+ emit q->currentIndexChanged();
+}
+
+void QQuickSearchFieldPrivate::createDelegateModel()
+{
+ Q_Q(QQuickSearchField);
+ bool ownedOldModel = ownModel;
+ QQmlInstanceModel *oldModel = delegateModel;
+
+ if (oldModel) {
+ disconnect(delegateModel, &QQmlInstanceModel::countChanged, this,
+ &QQuickSearchFieldPrivate::suggestionCountChanged);
+ disconnect(delegateModel, &QQmlInstanceModel::createdItem, this,
+ &QQuickSearchFieldPrivate::createdItem);
+ }
+
+ ownModel = false;
+ delegateModel = suggestionModel.value<QQmlInstanceModel *>();
+
+ if (!delegateModel && suggestionModel.isValid()) {
+ QQmlDelegateModel *dataModel = new QQmlDelegateModel(qmlContext(q), q);
+ dataModel->setModel(suggestionModel);
+ dataModel->setDelegate(delegate);
+ if (q->isComponentComplete())
+ dataModel->componentComplete();
+
+ ownModel = true;
+ delegateModel = dataModel;
+ }
+
+ if (delegateModel) {
+ connect(delegateModel, &QQmlInstanceModel::countChanged, this,
+ &QQuickSearchFieldPrivate::suggestionCountChanged);
+ connect(delegateModel, &QQmlInstanceModel::createdItem, this,
+ &QQuickSearchFieldPrivate::createdItem);
+ }
+
+ emit q->delegateModelChanged();
+
+ if (ownedOldModel)
+ delete oldModel;
+}
+
+QString QQuickSearchFieldPrivate::currentTextRole() const
+{
+ return textRole.isEmpty() ? QStringLiteral("modelData") : textRole;
+}
+
+void QQuickSearchFieldPrivate::selectAll()
+{
+ QQuickTextInput *input = qobject_cast<QQuickTextInput *>(contentItem);
+ if (!input)
+ return;
+ input->selectAll();
+}
+
+void QQuickSearchFieldPrivate::updateText()
+{
+ Q_Q(QQuickSearchField);
+ QQuickTextInput *input = qobject_cast<QQuickTextInput *>(contentItem);
+ if (!input)
+ return;
+
+ const QString textInput = input->text();
+
+ if (text != textInput) {
+ q->setText(textInput);
+ emit q->textEdited();
+
+ if (live)
+ emit q->searchTriggered();
+ }
+
+ if (!text.isEmpty() && !isPopupVisible())
+ showPopup();
+ else if (text.isEmpty() && isPopupVisible())
+ hidePopup();
+}
+
+void QQuickSearchFieldPrivate::updateDisplayText()
+{
+ Q_Q(QQuickSearchField);
+ const QString currentText = textAt(currentIndex);
+
+ if (text != currentText)
+ q->setText(currentText);
+}
+
+QString QQuickSearchFieldPrivate::textAt(int index) const
+{
+ if (!isValidIndex(index))
+ return QString();
+
+ return delegateModel->stringValue(index, currentTextRole());
+}
+
+bool QQuickSearchFieldPrivate::isValidIndex(int index) const
+{
+ return delegateModel && index >= 0 && index < delegateModel->count();
+}
+
+void QQuickSearchFieldPrivate::cancelPopup()
+{
+ Q_Q(QQuickSearchField);
+ quickCancelDeferred(q, popupName());
+}
+
+void QQuickSearchFieldPrivate::executePopup(bool complete)
+{
+ Q_Q(QQuickSearchField);
+ if (popup.wasExecuted())
+ return;
+
+ if (!popup || complete)
+ quickBeginDeferred(q, popupName(), popup);
+ if (complete)
+ quickCompleteDeferred(q, popupName(), popup);
+}
+
+bool QQuickSearchFieldPrivate::handlePress(const QPointF &point, ulong timestamp)
+{
+ Q_Q(QQuickSearchField);
+ QQuickControlPrivate::handlePress(point, timestamp);
+
+ QQuickItem *si = searchIndicator->indicator();
+ QQuickItem *ci = clearIndicator->indicator();
+ const bool isSearch = si && si->isEnabled() && si->contains(q->mapToItem(si, point));
+ const bool isClear = ci && ci->isEnabled() && ci->contains(q->mapToItem(ci, point));
+
+ if (isSearch) {
+ searchIndicator->setPressed(true);
+ startSearch();
+ } else if (isClear) {
+ clearIndicator->setPressed(true);
+ startClear();
+ }
+
+ return true;
+}
+
+bool QQuickSearchFieldPrivate::handleRelease(const QPointF &point, ulong timestamp)
+{
+ QQuickControlPrivate::handleRelease(point, timestamp);
+ if (searchIndicator->isPressed())
+ searchIndicator->setPressed(false);
+ else if (clearIndicator->isPressed())
+ clearIndicator->setPressed(false);
+ return true;
+}
+
+void QQuickSearchFieldPrivate::startSearch()
+{
+ Q_Q(QQuickSearchField);
+
+ QQuickTextInput *input = qobject_cast<QQuickTextInput *>(contentItem);
+ if (!input)
+ return;
+
+ input->forceActiveFocus();
+ emit q->searchButtonPressed();
+}
+
+void QQuickSearchFieldPrivate::startClear()
+{
+ Q_Q(QQuickSearchField);
+
+ if (text.isEmpty())
+ return;
+
+ // if text is not null then clear, also update suggestionModel
+ if (!text.isEmpty()) {
+ suggestionModel.clear();
+ q->setText(QString());
+
+ if (isPopupVisible())
+ hidePopup();
+
+ emit q->clearButtonPressed();
+ }
+}
+
+void QQuickSearchFieldPrivate::itemImplicitWidthChanged(QQuickItem *item)
+{
+ QQuickControlPrivate::itemImplicitWidthChanged(item);
+ if (item == searchIndicator->indicator())
+ emit searchIndicator->implicitIndicatorWidthChanged();
+ if (item == clearIndicator->indicator())
+ emit clearIndicator->implicitIndicatorWidthChanged();
+}
+
+void QQuickSearchFieldPrivate::itemImplicitHeightChanged(QQuickItem *item)
+{
+ QQuickControlPrivate::itemImplicitHeightChanged(item);
+ if (item == searchIndicator->indicator())
+ emit searchIndicator->implicitIndicatorHeightChanged();
+ if (item == clearIndicator->indicator())
+ emit clearIndicator->implicitIndicatorHeightChanged();
+}
+
+void QQuickSearchFieldPrivate::itemDestroyed(QQuickItem *item)
+{
+ QQuickControlPrivate::itemDestroyed(item);
+ if (item == searchIndicator->indicator())
+ searchIndicator->setIndicator(nullptr);
+ if (item == clearIndicator->indicator())
+ clearIndicator->setIndicator(nullptr);
+}
+
+QQuickSearchField::QQuickSearchField(QQuickItem *parent)
+ : QQuickControl(*(new QQuickSearchFieldPrivate), parent)
+{
+ Q_D(QQuickSearchField);
+ d->searchIndicator = new QQuickIndicatorButton(this);
+ d->clearIndicator = new QQuickIndicatorButton(this);
+
+ setFocusPolicy(Qt::StrongFocus);
+ setFlag(QQuickItem::ItemIsFocusScope);
+ setAcceptedMouseButtons(Qt::LeftButton);
+#if QT_CONFIG(cursor)
+ setCursor(Qt::ArrowCursor);
+#endif
+
+ d->init();
+}
+
+QQuickSearchField::~QQuickSearchField()
+{
+ Q_D(QQuickSearchField);
+ d->removeImplicitSizeListener(d->searchIndicator->indicator());
+ d->removeImplicitSizeListener(d->clearIndicator->indicator());
+
+ if (d->popup) {
+ QObjectPrivate::disconnect(d->popup.data(), &QQuickPopup::visibleChanged, d,
+ &QQuickSearchFieldPrivate::popupVisibleChanged);
+ d->hideOldPopup(d->popup);
+ d->popup = nullptr;
+ }
+}
+
+/*!
+ \qmlproperty model QtQuick.Controls::SearchField::suggestionModel
+
+ This property holds the data model used to display search suggestions in the popup menu.
+
+ \code
+ SearchField {
+ textRole: "age"
+ suggestionModel: ListModel {
+ ListElement { name: "Karen"; age: "66" }
+ ListElement { name: "Jim"; age: "32" }
+ ListElement { name: "Pamela"; age: "28" }
+ }
+ }
+ \endcode
+
+ \sa textRole
+ */
+
+QVariant QQuickSearchField::suggestionModel() const
+{
+ Q_D(const QQuickSearchField);
+ return d->suggestionModel;
+}
+
+void QQuickSearchField::setSuggestionModel(const QVariant &model)
+{
+ Q_D(QQuickSearchField);
+
+ QVariant suggestionModel = model;
+ if (suggestionModel.userType() == qMetaTypeId<QJSValue>())
+ suggestionModel = get<QJSValue>(std::move(suggestionModel)).toVariant();
+
+ if (d->suggestionModel == suggestionModel)
+ return;
+
+ d->suggestionModel = suggestionModel;
+ d->createDelegateModel();
+ emit suggestionCountChanged();
+ if (isComponentComplete()) {
+ setCurrentIndex(suggestionCount() > 0 ? 0 : -1);
+ }
+ emit suggestionModelChanged();
+}
+
+/*!
+ \readonly
+ \qmlproperty model QtQuick.Controls::SearchField::delegateModel
+
+ This property holds the model that provides delegate instances for the search field.
+
+ It is typically assigned to a \l ListView in the \l {Popup::}{contentItem}
+ of the \l popup.
+
+ */
+QQmlInstanceModel *QQuickSearchField::delegateModel() const
+{
+ Q_D(const QQuickSearchField);
+ return d->delegateModel;
+}
+
+/*!
+ \readonly
+ \qmlproperty int QtQuick.Controls::SearchField::suggestionCount
+
+ This property holds the number of suggestions to display from the suggestion model.
+ */
+int QQuickSearchField::suggestionCount() const
+{
+ Q_D(const QQuickSearchField);
+ return d->delegateModel ? d->delegateModel->count() : 0;
+}
+
+/*!
+ \qmlproperty int QtQuick.Controls::SearchField::currentIndex
+
+ This property holds the index of the currently selected suggestion in the popup list.
+
+ The default value is \c -1 when count is \c 0, and \c 0 otherwise.
+ */
+int QQuickSearchField::currentIndex() const
+{
+ Q_D(const QQuickSearchField);
+ return d->currentIndex;
+}
+
+void QQuickSearchField::setCurrentIndex(int index)
+{
+ Q_D(QQuickSearchField);
+ d->hasCurrentIndex = true;
+ d->setCurrentIndex(index);
+}
+
+/*!
+ \qmlproperty string QtQuick.Controls::SearchField::text
+
+ This property holds the current input text in the search field.
+
+ Text is bound to the user input, triggering suggestion updates or search logic.
+
+ \sa searchTriggered(), textEdited()
+ */
+QString QQuickSearchField::text() const
+{
+ Q_D(const QQuickSearchField);
+ return d->text;
+}
+
+void QQuickSearchField::setText(const QString &text)
+{
+ Q_D(QQuickSearchField);
+ if (d->text == text)
+ return;
+
+ d->text = text;
+ emit textChanged();
+}
+
+/*!
+ \qmlproperty string QtQuick.Controls::SearchField::textRole
+
+ This property holds the model role used to display items in the suggestion model
+ shown in the popup list.
+
+ When the model has multiple roles, \c textRole can be set to determine
+ which role should be displayed.
+ */
+QString QQuickSearchField::textRole() const
+{
+ Q_D(const QQuickSearchField);
+ return d->textRole;
+}
+
+void QQuickSearchField::setTextRole(const QString &textRole)
+{
+ Q_D(QQuickSearchField);
+ if (d->textRole == textRole)
+ return;
+
+ d->textRole = textRole;
+}
+
+/*!
+ \qmlproperty bool QtQuick.Controls::SearchField::live
+
+ This property holds a boolean value that determines whether the search is triggered
+ on every text edit.
+
+ When set to \c true, the \l searchTriggered() signal is emitted on each text change,
+ allowing you to respond to every keystroke.
+ When set to \c false, the \l searchTriggered() is only emitted when the user presses
+ the Enter or Return key.
+
+ \sa searchTriggered()
+ */
+
+bool QQuickSearchField::isLive() const
+{
+ Q_D(const QQuickSearchField);
+ return d->live;
+}
+
+void QQuickSearchField::setLive(const bool live)
+{
+ Q_D(QQuickSearchField);
+
+ if (d->live == live)
+ return;
+
+ d->live = live;
+}
+
+/*!
+ \qmlproperty real QtQuick.Controls::SearchField::searchIndicator
+ \readonly
+
+ This property holds the search indicator.
+ */
+QQuickIndicatorButton *QQuickSearchField::searchIndicator() const
+{
+ Q_D(const QQuickSearchField);
+ return d->searchIndicator;
+}
+
+/*!
+ \qmlproperty real QtQuick.Controls::SearchField::clearIndicator
+ \readonly
+
+ This property holds the clear indicator.
+*/
+QQuickIndicatorButton *QQuickSearchField::clearIndicator() const
+{
+ Q_D(const QQuickSearchField);
+ return d->clearIndicator;
+}
+
+
+/*!
+ \qmlproperty Popup QtQuick.Controls::SearchField::popup
+
+ This property holds the popup.
+
+ The popup can be opened or closed manually, if necessary:
+
+ \code
+ onSpecialEvent: searchField.popup.close()
+ \endcode
+ */
+QQuickPopup *QQuickSearchField::popup() const
+{
+ QQuickSearchFieldPrivate *d = const_cast<QQuickSearchFieldPrivate *>(d_func());
+ if (!d->popup)
+ d->executePopup(isComponentComplete());
+ return d->popup;
+}
+
+void QQuickSearchField::setPopup(QQuickPopup *popup)
+{
+ Q_D(QQuickSearchField);
+ if (d->popup == popup)
+ return;
+
+ if (!d->popup.isExecuting())
+ d->cancelPopup();
+
+ if (d->popup) {
+ QObjectPrivate::disconnect(d->popup.data(), &QQuickPopup::destroyed, d,
+ &QQuickSearchFieldPrivate::popupDestroyed);
+ QObjectPrivate::disconnect(d->popup.data(), &QQuickPopup::visibleChanged, d,
+ &QQuickSearchFieldPrivate::popupVisibleChanged);
+ QQuickSearchFieldPrivate::hideOldPopup(d->popup);
+ }
+
+ if (popup) {
+ QQuickPopupPrivate::get(popup)->allowVerticalFlip = true;
+ popup->setClosePolicy(QQuickPopup::CloseOnEscape | QQuickPopup::CloseOnPressOutsideParent);
+ QObjectPrivate::connect(popup, &QQuickPopup::visibleChanged, d,
+ &QQuickSearchFieldPrivate::popupVisibleChanged);
+ // QQuickPopup does not derive from QQuickItemChangeListener, so we cannot use
+ // QQuickItemChangeListener::itemDestroyed so we have to use QObject::destroyed
+ QObjectPrivate::connect(popup, &QQuickPopup::destroyed, d,
+ &QQuickSearchFieldPrivate::popupDestroyed);
+ }
+
+#if QT_CONFIG(quick_itemview)
+ if (QQuickItemView *itemView = popup->findChild<QQuickItemView *>())
+ itemView->setHighlightRangeMode(QQuickItemView::NoHighlightRange);
+#endif
+
+ d->popup = popup;
+ if (!d->popup.isExecuting())
+ emit popupChanged();
+}
+
+/*!
+ \qmlproperty Component QtQuick.Controls::SearchField::delegate
+
+ This property holds a delegate that presents an item in the search field popup.
+
+ It is recommended to use \l ItemDelegate (or any other \l AbstractButton
+ derivatives) as the delegate. This ensures that the interaction works as
+ expected, and the popup will automatically close when appropriate. When
+ other types are used as the delegate, the popup must be closed manually.
+ For example, if \l MouseArea is used:
+
+ \code
+ delegate: Rectangle {
+ // ...
+ MouseArea {
+ // ...
+ onClicked: searchField.popup.close()
+ }
+ }
+ \endcode
+*/
+QQmlComponent *QQuickSearchField::delegate() const
+{
+ Q_D(const QQuickSearchField);
+ return d->delegate;
+}
+
+void QQuickSearchField::setDelegate(QQmlComponent *delegate)
+{
+ Q_D(QQuickSearchField);
+ if (d->delegate == delegate)
+ return;
+
+ delete d->delegate;
+ d->delegate = delegate;
+ QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel *>(d->delegateModel);
+ if (delegateModel)
+ delegateModel->setDelegate(d->delegate);
+ emit delegateChanged();
+}
+
+bool QQuickSearchField::eventFilter(QObject *object, QEvent *event)
+{
+ Q_D(QQuickSearchField);
+
+ switch (event->type()) {
+ case QEvent::MouseButtonRelease: {
+ QQuickTextInput *input = qobject_cast<QQuickTextInput *>(d->contentItem);
+ if (input->hasFocus()) {
+ if (!d->text.isEmpty() && !d->isPopupVisible())
+ d->showPopup();
+ }
+ break;
+ }
+ case QEvent::FocusOut: {
+ const bool hasActiveFocus = d->popup && d->popup->hasActiveFocus();
+ const bool usingPopupWindows =
+ d->popup ? QQuickPopupPrivate::get(d->popup)->usePopupWindow() : false;
+ if (qGuiApp->focusObject() != this && !(hasActiveFocus && !usingPopupWindows)) {
+ d->hidePopup();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return QQuickControl::eventFilter(object, event);
+}
+
+void QQuickSearchField::focusInEvent(QFocusEvent *event)
+{
+ Q_D(QQuickSearchField);
+ QQuickControl::focusInEvent(event);
+
+ if ((event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason
+ || event->reason() == Qt::ShortcutFocusReason)
+ && d->contentItem)
+ d->contentItem->forceActiveFocus(event->reason());
+}
+
+void QQuickSearchField::focusOutEvent(QFocusEvent *event)
+{
+ Q_D(QQuickSearchField);
+ QQuickControl::focusOutEvent(event);
+
+ const bool hasActiveFocus = d->popup && d->popup->hasActiveFocus();
+ const bool usingPopupWindows = d->popup && QQuickPopupPrivate::get(d->popup)->usePopupWindow();
+
+ if (qGuiApp->focusObject() != d->contentItem && !(hasActiveFocus && !usingPopupWindows))
+ d->hidePopup();
+}
+
+void QQuickSearchField::keyPressEvent(QKeyEvent *event)
+{
+ Q_D(QQuickSearchField);
+
+ const auto key = event->key();
+
+ if (!d->suggestionModel.isNull() && !d->text.isEmpty()) {
+ switch (key) {
+ case Qt::Key_Escape:
+ case Qt::Key_Back:
+ if (d->isPopupVisible()) {
+ d->hidePopup();
+ event->accept();
+ } else {
+ setText(QString());
+ }
+ break;
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ d->updateDisplayText();
+ emit accepted();
+ emit searchTriggered();
+ event->accept();
+ break;
+ case Qt::Key_Up:
+ d->decreaseCurrentIndex();
+ event->accept();
+ break;
+ case Qt::Key_Down:
+ d->increaseCurrentIndex();
+ event->accept();
+ break;
+ case Qt::Key_Home:
+ d->setCurrentIndex(0);
+ event->accept();
+ break;
+ case Qt::Key_End:
+ d->setCurrentIndex(suggestionCount() - 1);
+ event->accept();
+ break;
+ default:
+ QQuickControl::keyPressEvent(event);
+ break;
+ }
+ }
+}
+
+void QQuickSearchField::classBegin()
+{
+ Q_D(QQuickSearchField);
+ QQuickControl::classBegin();
+
+ QQmlContext *context = qmlContext(this);
+ if (context) {
+ QQmlEngine::setContextForObject(d->searchIndicator, context);
+ QQmlEngine::setContextForObject(d->clearIndicator, context);
+ }
+}
+
+void QQuickSearchField::componentComplete()
+{
+ Q_D(QQuickSearchField);
+ QQuickIndicatorButtonPrivate::get(d->searchIndicator)->executeIndicator(true);
+ QQuickIndicatorButtonPrivate::get(d->clearIndicator)->executeIndicator(true);
+ QQuickControl::componentComplete();
+
+ if (d->popup)
+ d->executePopup(true);
+
+ if (d->delegateModel && d->ownModel)
+ static_cast<QQmlDelegateModel *>(d->delegateModel)->componentComplete();
+
+ if (suggestionCount() > 0) {
+ if (!d->hasCurrentIndex && d->currentIndex == -1)
+ setCurrentIndex(0);
+ }
+}
+
+void QQuickSearchField::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
+{
+ Q_D(QQuickSearchField);
+ if (oldItem) {
+ oldItem->removeEventFilter(this);
+ if (QQuickTextInput *oldInput = qobject_cast<QQuickTextInput *>(oldItem)) {
+ QObjectPrivate::disconnect(oldInput, &QQuickTextInput::textChanged, d,
+ &QQuickSearchFieldPrivate::updateText);
+ }
+ }
+
+ if (newItem) {
+ newItem->installEventFilter(this);
+ if (QQuickTextInput *newInput = qobject_cast<QQuickTextInput *>(newItem)) {
+ QObjectPrivate::connect(newInput, &QQuickTextInput::textChanged, d,
+ &QQuickSearchFieldPrivate::updateText);
+ }
+ #if QT_CONFIG(cursor)
+ newItem->setCursor(Qt::IBeamCursor);
+ #endif
+ }
+}
+
+void QQuickSearchField::itemChange(ItemChange change, const ItemChangeData &data)
+{
+ Q_D(QQuickSearchField);
+ QQuickControl::itemChange(change, data);
+ if (change == ItemVisibleHasChanged && !data.boolValue) {
+ d->hidePopup();
+ // TO-DO: CHECK When the popup isn't visible, there shouldn't be any current item
+ d->setCurrentIndex(-1);
+ }
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qquicksearchfield_p.cpp"
diff --git a/src/quicktemplates/qquicksearchfield_p.h b/src/quicktemplates/qquicksearchfield_p.h
new file mode 100644
index 0000000000..89fcd6dda3
--- /dev/null
+++ b/src/quicktemplates/qquicksearchfield_p.h
@@ -0,0 +1,115 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKSEARCHFIELD_P_H
+#define QQUICKSEARCHFIELD_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQuickTemplates2/private/qquickcontrol_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQuickSearchFieldPrivate;
+class QQuickTextField;
+class QQuickPopup;
+class QQmlInstanceModel;
+class QQuickIndicatorButton;
+
+class Q_QUICKTEMPLATES2_EXPORT QQuickSearchField : public QQuickControl
+{
+ Q_OBJECT
+ Q_PROPERTY(QVariant suggestionModel READ suggestionModel WRITE setSuggestionModel
+ NOTIFY suggestionModelChanged FINAL)
+ Q_PROPERTY(QQmlInstanceModel *delegateModel READ delegateModel NOTIFY delegateModelChanged FINAL)
+ Q_PROPERTY(int suggestionCount READ suggestionCount NOTIFY suggestionCountChanged FINAL)
+ Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged
+ FINAL)
+ Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
+ Q_PROPERTY(QString textRole READ textRole WRITE setTextRole NOTIFY textRoleChanged FINAL)
+ Q_PROPERTY(bool live READ isLive WRITE setLive NOTIFY liveChanged)
+ Q_PROPERTY(QQuickIndicatorButton *searchIndicator READ searchIndicator CONSTANT FINAL)
+ Q_PROPERTY(QQuickIndicatorButton *clearIndicator READ clearIndicator CONSTANT FINAL)
+ Q_PROPERTY(QQuickPopup *popup READ popup WRITE setPopup NOTIFY popupChanged FINAL)
+ Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL)
+
+ QML_NAMED_ELEMENT(SearchField)
+ QML_ADDED_IN_VERSION(6, 10)
+
+public:
+ explicit QQuickSearchField(QQuickItem *parent = nullptr);
+ ~QQuickSearchField();
+
+ QVariant suggestionModel() const;
+ void setSuggestionModel(const QVariant &model);
+
+ QQmlInstanceModel *delegateModel() const;
+
+ int suggestionCount() const;
+
+ int currentIndex() const;
+ void setCurrentIndex(int index);
+
+ QString text() const;
+ void setText(const QString &text);
+
+ QString textRole() const;
+ void setTextRole(const QString &textRole);
+
+ bool isLive() const;
+ void setLive(const bool live);
+
+ QQuickIndicatorButton *searchIndicator() const;
+ QQuickIndicatorButton *clearIndicator() const;
+
+ QQuickPopup *popup() const;
+ void setPopup(QQuickPopup *popup);
+
+ QQmlComponent *delegate() const;
+ void setDelegate(QQmlComponent *delegate);
+
+Q_SIGNALS:
+ void activated(int index);
+ void accepted();
+ void searchTriggered();
+ void textEdited();
+ void suggestionModelChanged();
+ void delegateModelChanged();
+ void suggestionCountChanged();
+ void currentIndexChanged();
+ void textChanged();
+ void textRoleChanged();
+ void liveChanged();
+ void popupChanged();
+ void delegateChanged();
+
+ void searchButtonPressed();
+ void clearButtonPressed();
+
+protected:
+ bool eventFilter(QObject *object, QEvent *event) override;
+ void focusInEvent(QFocusEvent *event) override;
+ void focusOutEvent(QFocusEvent *event) override;
+ void keyPressEvent(QKeyEvent *event) override;
+ void classBegin() override;
+ void componentComplete() override;
+ void contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) override;
+ void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) override;
+
+private:
+ Q_DISABLE_COPY(QQuickSearchField)
+ Q_DECLARE_PRIVATE(QQuickSearchField)
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKSEARCHFIELD_P_H
diff --git a/tests/auto/quickcontrols/controls/data/tst_searchfield.qml b/tests/auto/quickcontrols/controls/data/tst_searchfield.qml
new file mode 100644
index 0000000000..b29bad95a0
--- /dev/null
+++ b/tests/auto/quickcontrols/controls/data/tst_searchfield.qml
@@ -0,0 +1,230 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Window
+import QtTest
+import QtQuick.Controls
+import Qt.test.controls
+
+TestCase {
+ id: testCase
+ width: 400
+ height: 400
+ visible: true
+ when: windowShown
+ name: "SearchField"
+
+ Component {
+ id: signalSpy
+ SignalSpy { }
+ }
+
+ Component {
+ id: searchField
+ SearchField { }
+ }
+
+ Component {
+ id: searchText
+ SearchField {
+ TextField{ }
+ }
+ }
+
+ function init() {
+ failOnWarning(/.?/)
+ }
+
+ function test_defaults() {
+ let control = createTemporaryObject(searchField, testCase)
+ verify(control)
+
+ compare(control.suggestionModel, undefined)
+ compare(control.suggestionCount, 0)
+ compare(control.currentIndex, -1)
+ compare(control.text, "")
+ compare(control.textRole, "")
+ compare(control.live, true)
+ verify(control.delegate)
+ verify(control.popup)
+ }
+
+ // TO-DO: Implement SFPM logic after 6.10
+ // ListModel {
+ // id: specialCharModels
+ // ListElement { text: "こんにちは" }
+ // ListElement { text: "Pi: π (3.14)"; }
+ // ListElement { text: "Math: ∑ ∞ ≈"; }
+ // ListElement { text: "Emoji: 😃🎉🔥"; }
+ // ListElement { text: "Currency: € ¥ ₹ $"; }
+ // ListElement { text: "α β γ"; }
+ // ListElement { text: "Привет"; }
+ // ListElement { text: "مرحبًا"; }
+ // ListElement { text: "你好"; }
+ // ListElement { text: "שלום"; }
+ // ListElement { text: "Brackets: { [ ( < > ) ] }"; }
+ // }
+
+ // function test_specialCharacters() {
+ // let control = createTemporaryObject(searchField, testCase)
+ // verify(control)
+
+ // control.suggestionModel = specialCharModels
+ // let textItem = control.contentItem
+ // textItem.text = "e"
+
+ // compare(control.text, "e")
+ // compare(control.suggestionCount, 3)
+ // compare(control.currentIndex, 0)
+ // compare(control.popup.visible, true)
+
+ // textItem.text = "П"
+
+ // compare(control.text, "П")
+ // compare(control.suggestionCount, 1)
+ // compare(control.currentIndex, 0)
+ // compare(control.popup.visible, true)
+
+ // textItem.text = "🎉"
+
+ // compare(control.text, "🎉")
+ // compare(control.suggestionCount, 1)
+ // compare(control.currentIndex, 0)
+ // compare(control.popup.visible, true)
+ // }
+
+ ListModel {
+ id : fruitModel
+ ListElement { name: "Apple"; color: "green" }
+ ListElement { name: "Cherry"; color: "red" }
+ ListElement { name: "Banana"; color: "yellow" }
+ ListElement { name: "Orange"; color: "orange" }
+ ListElement { name: "WaterMelon"; color: "pink" }
+ }
+
+ function test_textRole() {
+ ignoreWarning(/Unable to assign QQmlDMAbstractItemModelData to QString/)
+ let control = createTemporaryObject(searchField, testCase)
+ verify(control)
+
+ control.suggestionModel = fruitModel
+ control.textRole = "name"
+
+ let textItem = control.contentItem
+ textItem.text = "a"
+
+ compare(control.text, "a")
+ compare(control.suggestionCount, 5)
+ compare(control.popup.visible,true)
+
+ control.textRole = "color"
+
+ textItem.text = "r"
+
+ compare(control.text, "r")
+ compare(control.suggestionCount, 5)
+ compare(control.popup.visible,true)
+ }
+
+ Component {
+ id: suggestion
+ SearchField {
+ onTextEdited: {
+ if (text === "a") {
+ suggestionModel = ["Apple", "Apricot"]
+ } else if (text === "c") {
+ suggestionModel = ["Cherry", "Coconut", "Cranberry"]
+ }
+ }
+ }
+ }
+
+ function test_suggestionPopup() {
+ let control = createTemporaryObject(suggestion, testCase)
+ verify(control)
+
+ compare(control.popup.visible, false)
+
+ let textItem = control.contentItem
+
+ textItem.text = "a"
+ compare(control.suggestionCount, 2)
+ compare(control.currentIndex, 0)
+ compare(control.popup.visible, true)
+
+ textItem.text = "c"
+ compare(control.suggestionCount, 3)
+ compare(control.currentIndex, 0)
+ compare(control.popup.visible, true)
+ }
+
+ function test_textEdited() {
+ let control = createTemporaryObject(searchField, testCase)
+ verify(control)
+
+ let textEditedSpy = signalSpy.createObject(control, {target: control, signalName: "textEdited"})
+ verify(textEditedSpy.valid)
+
+ let searchTriggeredSpy = signalSpy.createObject(control, {target: control, signalName: "searchTriggered"})
+ verify(searchTriggeredSpy.valid)
+
+ control.live = true
+ let textItem = control.contentItem
+ textItem.text = "a"
+
+ compare(control.text, "a")
+ compare(textEditedSpy.count, 1)
+
+ compare(searchTriggeredSpy.count, 1)
+ }
+
+ function test_arrowKeys() {
+ ignoreWarning(/Unable to assign QQmlDMAbstractItemModelData to QString/)
+ let control = createTemporaryObject(searchField, testCase)
+ verify(control)
+
+ let openedSpy = signalSpy.createObject(control, {target: control.popup, signalName: "opened"})
+ verify(openedSpy.valid)
+
+ let closedSpy = signalSpy.createObject(control, {target: control.popup, signalName: "closed"})
+ verify(closedSpy.valid)
+
+ let acceptedSpy = signalSpy.createObject(control, {target: control, signalName: "accepted"})
+ verify(closedSpy.valid)
+
+ let searchTriggeredSpy = signalSpy.createObject(control, {target: control, signalName: "searchTriggered"})
+ verify(searchTriggeredSpy.valid)
+
+ control.forceActiveFocus()
+ verify(control.activeFocus)
+
+ control.suggestionModel = fruitModel
+ control.textRole = "name"
+
+ let textItem = control.contentItem
+ textItem.text = "a"
+
+ compare(control.popup.visible, true)
+
+ keyClick(Qt.Key_Down)
+ compare(control.currentIndex, 1)
+
+ keyClick(Qt.Key_Down)
+ compare(control.currentIndex, 2)
+
+ keyClick(Qt.Key_Up)
+ compare(control.currentIndex, 1)
+
+ keyClick(Qt.Key_Enter)
+ compare(control.text, "Cherry")
+ compare(acceptedSpy.count, 1)
+ compare(searchTriggeredSpy.count, 2)
+
+ keyClick(Qt.Key_Back)
+ compare(control.popup.visible, false)
+
+ keyClick(Qt.Key_Escape)
+ compare(control.text, "")
+ }
+}
diff --git a/tests/baseline/controls/data/searchfield/searchfield.qml b/tests/baseline/controls/data/searchfield/searchfield.qml
new file mode 100644
index 0000000000..a75ce701ec
--- /dev/null
+++ b/tests/baseline/controls/data/searchfield/searchfield.qml
@@ -0,0 +1,84 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Proxy 1.0
+
+GridLayout{
+ width: 500
+ height: 300
+ anchors.fill: parent
+ rows: 4
+ flow: GridLayout.TopToBottom
+
+ SearchField {
+ live: false
+ }
+
+ // TO-DO: Add a test case for autoSuggest property
+ // SearchField {
+ // autoSuggest: true
+ // }
+
+ SearchField {
+ suggestionModel: ListModel {
+ ListElement { color: "blue" }
+ ListElement { color: "green" }
+ ListElement { color: "red" }
+ ListElement { color: "yellow" }
+ ListElement { color: "orange" }
+ ListElement { color: "purple" }
+ ListElement { color: "cyan" }
+ ListElement { color: "magenta" }
+ ListElement { color: "chartreuse" }
+ ListElement { color: "aquamarine" }
+ ListElement { color: "indigo" }
+ ListElement { color: "black" }
+ ListElement { color: "lightsteelblue" }
+ ListElement { color: "violet" }
+ ListElement { color: "grey" }
+ ListElement { color: "springgreen" }
+ ListElement { color: "salmon" }
+ ListElement { color: "blanchedalmond" }
+ ListElement { color: "forestgreen" }
+ ListElement { color: "pink" }
+ ListElement { color: "navy" }
+ ListElement { color: "goldenrod" }
+ ListElement { color: "crimson" }
+ ListElement { color: "turquoise" }
+ }
+ }
+
+ SearchField {
+ suggestionModel: ["January", "February", "March", "April", "May", "June", "July", "August",
+ "September", "October", "November", "December"]
+ }
+
+ SearchField {
+ suggestionModel: ListModel {
+ ListElement { name: "Apple"; color: "green" }
+ ListElement { name: "Cherry"; color: "red" }
+ ListElement { name: "Banana"; color: "yellow" }
+ ListElement { name: "Orange"; color: "orange" }
+ ListElement { name: "WaterMelon"; color: "pink" }
+ }
+ textRole: "color"
+ }
+
+ SearchField {
+ id: searchField
+ suggestionModel: QSortFilterProxyModel {
+ id: colorModel
+ sourceModel: ListModel {
+ ListElement { color: "blue" }
+ ListElement { color: "green" }
+ ListElement { color: "red" }
+ ListElement { color: "yellow" }
+ ListElement { color: "orange" }
+ ListElement { color: "purple" }
+ }
+ }
+ onTextChanged: {
+ colorModel.filterRegularExpression = new RegExp(searchField.text, "i")
+ }
+ }
+}
diff --git a/tests/baseline/controls/tst_baseline_controls.cpp b/tests/baseline/controls/tst_baseline_controls.cpp
index 485510a9dd..8604ddf323 100644
--- a/tests/baseline/controls/tst_baseline_controls.cpp
+++ b/tests/baseline/controls/tst_baseline_controls.cpp
@@ -11,6 +11,8 @@
#include <QtGui/QPalette>
#include <QtGui/QFont>
#include <QtQuickControls2/QQuickStyle>
+#include <QSortFilterProxyModel>
+#include <QQmlApplicationEngine>
#include <algorithm>
@@ -153,6 +155,9 @@ void tst_Baseline_Controls::initTestCase()
qInfo("PlatformName computed to be : %s", qPrintable(platformName));
qInfo("Color Scheme computed as : %s", qPrintable(colorSchemeIdStr));
qInfo("Native style name is : %s", qPrintable(QQuickStyle::name()));
+
+ qmlRegisterAnonymousType<QAbstractItemModel>("Proxy", 1);
+ qmlRegisterType<QSortFilterProxyModel>("Proxy", 1, 0, "QSortFilterProxyModel");
}
void tst_Baseline_Controls::init()