aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
8 files changed, 1290 insertions, 0 deletions
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