diff options
author | Dilek Akcay <[email protected]> | 2024-12-30 16:14:56 +0100 |
---|---|---|
committer | Jan Arve Sæther <[email protected]> | 2025-06-01 20:21:39 +0000 |
commit | 857616614527f41829f1f0e9fd3e147a76ccf74d (patch) | |
tree | 898735498bb97e22f79810c24586d75aada52ddf | |
parent | 5f9eafcf3999829aca39ba233aadb74bfa264beb (diff) |
Add a new QQuickSearchField as part of the Qt Quick Controls to
simplify implementing search functionality for lists of items.
Task-number: QTBUG-126188
Change-Id: I634131161447616a2d66e7f301bd8a24adac2d7f
Reviewed-by: Jan Arve Sæther <[email protected]>
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/[email protected]" "images/[email protected]" "images/[email protected]" + "images/close_circle.png" "images/dial-indicator.png" "images/[email protected]" "images/[email protected]" @@ -222,6 +224,7 @@ set(qtquickcontrols2basicstyle_resource_files "images/[email protected]" "images/[email protected]" "images/[email protected]" + "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 Binary files differnew file mode 100644 index 0000000000..4b2644d4f5 --- /dev/null +++ b/src/quickcontrols/basic/images/close_circle.png diff --git a/src/quickcontrols/basic/images/search-magnifier.png b/src/quickcontrols/basic/images/search-magnifier.png Binary files differnew file mode 100644 index 0000000000..1bd4da97a0 --- /dev/null +++ b/src/quickcontrols/basic/images/search-magnifier.png diff --git a/src/quickcontrols/doc/images/qtquickcontrols-searchfield.gif b/src/quickcontrols/doc/images/qtquickcontrols-searchfield.gif Binary files differnew file mode 100644 index 0000000000..c323767bd0 --- /dev/null +++ b/src/quickcontrols/doc/images/qtquickcontrols-searchfield.gif 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() |