diff options
Diffstat (limited to 'src/quicktemplates/qquickcombobox.cpp')
-rw-r--r-- | src/quicktemplates/qquickcombobox.cpp | 2245 |
1 files changed, 2245 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickcombobox.cpp b/src/quicktemplates/qquickcombobox.cpp new file mode 100644 index 0000000000..c55e988b74 --- /dev/null +++ b/src/quicktemplates/qquickcombobox.cpp @@ -0,0 +1,2245 @@ +// Copyright (C) 2017 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 "qquickcombobox_p.h" +#include "qquickcontrol_p_p.h" +#include "qquickabstractbutton_p.h" +#include "qquickabstractbutton_p_p.h" +#include "qquickpopup_p_p.h" +#include "qquickdeferredexecute_p_p.h" + +#include <QtCore/qregularexpression.h> +#include <QtCore/qabstractitemmodel.h> +#include <QtCore/qglobal.h> +#include <QtGui/qinputmethod.h> +#include <QtGui/qguiapplication.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> +#include <QtQml/qjsvalue.h> +#include <QtQml/qqmlcontext.h> +#include <QtQml/private/qlazilyallocated_p.h> +#include <private/qqmldelegatemodel_p.h> +#include <QtQuick/private/qquickaccessibleattached_p.h> +#include <QtQuick/private/qquickevents_p_p.h> +#include <QtQuick/private/qquicktextinput_p.h> +#include <QtQuick/private/qquicktextinput_p_p.h> +#if QT_CONFIG(quick_itemview) +#include <QtQuick/private/qquickitemview_p.h> +#endif + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcCalculateWidestTextWidth, "qt.quick.controls.combobox.calculatewidesttextwidth") + +/*! + \qmltype ComboBox + \inherits Control +//! \instantiates QQuickComboBox + \inqmlmodule QtQuick.Controls + \since 5.7 + \ingroup qtquickcontrols-input + \ingroup qtquickcontrols-focusscopes + \brief Combined button and popup list for selecting options. + + \image qtquickcontrols-combobox.gif + + ComboBox is a combined button and popup list. It provides a means of + presenting a list of options to the user in a way that takes up the + minimum amount of screen space. + + ComboBox is populated with a data model. The data model is commonly + a JavaScript array, a \l ListModel or an integer, but other types + of \l {qml-data-models}{data models} are also supported. + + \code + ComboBox { + model: ["First", "Second", "Third"] + } + \endcode + + \section1 Editable ComboBox + + ComboBox can be made \l editable. An editable combo box auto-completes + its text based on what is available in the model. + + The following example demonstrates appending content to an editable + combo box by reacting to the \l accepted signal. + + \snippet qtquickcontrols-combobox-accepted.qml combobox + + \section1 ComboBox's Popup + + By default, clicking outside of ComboBox's popup will close it, and the + event is propagated to items lower in the stacking order. To prevent the + popup from closing, set its \l {Popup::}{closePolicy}: + + \snippet qtquickcontrols-combobox-popup.qml closePolicy + + To prevent event propagation, set its \l {Popup::}{modal} property to + \c true: + + \snippet qtquickcontrols-combobox-popup.qml modal + + \section1 ComboBox Model Roles + + ComboBox 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, ComboBox must be configured + to use a specific \l {textRole}{text role} for its \l {displayText}{display text} + and \l delegate instances. If you want to use a role of the model item + that corresponds to the text role, set \l valueRole. The \l currentValue + property and \l indexOfValue() method can then be used to get information + about those values. + + For example: + + \snippet qtquickcontrols-combobox-valuerole.qml file + + \note If ComboBox is assigned a data model that has multiple named roles, but + \l textRole is not defined, ComboBox is unable to visualize it and throws a + \c {ReferenceError: modelData is not defined}. + + \sa {Customizing ComboBox}, {Input Controls}, {Focus Management in Qt Quick Controls} +*/ + +/*! + \qmlsignal void QtQuick.Controls::ComboBox::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), + or while the popup is closed and the combo box is navigated via + keyboard, causing the \l currentIndex to change. + The \l currentIndex property is set to \a index. + + \sa currentIndex +*/ + +/*! + \qmlsignal void QtQuick.Controls::ComboBox::highlighted(int index) + + This signal is emitted when the item at \a index in the popup list is highlighted by the user. + + The highlighted signal is only emitted when the popup is open and an item + is highlighted, but not necessarily \l activated. + + \sa highlightedIndex +*/ + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlsignal void QtQuick.Controls::ComboBox::accepted() + + This signal is emitted when the \uicontrol Return or \uicontrol Enter key is pressed + on an \l editable combo box. + + You can handle this signal in order to add the newly entered + item to the model, for example: + + \snippet qtquickcontrols-combobox-accepted.qml combobox + + Before the signal is emitted, a check is done to see if the string + exists in the model. If it does, \l currentIndex will be set to its index, + and \l currentText to the string itself. + + After the signal has been emitted, and if the first check failed (that is, + the item did not exist), another check will be done to see if the item was + added by the signal handler. If it was, the \l currentIndex and + \l currentText are updated accordingly. Otherwise, they will be set to + \c -1 and \c "", respectively. + + \note If there is a \l validator set on the combo box, the signal will only be + emitted if the input is in an acceptable state. +*/ + +namespace { + enum Activation { NoActivate, Activate }; + enum Highlighting { NoHighlight, Highlight }; +} + +// ### Qt7: Remove this class. Use QQmlDelegateModel instead. +class QQuickComboBoxDelegateModel : public QQmlDelegateModel +{ +public: + explicit QQuickComboBoxDelegateModel(QQuickComboBox *combo); + QVariant variantValue(int index, const QString &role) override; + +private: + QQuickComboBox *combo = nullptr; +}; + +QQuickComboBoxDelegateModel::QQuickComboBoxDelegateModel(QQuickComboBox *combo) + : QQmlDelegateModel(qmlContext(combo), combo), + combo(combo) +{ +} + +QVariant QQuickComboBoxDelegateModel::variantValue(int index, const QString &role) +{ + // ### Qt7: Get rid of this. Why do we special case lists of variant maps with + // exactly one entry? There are many other ways of producing a list of + // map-like things with exactly one entry. And what if some of the maps + // in the list have more than one entry? You get inconsistent results. + if (role == QLatin1String("modelData")) { + const QVariant model = combo->model(); + if (model.metaType() == QMetaType::fromType<QVariantList>()) { + const QVariant object = model.toList().value(index); + if (object.metaType() == QMetaType::fromType<QVariantMap>()) { + const QVariantMap data = object.toMap(); + if (data.size() == 1) + return data.first(); + } + } + } + + return QQmlDelegateModel::variantValue(index, role); +} + +class QQuickComboBoxPrivate : public QQuickControlPrivate +{ +public: + Q_DECLARE_PUBLIC(QQuickComboBox) + + bool isPopupVisible() const; + void showPopup(); + void hidePopup(bool accept); + void togglePopup(bool accept); + void popupVisibleChanged(); + void popupDestroyed(); + + void itemClicked(); + void itemHovered(); + + void createdItem(int index, QObject *object); + void modelUpdated(); + void countChanged(); + + QString effectiveTextRole() const; + void updateEditText(); + void updateCurrentText(); + void updateCurrentValue(); + void updateCurrentTextAndValue(); + void updateAcceptableInput(); + + bool isValidIndex(int index) const; + + void acceptInput(); + QString tryComplete(const QString &inputText); + + void incrementCurrentIndex(); + void decrementCurrentIndex(); + void setCurrentIndex(int index, Activation activate); + void updateHighlightedIndex(); + void setHighlightedIndex(int index, Highlighting highlight); + + void keySearch(const QString &text); + int match(int start, const QString &text, Qt::MatchFlags flags) const; + + void createDelegateModel(); + + bool handlePress(const QPointF &point, ulong timestamp) override; + bool handleMove(const QPointF &point, ulong timestamp) override; + bool handleRelease(const QPointF &point, ulong timestamp) override; + void handleUngrab() override; + + void cancelIndicator(); + void executeIndicator(bool complete = false); + + void cancelPopup(); + void executePopup(bool complete = false); + + void itemImplicitWidthChanged(QQuickItem *item) override; + void itemImplicitHeightChanged(QQuickItem *item) override; + void itemDestroyed(QQuickItem *item) override; + + void setInputMethodHints(Qt::InputMethodHints hints, bool force = false); + + virtual qreal getContentWidth() const override; + qreal calculateWidestTextWidth() const; + void maybeUpdateImplicitContentWidth(); + + static void hideOldPopup(QQuickPopup *popup); + + QPalette defaultPalette() const override { return QQuickTheme::palette(QQuickTheme::ComboBox); } + + bool flat = false; + bool down = false; + bool hasDown = false; + bool pressed = false; + bool ownModel = false; + bool keyNavigating = false; + bool hasDisplayText = false; + bool hasCurrentIndex = false; + bool hasCalculatedWidestText = false; + int highlightedIndex = -1; + int currentIndex = -1; + QQuickComboBox::ImplicitContentWidthPolicy implicitContentWidthPolicy = QQuickComboBox::ContentItemImplicitWidth; + QVariant model; + QString textRole; + QString currentText; + QString displayText; + QString valueRole; + QVariant currentValue; + QQuickItem *pressedItem = nullptr; + QQmlInstanceModel *delegateModel = nullptr; + QQmlComponent *delegate = nullptr; + QQuickDeferredPointer<QQuickItem> indicator; + QQuickDeferredPointer<QQuickPopup> popup; + bool m_acceptableInput = true; + + struct ExtraData { + bool editable = false; + bool accepting = false; + bool allowComplete = false; + bool selectTextByMouse = false; + Qt::InputMethodHints inputMethodHints = Qt::ImhNone; + QString editText; +#if QT_CONFIG(validator) + QValidator *validator = nullptr; +#endif + }; + QLazilyAllocated<ExtraData> extra; +}; + +bool QQuickComboBoxPrivate::isPopupVisible() const +{ + return popup && popup->isVisible(); +} + +void QQuickComboBoxPrivate::showPopup() +{ + if (!popup) + executePopup(true); + + if (popup && !popup->isVisible()) + popup->open(); +} + +void QQuickComboBoxPrivate::hidePopup(bool accept) +{ + Q_Q(QQuickComboBox); + if (accept) { + q->setCurrentIndex(highlightedIndex); + emit q->activated(currentIndex); + } + if (popup && popup->isVisible()) + popup->close(); +} + +void QQuickComboBoxPrivate::togglePopup(bool accept) +{ + if (!popup || !popup->isVisible()) + showPopup(); + else + hidePopup(accept); +} + +void QQuickComboBoxPrivate::popupVisibleChanged() +{ + Q_Q(QQuickComboBox); + if (isPopupVisible()) + QGuiApplication::inputMethod()->reset(); + +#if QT_CONFIG(quick_itemview) + QQuickItemView *itemView = popup->findChild<QQuickItemView *>(); + if (itemView) + itemView->setHighlightRangeMode(QQuickItemView::NoHighlightRange); +#endif + + updateHighlightedIndex(); + +#if QT_CONFIG(quick_itemview) + if (itemView) + itemView->positionViewAtIndex(highlightedIndex, QQuickItemView::Beginning); +#endif + + if (!hasDown) { + q->setDown(pressed || isPopupVisible()); + hasDown = false; + } +} + +void QQuickComboBoxPrivate::popupDestroyed() +{ + Q_Q(QQuickComboBox); + popup = nullptr; + emit q->popupChanged(); +} + +void QQuickComboBoxPrivate::itemClicked() +{ + Q_Q(QQuickComboBox); + int index = delegateModel->indexOf(q->sender(), nullptr); + if (index != -1) { + setHighlightedIndex(index, Highlight); + hidePopup(true); + } +} + +void QQuickComboBoxPrivate::itemHovered() +{ + Q_Q(QQuickComboBox); + if (keyNavigating) + return; + + 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) { + setHighlightedIndex(index, Highlight); + +#if QT_CONFIG(quick_itemview) + if (QQuickItemView *itemView = popup->findChild<QQuickItemView *>()) + itemView->positionViewAtIndex(index, QQuickItemView::Contain); +#endif + } +} + +void QQuickComboBoxPrivate::createdItem(int index, QObject *object) +{ + Q_Q(QQuickComboBox); + 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, &QQuickComboBoxPrivate::itemClicked); + connect(button, &QQuickAbstractButton::hoveredChanged, this, &QQuickComboBoxPrivate::itemHovered); + } + + if (index == currentIndex && !q->isEditable()) + updateCurrentTextAndValue(); +} + +void QQuickComboBoxPrivate::modelUpdated() +{ + if (componentComplete && (!extra.isAllocated() || !extra->accepting)) { + updateCurrentTextAndValue(); + + if (implicitContentWidthPolicy == QQuickComboBox::WidestText) + updateImplicitContentSize(); + } +} + +void QQuickComboBoxPrivate::countChanged() +{ + Q_Q(QQuickComboBox); + if (q->count() == 0) + q->setCurrentIndex(-1); + emit q->countChanged(); +} + +QString QQuickComboBoxPrivate::effectiveTextRole() const +{ + return textRole.isEmpty() ? QStringLiteral("modelData") : textRole; +} + +void QQuickComboBoxPrivate::updateEditText() +{ + Q_Q(QQuickComboBox); + QQuickTextInput *input = qobject_cast<QQuickTextInput *>(contentItem); + if (!input) + return; + + const QString text = input->text(); + + if (extra.isAllocated() && extra->allowComplete && !text.isEmpty()) { + const QString completed = tryComplete(text); + if (completed.size() > text.size()) { + input->setText(completed); + // This will select the text backwards. + input->select(completed.size(), text.size()); + return; + } + } + q->setEditText(text); +} + +void QQuickComboBoxPrivate::updateCurrentText() +{ + Q_Q(QQuickComboBox); + const QString text = q->textAt(currentIndex); + if (currentText != text) { + currentText = text; + if (!hasDisplayText) + q->maybeSetAccessibleName(text); + emit q->currentTextChanged(); + } + if (!hasDisplayText && displayText != text) { + displayText = text; + emit q->displayTextChanged(); + } + if (!extra.isAllocated() || !extra->accepting) + q->setEditText(currentText); +} + +void QQuickComboBoxPrivate::updateCurrentValue() +{ + Q_Q(QQuickComboBox); + const QVariant value = q->valueAt(currentIndex); + if (currentValue == value) + return; + + currentValue = value; + emit q->currentValueChanged(); +} + +void QQuickComboBoxPrivate::updateCurrentTextAndValue() +{ + updateCurrentText(); + updateCurrentValue(); +} + +void QQuickComboBoxPrivate::updateAcceptableInput() +{ + Q_Q(QQuickComboBox); + + if (!contentItem) + return; + + const QQuickTextInput *textInputContentItem = qobject_cast<QQuickTextInput *>(contentItem); + + if (!textInputContentItem) + return; + + const bool newValue = textInputContentItem->hasAcceptableInput(); + + if (m_acceptableInput != newValue) { + m_acceptableInput = newValue; + emit q->acceptableInputChanged(); + } +} + +bool QQuickComboBoxPrivate::isValidIndex(int index) const +{ + return delegateModel && index >= 0 && index < delegateModel->count(); +} + +void QQuickComboBoxPrivate::acceptInput() +{ + Q_Q(QQuickComboBox); + int idx = q->find(extra.value().editText, Qt::MatchFixedString); + if (idx > -1) { + // The item that was accepted already exists, so make it the current item. + q->setCurrentIndex(idx); + // After accepting text that matches an existing entry, the selection should be cleared. + QQuickTextInput *input = qobject_cast<QQuickTextInput *>(contentItem); + if (input) { + const auto text = input->text(); + input->select(text.size(), text.size()); + } + } + + extra.value().accepting = true; + emit q->accepted(); + + // The user might have added the item since it didn't exist, so check again + // to see if we can select that new item. + if (idx == -1) + q->setCurrentIndex(q->find(extra.value().editText, Qt::MatchFixedString)); + extra.value().accepting = false; +} + +QString QQuickComboBoxPrivate::tryComplete(const QString &input) +{ + Q_Q(QQuickComboBox); + QString match; + + const int itemCount = q->count(); + for (int idx = 0; idx < itemCount; ++idx) { + const QString text = q->textAt(idx); + if (!text.startsWith(input, Qt::CaseInsensitive)) + continue; + + // either the first or the shortest match + if (match.isEmpty() || text.size() < match.size()) + match = text; + } + + if (match.isEmpty()) + return input; + + return input + match.mid(input.size()); +} + +void QQuickComboBoxPrivate::setCurrentIndex(int index, Activation activate) +{ + Q_Q(QQuickComboBox); + if (currentIndex == index) + return; + + currentIndex = index; + emit q->currentIndexChanged(); + + if (componentComplete) + updateCurrentTextAndValue(); + + if (activate) + emit q->activated(index); +} + +void QQuickComboBoxPrivate::incrementCurrentIndex() +{ + Q_Q(QQuickComboBox); + if (extra.isAllocated()) + extra->allowComplete = false; + if (isPopupVisible()) { + if (highlightedIndex < q->count() - 1) + setHighlightedIndex(highlightedIndex + 1, Highlight); + } else { + if (currentIndex < q->count() - 1) + setCurrentIndex(currentIndex + 1, Activate); + } + if (extra.isAllocated()) + extra->allowComplete = true; +} + +void QQuickComboBoxPrivate::decrementCurrentIndex() +{ + if (extra.isAllocated()) + extra->allowComplete = false; + if (isPopupVisible()) { + if (highlightedIndex > 0) + setHighlightedIndex(highlightedIndex - 1, Highlight); + } else { + if (currentIndex > 0) + setCurrentIndex(currentIndex - 1, Activate); + } + if (extra.isAllocated()) + extra->allowComplete = true; +} + +void QQuickComboBoxPrivate::updateHighlightedIndex() +{ + setHighlightedIndex(popup->isVisible() ? currentIndex : -1, NoHighlight); +} + +void QQuickComboBoxPrivate::setHighlightedIndex(int index, Highlighting highlight) +{ + Q_Q(QQuickComboBox); + if (highlightedIndex == index) + return; + + highlightedIndex = index; + emit q->highlightedIndexChanged(); + + if (highlight) + emit q->highlighted(index); +} + +void QQuickComboBoxPrivate::keySearch(const QString &text) +{ + const int startIndex = isPopupVisible() ? highlightedIndex : currentIndex; + const int index = match(startIndex + 1, text, Qt::MatchStartsWith | Qt::MatchWrap); + if (index != -1) { + if (isPopupVisible()) + setHighlightedIndex(index, Highlight); + else + setCurrentIndex(index, Activate); + } +} + +int QQuickComboBoxPrivate::match(int start, const QString &text, Qt::MatchFlags flags) const +{ + Q_Q(const QQuickComboBox); + uint matchType = flags & 0x0F; + bool wrap = flags & Qt::MatchWrap; + Qt::CaseSensitivity cs = flags & Qt::MatchCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; + QRegularExpression::PatternOptions options = flags & Qt::MatchCaseSensitive ? QRegularExpression::NoPatternOption + : QRegularExpression::CaseInsensitiveOption; + int from = start; + int to = q->count(); + + // iterates twice if wrapping + for (int i = 0; (wrap && i < 2) || (!wrap && i < 1); ++i) { + for (int idx = from; idx < to; ++idx) { + QString t = q->textAt(idx); + switch (matchType) { + case Qt::MatchExactly: + if (t == text) + return idx; + break; + case Qt::MatchRegularExpression: { + QRegularExpression rx(QRegularExpression::anchoredPattern(text), options); + if (rx.match(t).hasMatch()) + return idx; + break; + } + case Qt::MatchWildcard: { + QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(text), + options); + if (rx.match(t).hasMatch()) + return idx; + break; + } + case Qt::MatchStartsWith: + if (t.startsWith(text, cs)) + return idx; + break; + case Qt::MatchEndsWith: + if (t.endsWith(text, cs)) + return idx; + break; + case Qt::MatchFixedString: + if (t.compare(text, cs) == 0) + return idx; + break; + case Qt::MatchContains: + default: + if (t.contains(text, cs)) + return idx; + break; + } + } + // prepare for the next iteration + from = 0; + to = start; + } + return -1; +} + +void QQuickComboBoxPrivate::createDelegateModel() +{ + Q_Q(QQuickComboBox); + bool ownedOldModel = ownModel; + QQmlInstanceModel* oldModel = delegateModel; + if (oldModel) { + disconnect(delegateModel, &QQmlInstanceModel::countChanged, this, &QQuickComboBoxPrivate::countChanged); + disconnect(delegateModel, &QQmlInstanceModel::modelUpdated, this, &QQuickComboBoxPrivate::modelUpdated); + disconnect(delegateModel, &QQmlInstanceModel::createdItem, this, &QQuickComboBoxPrivate::createdItem); + } + + ownModel = false; + delegateModel = model.value<QQmlInstanceModel *>(); + + if (!delegateModel && model.isValid()) { + QQmlDelegateModel *dataModel = new QQuickComboBoxDelegateModel(q); + dataModel->setModel(model); + dataModel->setDelegate(delegate); + if (q->isComponentComplete()) + dataModel->componentComplete(); + + ownModel = true; + delegateModel = dataModel; + } + + if (delegateModel) { + connect(delegateModel, &QQmlInstanceModel::countChanged, this, &QQuickComboBoxPrivate::countChanged); + connect(delegateModel, &QQmlInstanceModel::modelUpdated, this, &QQuickComboBoxPrivate::modelUpdated); + connect(delegateModel, &QQmlInstanceModel::createdItem, this, &QQuickComboBoxPrivate::createdItem); + } + + emit q->delegateModelChanged(); + + if (ownedOldModel) + delete oldModel; +} + +bool QQuickComboBoxPrivate::handlePress(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::handlePress(point, timestamp); + q->setPressed(true); + return true; +} + +bool QQuickComboBoxPrivate::handleMove(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::handleMove(point, timestamp); + q->setPressed(q->contains(point)); + return true; +} + +bool QQuickComboBoxPrivate::handleRelease(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::handleRelease(point, timestamp); + if (pressed) { + q->setPressed(false); + togglePopup(false); + } + return true; +} + +void QQuickComboBoxPrivate::handleUngrab() +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::handleUngrab(); + q->setPressed(false); +} + +void QQuickComboBoxPrivate::cancelIndicator() +{ + Q_Q(QQuickComboBox); + quickCancelDeferred(q, indicatorName()); +} + +void QQuickComboBoxPrivate::executeIndicator(bool complete) +{ + Q_Q(QQuickComboBox); + if (indicator.wasExecuted()) + return; + + if (!indicator || complete) + quickBeginDeferred(q, indicatorName(), indicator); + if (complete) + quickCompleteDeferred(q, indicatorName(), indicator); +} + +static inline QString popupName() { return QStringLiteral("popup"); } + +void QQuickComboBoxPrivate::cancelPopup() +{ + Q_Q(QQuickComboBox); + quickCancelDeferred(q, popupName()); +} + +void QQuickComboBoxPrivate::executePopup(bool complete) +{ + Q_Q(QQuickComboBox); + if (popup.wasExecuted()) + return; + + if (!popup || complete) + quickBeginDeferred(q, popupName(), popup); + if (complete) + quickCompleteDeferred(q, popupName(), popup); +} + +void QQuickComboBoxPrivate::itemImplicitWidthChanged(QQuickItem *item) +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::itemImplicitWidthChanged(item); + if (item == indicator) + emit q->implicitIndicatorWidthChanged(); +} + +void QQuickComboBoxPrivate::setInputMethodHints(Qt::InputMethodHints hints, bool force) +{ + Q_Q(QQuickComboBox); + if (!force && hints == q->inputMethodHints()) + return; + + extra.value().inputMethodHints = hints; + emit q->inputMethodHintsChanged(); +} + +void QQuickComboBoxPrivate::itemImplicitHeightChanged(QQuickItem *item) +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::itemImplicitHeightChanged(item); + if (item == indicator) + emit q->implicitIndicatorHeightChanged(); +} + +void QQuickComboBoxPrivate::itemDestroyed(QQuickItem *item) +{ + Q_Q(QQuickComboBox); + QQuickControlPrivate::itemDestroyed(item); + if (item == indicator) { + indicator = nullptr; + emit q->indicatorChanged(); + } +} + +qreal QQuickComboBoxPrivate::getContentWidth() const +{ + if (componentComplete) { + switch (implicitContentWidthPolicy) { + case QQuickComboBox::WidestText: + return calculateWidestTextWidth(); + case QQuickComboBox::WidestTextWhenCompleted: + if (!hasCalculatedWidestText) + return calculateWidestTextWidth(); + break; + default: + break; + } + } + + return QQuickControlPrivate::getContentWidth(); +} + +qreal QQuickComboBoxPrivate::calculateWidestTextWidth() const +{ + Q_Q(const QQuickComboBox); + if (!componentComplete) + return 0; + + const int count = q->count(); + if (count == 0) + return 0; + + auto textInput = qobject_cast<QQuickTextInput*>(contentItem); + if (!textInput) + return 0; + + qCDebug(lcCalculateWidestTextWidth) << "calculating widest text from" << count << "items..."; + + // Avoid the index check and repeated calls to effectiveTextRole() + // that would result from calling textAt() in a loop. + const QString textRole = effectiveTextRole(); + auto textInputPrivate = QQuickTextInputPrivate::get(textInput); + qreal widest = 0; + for (int i = 0; i < count; ++i) { + const QString text = delegateModel->stringValue(i, textRole); + const qreal textImplicitWidth = textInputPrivate->calculateImplicitWidthForText(text); + widest = qMax(widest, textImplicitWidth); + } + + qCDebug(lcCalculateWidestTextWidth) << "... widest text is" << widest; + return widest; +} + +/*! + \internal + + If the user requested it (and we haven't already done it, depending on the policy), + update the implicit content width to the largest text in the model. +*/ +void QQuickComboBoxPrivate::maybeUpdateImplicitContentWidth() +{ + if (!componentComplete) + return; + + if (implicitContentWidthPolicy == QQuickComboBox::ContentItemImplicitWidth + || (implicitContentWidthPolicy == QQuickComboBox::WidestTextWhenCompleted && hasCalculatedWidestText)) + return; + + updateImplicitContentWidth(); + hasCalculatedWidestText = true; +} + +void QQuickComboBoxPrivate::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 +} + +QQuickComboBox::QQuickComboBox(QQuickItem *parent) + : QQuickControl(*(new QQuickComboBoxPrivate), parent) +{ + setFocusPolicy(Qt::StrongFocus); + setFlag(QQuickItem::ItemIsFocusScope); + setAcceptedMouseButtons(Qt::LeftButton); +#if QT_CONFIG(cursor) + setCursor(Qt::ArrowCursor); +#endif + Q_D(QQuickComboBox); + d->setInputMethodHints(Qt::ImhNoPredictiveText, true); + d->setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Fixed); +} + +QQuickComboBox::~QQuickComboBox() +{ + Q_D(QQuickComboBox); + d->removeImplicitSizeListener(d->indicator); + if (d->popup) { + // Disconnect visibleChanged() to avoid a spurious highlightedIndexChanged() signal + // emission during the destruction of the (visible) popup. (QTBUG-57650) + QObjectPrivate::disconnect(d->popup.data(), &QQuickPopup::visibleChanged, d, &QQuickComboBoxPrivate::popupVisibleChanged); + QQuickComboBoxPrivate::hideOldPopup(d->popup); + d->popup = nullptr; + } +} + +/*! + \readonly + \qmlproperty int QtQuick.Controls::ComboBox::count + + This property holds the number of items in the combo box. +*/ +int QQuickComboBox::count() const +{ + Q_D(const QQuickComboBox); + return d->delegateModel ? d->delegateModel->count() : 0; +} + +/*! + \qmlproperty model QtQuick.Controls::ComboBox::model + + This property holds the model providing data for the combo box. + + \code + ComboBox { + textRole: "key" + model: ListModel { + ListElement { key: "First"; value: 123 } + ListElement { key: "Second"; value: 456 } + ListElement { key: "Third"; value: 789 } + } + } + \endcode + + \sa textRole, {qml-data-models}{Data Models} +*/ +QVariant QQuickComboBox::model() const +{ + Q_D(const QQuickComboBox); + return d->model; +} + +void QQuickComboBox::setModel(const QVariant& m) +{ + Q_D(QQuickComboBox); + QVariant model = m; + if (model.userType() == qMetaTypeId<QJSValue>()) + model = model.value<QJSValue>().toVariant(); + + if (d->model == model) + return; + + if (QAbstractItemModel* aim = qvariant_cast<QAbstractItemModel *>(d->model)) { + QObjectPrivate::disconnect(aim, &QAbstractItemModel::dataChanged, + d, QOverload<>::of(&QQuickComboBoxPrivate::updateCurrentTextAndValue)); + } + if (QAbstractItemModel* aim = qvariant_cast<QAbstractItemModel *>(model)) { + QObjectPrivate::connect(aim, &QAbstractItemModel::dataChanged, + d, QOverload<>::of(&QQuickComboBoxPrivate::updateCurrentTextAndValue)); + } + + d->model = model; + d->createDelegateModel(); + emit countChanged(); + if (isComponentComplete()) { + setCurrentIndex(count() > 0 ? 0 : -1); + d->updateCurrentTextAndValue(); + } + emit modelChanged(); + + d->maybeUpdateImplicitContentWidth(); +} + +/*! + \internal + \qmlproperty model QtQuick.Controls::ComboBox::delegateModel + + This property holds the model providing delegate instances for the combo box. +*/ +QQmlInstanceModel *QQuickComboBox::delegateModel() const +{ + Q_D(const QQuickComboBox); + return d->delegateModel; +} + + +/*! + \readonly + \qmlproperty bool QtQuick.Controls::ComboBox::pressed + + This property holds whether the combo box button is physically pressed. + A button can be pressed by either touch or key events. + + \sa down +*/ +bool QQuickComboBox::isPressed() const +{ + Q_D(const QQuickComboBox); + return d->pressed; +} + +void QQuickComboBox::setPressed(bool pressed) +{ + Q_D(QQuickComboBox); + if (d->pressed == pressed) + return; + + d->pressed = pressed; + emit pressedChanged(); + + if (!d->hasDown) { + setDown(d->pressed || d->isPopupVisible()); + d->hasDown = false; + } +} + +/*! + \readonly + \qmlproperty int QtQuick.Controls::ComboBox::highlightedIndex + + This property holds the index of the highlighted item in the combo box popup list. + + When a highlighted item is activated, the popup is closed, \l currentIndex + is set to \c highlightedIndex, and the value of this property is reset to + \c -1, as there is no longer a highlighted item. + + \sa highlighted(), currentIndex +*/ +int QQuickComboBox::highlightedIndex() const +{ + Q_D(const QQuickComboBox); + return d->highlightedIndex; +} + +/*! + \qmlproperty int QtQuick.Controls::ComboBox::currentIndex + + This property holds the index of the current item in the combo box. + + The default value is \c -1 when \l count is \c 0, and \c 0 otherwise. + + \sa activated(), currentText, highlightedIndex +*/ +int QQuickComboBox::currentIndex() const +{ + Q_D(const QQuickComboBox); + return d->currentIndex; +} + +void QQuickComboBox::setCurrentIndex(int index) +{ + Q_D(QQuickComboBox); + d->hasCurrentIndex = true; + d->setCurrentIndex(index, NoActivate); +} + +/*! + \readonly + \qmlproperty string QtQuick.Controls::ComboBox::currentText + + This property holds the text of the current item in the combo box. + + \sa currentIndex, displayText, textRole, editText +*/ +QString QQuickComboBox::currentText() const +{ + Q_D(const QQuickComboBox); + return d->currentText; +} + +/*! + \qmlproperty string QtQuick.Controls::ComboBox::displayText + + This property holds the text that is displayed on the combo box button. + + By default, the display text presents the current selection. That is, + it follows the text of the current item. However, the default display + text can be overridden with a custom value. + + \code + ComboBox { + currentIndex: 1 + displayText: "Size: " + currentText + model: ["S", "M", "L"] + } + \endcode + + \sa currentText, textRole +*/ +QString QQuickComboBox::displayText() const +{ + Q_D(const QQuickComboBox); + return d->displayText; +} + +void QQuickComboBox::setDisplayText(const QString &text) +{ + Q_D(QQuickComboBox); + d->hasDisplayText = true; + if (d->displayText == text) + return; + + d->displayText = text; + maybeSetAccessibleName(text); + emit displayTextChanged(); +} + +void QQuickComboBox::resetDisplayText() +{ + Q_D(QQuickComboBox); + if (!d->hasDisplayText) + return; + + d->hasDisplayText = false; + d->updateCurrentText(); +} + + +/*! + \qmlproperty string QtQuick.Controls::ComboBox::textRole + + This property holds the model role used for populating the combo box. + + When the model has multiple roles, \c textRole can be set to determine + which role should be displayed. + + \sa model, currentText, displayText, {ComboBox Model Roles} +*/ +QString QQuickComboBox::textRole() const +{ + Q_D(const QQuickComboBox); + return d->textRole; +} + +void QQuickComboBox::setTextRole(const QString &role) +{ + Q_D(QQuickComboBox); + if (d->textRole == role) + return; + + d->textRole = role; + if (isComponentComplete()) + d->updateCurrentText(); + emit textRoleChanged(); +} + +/*! + \since QtQuick.Controls 2.14 (Qt 5.14) + \qmlproperty string QtQuick.Controls::ComboBox::valueRole + + This property holds the model role used for storing the value associated + with each item in the model. + + For an example of how to use this property, see \l {ComboBox Model Roles}. + + \sa model, currentValue +*/ +QString QQuickComboBox::valueRole() const +{ + Q_D(const QQuickComboBox); + return d->valueRole; +} + +void QQuickComboBox::setValueRole(const QString &role) +{ + Q_D(QQuickComboBox); + if (d->valueRole == role) + return; + + d->valueRole = role; + if (isComponentComplete()) + d->updateCurrentValue(); + emit valueRoleChanged(); +} + +/*! + \qmlproperty Component QtQuick.Controls::ComboBox::delegate + + This property holds a delegate that presents an item in the combo box 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: comboBox.popup.close() + } + } + \endcode + + \sa ItemDelegate, {Customizing ComboBox} +*/ +QQmlComponent *QQuickComboBox::delegate() const +{ + Q_D(const QQuickComboBox); + return d->delegate; +} + +void QQuickComboBox::setDelegate(QQmlComponent* delegate) +{ + Q_D(QQuickComboBox); + 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(); +} + +/*! + \qmlproperty Item QtQuick.Controls::ComboBox::indicator + + This property holds the drop indicator item. + + \sa {Customizing ComboBox} +*/ +QQuickItem *QQuickComboBox::indicator() const +{ + QQuickComboBoxPrivate *d = const_cast<QQuickComboBoxPrivate *>(d_func()); + if (!d->indicator) + d->executeIndicator(); + return d->indicator; +} + +void QQuickComboBox::setIndicator(QQuickItem *indicator) +{ + Q_D(QQuickComboBox); + if (d->indicator == indicator) + return; + + QQuickControlPrivate::warnIfCustomizationNotSupported(this, indicator, QStringLiteral("indicator")); + + if (!d->indicator.isExecuting()) + d->cancelIndicator(); + + const qreal oldImplicitIndicatorWidth = implicitIndicatorWidth(); + const qreal oldImplicitIndicatorHeight = implicitIndicatorHeight(); + + d->removeImplicitSizeListener(d->indicator); + QQuickControlPrivate::hideOldItem(d->indicator); + d->indicator = indicator; + if (indicator) { + if (!indicator->parentItem()) + indicator->setParentItem(this); + d->addImplicitSizeListener(indicator); + } + + if (!qFuzzyCompare(oldImplicitIndicatorWidth, implicitIndicatorWidth())) + emit implicitIndicatorWidthChanged(); + if (!qFuzzyCompare(oldImplicitIndicatorHeight, implicitIndicatorHeight())) + emit implicitIndicatorHeightChanged(); + if (!d->indicator.isExecuting()) + emit indicatorChanged(); +} + +/*! + \qmlproperty Popup QtQuick.Controls::ComboBox::popup + + This property holds the popup. + + The popup can be opened or closed manually, if necessary: + + \code + onSpecialEvent: comboBox.popup.close() + \endcode + + \sa {Customizing ComboBox} +*/ +QQuickPopup *QQuickComboBox::popup() const +{ + QQuickComboBoxPrivate *d = const_cast<QQuickComboBoxPrivate *>(d_func()); + if (!d->popup) + d->executePopup(isComponentComplete()); + return d->popup; +} + +void QQuickComboBox::setPopup(QQuickPopup *popup) +{ + Q_D(QQuickComboBox); + if (d->popup == popup) + return; + + if (!d->popup.isExecuting()) + d->cancelPopup(); + + if (d->popup) { + QObjectPrivate::disconnect(d->popup.data(), &QQuickPopup::destroyed, d, &QQuickComboBoxPrivate::popupDestroyed); + QObjectPrivate::disconnect(d->popup.data(), &QQuickPopup::visibleChanged, d, &QQuickComboBoxPrivate::popupVisibleChanged); + QQuickComboBoxPrivate::hideOldPopup(d->popup); + } + if (popup) { + QQuickPopupPrivate::get(popup)->allowVerticalFlip = true; + popup->setClosePolicy(QQuickPopup::CloseOnEscape | QQuickPopup::CloseOnPressOutsideParent); + QObjectPrivate::connect(popup, &QQuickPopup::visibleChanged, d, &QQuickComboBoxPrivate::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, &QQuickComboBoxPrivate::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(); +} + +/*! + \since QtQuick.Controls 2.1 (Qt 5.8) + \qmlproperty bool QtQuick.Controls::ComboBox::flat + + This property holds whether the combo box button is flat. + + A flat combo box button does not draw a background unless it is interacted + with. In comparison to normal combo boxes, flat combo boxes provide looks + that make them stand out less from the rest of the UI. For instance, when + placing a combo box into a tool bar, it may be desirable to make the combo + box flat so it matches better with the flat looks of tool buttons. + + The default value is \c false. +*/ +bool QQuickComboBox::isFlat() const +{ + Q_D(const QQuickComboBox); + return d->flat; +} + +void QQuickComboBox::setFlat(bool flat) +{ + Q_D(QQuickComboBox); + if (d->flat == flat) + return; + + d->flat = flat; + emit flatChanged(); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty bool QtQuick.Controls::ComboBox::down + + This property holds whether the combo box button is visually down. + + Unless explicitly set, this property is \c true when either \c pressed + or \c popup.visible is \c true. To return to the default value, set this + property to \c undefined. + + \sa pressed, popup +*/ +bool QQuickComboBox::isDown() const +{ + Q_D(const QQuickComboBox); + return d->down; +} + +void QQuickComboBox::setDown(bool down) +{ + Q_D(QQuickComboBox); + d->hasDown = true; + + if (d->down == down) + return; + + d->down = down; + emit downChanged(); +} + +void QQuickComboBox::resetDown() +{ + Q_D(QQuickComboBox); + if (!d->hasDown) + return; + + setDown(d->pressed || d->isPopupVisible()); + d->hasDown = false; +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty bool QtQuick.Controls::ComboBox::editable + + This property holds whether the combo box is editable. + + The default value is \c false. + + \sa validator +*/ +bool QQuickComboBox::isEditable() const +{ + Q_D(const QQuickComboBox); + return d->extra.isAllocated() && d->extra->editable; +} + +void QQuickComboBox::setEditable(bool editable) +{ + Q_D(QQuickComboBox); + if (editable == isEditable()) + return; + + if (d->contentItem) { + if (editable) { + d->contentItem->installEventFilter(this); + if (QQuickTextInput *input = qobject_cast<QQuickTextInput *>(d->contentItem)) { + QObjectPrivate::connect(input, &QQuickTextInput::textChanged, d, &QQuickComboBoxPrivate::updateEditText); + QObjectPrivate::connect(input, &QQuickTextInput::accepted, d, &QQuickComboBoxPrivate::acceptInput); + } +#if QT_CONFIG(cursor) + d->contentItem->setCursor(Qt::IBeamCursor); +#endif + } else { + d->contentItem->removeEventFilter(this); + if (QQuickTextInput *input = qobject_cast<QQuickTextInput *>(d->contentItem)) { + QObjectPrivate::disconnect(input, &QQuickTextInput::textChanged, d, &QQuickComboBoxPrivate::updateEditText); + QObjectPrivate::disconnect(input, &QQuickTextInput::accepted, d, &QQuickComboBoxPrivate::acceptInput); + } +#if QT_CONFIG(cursor) + d->contentItem->unsetCursor(); +#endif + } + } + + d->extra.value().editable = editable; + setAccessibleProperty("editable", editable); + emit editableChanged(); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty string QtQuick.Controls::ComboBox::editText + + This property holds the text in the text field of an editable combo box. + + \sa editable, currentText, displayText +*/ +QString QQuickComboBox::editText() const +{ + Q_D(const QQuickComboBox); + return d->extra.isAllocated() ? d->extra->editText : QString(); +} + +void QQuickComboBox::setEditText(const QString &text) +{ + Q_D(QQuickComboBox); + if (text == editText()) + return; + + d->extra.value().editText = text; + emit editTextChanged(); +} + +void QQuickComboBox::resetEditText() +{ + setEditText(QString()); +} + +#if QT_CONFIG(validator) +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty Validator QtQuick.Controls::ComboBox::validator + + This property holds an input text validator for an editable combo box. + + When a validator is set, the text field will only accept input which + leaves the text property in an intermediate state. The \l accepted signal + will only be emitted if the text is in an acceptable state when the + \uicontrol Return or \uicontrol Enter key is pressed. + + The currently supported validators are \l[QtQuick]{IntValidator}, + \l[QtQuick]{DoubleValidator}, and \l[QtQuick]{RegularExpressionValidator}. An + example of using validators is shown below, which allows input of + integers between \c 0 and \c 10 into the text field: + + \code + ComboBox { + model: 10 + editable: true + validator: IntValidator { + top: 9 + bottom: 0 + } + } + \endcode + + \sa acceptableInput, accepted, editable +*/ +QValidator *QQuickComboBox::validator() const +{ + Q_D(const QQuickComboBox); + return d->extra.isAllocated() ? d->extra->validator : nullptr; +} + +void QQuickComboBox::setValidator(QValidator *validator) +{ + Q_D(QQuickComboBox); + if (validator == QQuickComboBox::validator()) + return; + + d->extra.value().validator = validator; +#if QT_CONFIG(validator) + if (validator) + validator->setLocale(d->locale); +#endif + emit validatorChanged(); +} +#endif + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty flags QtQuick.Controls::ComboBox::inputMethodHints + + Provides hints to the input method about the expected content of the combo box and how it + should operate. + + The default value is \c Qt.ImhNoPredictiveText. + + \include inputmethodhints.qdocinc +*/ +Qt::InputMethodHints QQuickComboBox::inputMethodHints() const +{ + Q_D(const QQuickComboBox); + return d->extra.isAllocated() ? d->extra->inputMethodHints : Qt::ImhNoPredictiveText; +} + +void QQuickComboBox::setInputMethodHints(Qt::InputMethodHints hints) +{ + Q_D(QQuickComboBox); + d->setInputMethodHints(hints); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty bool QtQuick.Controls::ComboBox::inputMethodComposing + \readonly + + This property holds whether an editable combo box has partial text input from an input method. + + While it is composing, an input method may rely on mouse or key events from the combo box to + edit or commit the partial text. This property can be used to determine when to disable event + handlers that may interfere with the correct operation of an input method. +*/ +bool QQuickComboBox::isInputMethodComposing() const +{ + Q_D(const QQuickComboBox); + return d->contentItem && d->contentItem->property("inputMethodComposing").toBool(); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty bool QtQuick.Controls::ComboBox::acceptableInput + \readonly + + This property holds whether the combo box contains acceptable text in the editable text field. + + If a validator has been set, the value is \c true only if the current text is acceptable + to the validator as a final string (not as an intermediate string). + + \sa validator, accepted +*/ +bool QQuickComboBox::hasAcceptableInput() const +{ + Q_D(const QQuickComboBox); + return d->m_acceptableInput; +} + +/*! + \since QtQuick.Controls 2.5 (Qt 5.12) + \qmlproperty real QtQuick.Controls::ComboBox::implicitIndicatorWidth + \readonly + + This property holds the implicit indicator width. + + The value is equal to \c {indicator ? indicator.implicitWidth : 0}. + + This is typically used, together with \l {Control::}{implicitContentWidth} and + \l {Control::}{implicitBackgroundWidth}, to calculate the \l {Item::}{implicitWidth}. + + \sa implicitIndicatorHeight +*/ +qreal QQuickComboBox::implicitIndicatorWidth() const +{ + Q_D(const QQuickComboBox); + if (!d->indicator) + return 0; + return d->indicator->implicitWidth(); +} + +/*! + \since QtQuick.Controls 2.5 (Qt 5.12) + \qmlproperty real QtQuick.Controls::ComboBox::implicitIndicatorHeight + \readonly + + This property holds the implicit indicator height. + + The value is equal to \c {indicator ? indicator.implicitHeight : 0}. + + This is typically used, together with \l {Control::}{implicitContentHeight} and + \l {Control::}{implicitBackgroundHeight}, to calculate the \l {Item::}{implicitHeight}. + + \sa implicitIndicatorWidth +*/ +qreal QQuickComboBox::implicitIndicatorHeight() const +{ + Q_D(const QQuickComboBox); + if (!d->indicator) + return 0; + return d->indicator->implicitHeight(); +} + +/*! + \readonly + \since QtQuick.Controls 2.14 (Qt 5.14) + \qmlproperty var QtQuick.Controls::ComboBox::currentValue + + This property holds the value of the current item in the combo box. + + For an example of how to use this property, see \l {ComboBox Model Roles}. + + \sa currentIndex, currentText, valueRole +*/ +QVariant QQuickComboBox::currentValue() const +{ + Q_D(const QQuickComboBox); + return d->currentValue; +} + +/*! + \readonly + \since QtQuick.Controls 2.14 (Qt 5.14) + \qmlmethod var QtQuick.Controls::ComboBox::valueAt(int index) + + Returns the value at position \a index in the combo box. + + \sa indexOfValue +*/ +QVariant QQuickComboBox::valueAt(int index) const +{ + Q_D(const QQuickComboBox); + if (!d->isValidIndex(index)) + return QVariant(); + + const QString effectiveValueRole = d->valueRole.isEmpty() ? QStringLiteral("modelData") : d->valueRole; + return d->delegateModel->variantValue(index, effectiveValueRole); +} + +/*! + \since QtQuick.Controls 2.14 (Qt 5.14) + \qmlmethod int QtQuick.Controls::ComboBox::indexOfValue(object value) + + Returns the index of the specified \a value, or \c -1 if no match is found. + + For an example of how to use this method, see \l {ComboBox Model Roles}. + + \include qquickcombobox.qdocinc functions-after-component-completion + + \sa find(), currentValue, currentIndex, valueRole, valueAt +*/ +int QQuickComboBox::indexOfValue(const QVariant &value) const +{ + for (int i = 0; i < count(); ++i) { + const QVariant ourValue = valueAt(i); + if (value == ourValue) + return i; + } + return -1; +} + +/*! + \since QtQuick.Controls 2.15 (Qt 5.15) + \qmlproperty bool QtQuick.Controls::ComboBox::selectTextByMouse + + This property holds whether the text field for an editable ComboBox + can be selected with the mouse. + + The default value is \c false. +*/ +bool QQuickComboBox::selectTextByMouse() const +{ + Q_D(const QQuickComboBox); + return d->extra.isAllocated() ? d->extra->selectTextByMouse : false; +} + +void QQuickComboBox::setSelectTextByMouse(bool canSelect) +{ + Q_D(QQuickComboBox); + if (canSelect == selectTextByMouse()) + return; + + d->extra.value().selectTextByMouse = canSelect; + emit selectTextByMouseChanged(); +} + +/*! + \since QtQuick.Controls 6.0 (Qt 6.0) + \qmlproperty enumeration QtQuick.Controls::ComboBox::implicitContentWidthPolicy + + This property controls how the \l{Control::}{implicitContentWidth} of the ComboBox is + calculated. + + When the width of a ComboBox is not large enough to display text, that text + is elided. Depending on which parts of the text are elided, this can make + selecting an item difficult for the end user. An efficient way of ensuring + that a ComboBox is wide enough to avoid text being elided is to set a width + that is known to be large enough: + + \code + width: 300 + implicitContentWidthPolicy: ComboBox.ContentItemImplicitWidth + \endcode + + However, it is often not possible to know whether or not a hard-coded value + will be large enough, as the size of text depends on many factors, such as + font family, font size, translations, and so on. + + implicitContentWidthPolicy provides an easy way to control how the + implicitContentWidth is calculated, which in turn affects the + \l{Item::}{implicitWidth} of the ComboBox and ensures that text will not be elided. + + The available values are: + + \value ContentItemImplicitWidth + The implicitContentWidth will default to that of the \l{Control::}{contentItem}. + This is the most efficient option, as no extra text layout is done. + \value WidestText + The implicitContentWidth will be set to the implicit width of the + the largest text for the given \l textRole every time the model + changes. + This option should be used with smaller models, as it can be expensive. + \value WidestTextWhenCompleted + The implicitContentWidth will be set to the implicit width of the + the largest text for the given \l textRole once after + \l {QQmlParserStatus::componentComplete()}{component completion}. + This option should be used with smaller models, as it can be expensive. + + The default value is \c ContentItemImplicitWidth. + + As this property only affects the \c implicitWidth of the ComboBox, setting + an explicit \l{Item::}{width} can still result in eliding. + + \note This feature requires the contentItem to be a type derived from + \l TextInput. + + \note This feature requires text to be laid out, and can therefore be + expensive for large models or models whose contents are updated + frequently. +*/ +QQuickComboBox::ImplicitContentWidthPolicy QQuickComboBox::implicitContentWidthPolicy() const +{ + Q_D(const QQuickComboBox); + return d->implicitContentWidthPolicy; +} + +void QQuickComboBox::setImplicitContentWidthPolicy(QQuickComboBox::ImplicitContentWidthPolicy policy) +{ + Q_D(QQuickComboBox); + if (policy == d->implicitContentWidthPolicy) + return; + + d->implicitContentWidthPolicy = policy; + d->maybeUpdateImplicitContentWidth(); + emit implicitContentWidthPolicyChanged(); +} +/*! + \qmlmethod string QtQuick.Controls::ComboBox::textAt(int index) + + Returns the text for the specified \a index, or an empty string + if the index is out of bounds. + + \include qquickcombobox.qdocinc functions-after-component-completion + For example: + \snippet qtquickcontrols-combobox-textat.qml textat + + \sa textRole +*/ +QString QQuickComboBox::textAt(int index) const +{ + Q_D(const QQuickComboBox); + if (!d->isValidIndex(index)) + return QString(); + + return d->delegateModel->stringValue(index, d->effectiveTextRole()); +} + +/*! + \qmlmethod int QtQuick.Controls::ComboBox::find(string text, enumeration flags) + + Returns the index of the specified \a text, or \c -1 if no match is found. + + The way the search is performed is defined by the specified match \a flags. By default, + combo box performs case sensitive exact matching (\c Qt.MatchExactly). All other match + types are case-insensitive unless the \c Qt.MatchCaseSensitive flag is also specified. + + \value Qt.MatchExactly The search term matches exactly (default). + \value Qt.MatchRegularExpression The search term matches as a regular expression. + \value Qt.MatchWildcard The search term matches using wildcards. + \value Qt.MatchFixedString The search term matches as a fixed string. + \value Qt.MatchStartsWith The search term matches the start of the item. + \value Qt.MatchEndsWidth The search term matches the end of the item. + \value Qt.MatchContains The search term is contained in the item. + \value Qt.MatchCaseSensitive The search is case sensitive. + + \include qquickcombobox.qdocinc functions-after-component-completion + For example: + \snippet qtquickcontrols-combobox-find.qml find + + \sa textRole +*/ +int QQuickComboBox::find(const QString &text, Qt::MatchFlags flags) const +{ + Q_D(const QQuickComboBox); + return d->match(0, text, flags); +} + +/*! + \qmlmethod void QtQuick.Controls::ComboBox::incrementCurrentIndex() + + Increments the current index of the combo box, or the highlighted + index if the popup list is visible. + + \sa currentIndex, highlightedIndex +*/ +void QQuickComboBox::incrementCurrentIndex() +{ + Q_D(QQuickComboBox); + d->incrementCurrentIndex(); +} + +/*! + \qmlmethod void QtQuick.Controls::ComboBox::decrementCurrentIndex() + + Decrements the current index of the combo box, or the highlighted + index if the popup list is visible. + + \sa currentIndex, highlightedIndex +*/ +void QQuickComboBox::decrementCurrentIndex() +{ + Q_D(QQuickComboBox); + d->decrementCurrentIndex(); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlmethod void QtQuick.Controls::ComboBox::selectAll() + + Selects all the text in the editable text field of the combo box. + + \sa editText +*/ +void QQuickComboBox::selectAll() +{ + Q_D(QQuickComboBox); + QQuickTextInput *input = qobject_cast<QQuickTextInput *>(d->contentItem); + if (!input) + return; + input->selectAll(); +} + +bool QQuickComboBox::eventFilter(QObject *object, QEvent *event) +{ + Q_D(QQuickComboBox); + switch (event->type()) { + case QEvent::MouseButtonRelease: + if (d->isPopupVisible()) + d->hidePopup(false); + break; + case QEvent::KeyPress: { + QKeyEvent *ke = static_cast<QKeyEvent *>(event); + if (d->filterKeyEvent(ke, false)) + return true; + event->accept(); + if (d->extra.isAllocated()) + d->extra->allowComplete = ke->key() != Qt::Key_Backspace && ke->key() != Qt::Key_Delete; + break; + } + case QEvent::FocusOut: + if (qGuiApp->focusObject() != this && (!d->popup || !d->popup->hasActiveFocus())) { + // Only close the popup if focus was transferred somewhere else + // than to the popup or the popup button (which normally means that + // the user clicked on the popup button to open it, not close it). + d->hidePopup(false); + setPressed(false); + + // The focus left the text field, so if the edit text matches an item in the model, + // change our currentIndex to that. This matches widgets' behavior. + const int indexForEditText = find(d->extra.value().editText, Qt::MatchFixedString); + if (indexForEditText > -1) + setCurrentIndex(indexForEditText); + } + break; +#if QT_CONFIG(im) + case QEvent::InputMethod: + if (d->extra.isAllocated()) + d->extra->allowComplete = !static_cast<QInputMethodEvent*>(event)->commitString().isEmpty(); + break; +#endif + default: + break; + } + return QQuickControl::eventFilter(object, event); +} + +void QQuickComboBox::focusInEvent(QFocusEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::focusInEvent(event); + // Setting focus on TextField should not be done when drop down indicator was clicked + // That is why, if focus is not set with key reason, it should not be passed to textEdit by default. + // Focus on Edit Text should be set only intentionally by user. + if ((event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason || + event->reason() == Qt::ShortcutFocusReason) && d->contentItem && isEditable()) + d->contentItem->forceActiveFocus(event->reason()); +} + +void QQuickComboBox::focusOutEvent(QFocusEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::focusOutEvent(event); + + if (qGuiApp->focusObject() != d->contentItem && (!d->popup || !d->popup->hasActiveFocus())) { + // Only close the popup if focus was transferred + // somewhere else than to the popup or the inner line edit (which is + // normally done from QQuickComboBox::focusInEvent). + d->hidePopup(false); + setPressed(false); + } +} + +#if QT_CONFIG(im) +void QQuickComboBox::inputMethodEvent(QInputMethodEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::inputMethodEvent(event); + if (!isEditable() && !event->commitString().isEmpty()) + d->keySearch(event->commitString()); + else + event->ignore(); +} +#endif + +void QQuickComboBox::keyPressEvent(QKeyEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::keyPressEvent(event); + + const auto key = event->key(); + if (!isEditable()) { + const auto buttonPressKeys = QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::ButtonPressKeys).value<QList<Qt::Key>>(); + if (buttonPressKeys.contains(key)) { + if (!event->isAutoRepeat()) + setPressed(true); + event->accept(); + return; + } + } + + switch (key) { + case Qt::Key_Escape: + case Qt::Key_Back: + if (d->isPopupVisible()) + event->accept(); + break; + case Qt::Key_Enter: + case Qt::Key_Return: + if (d->isPopupVisible()) + setPressed(true); + event->accept(); + break; + case Qt::Key_Up: + d->keyNavigating = true; + d->decrementCurrentIndex(); + event->accept(); + break; + case Qt::Key_Down: + d->keyNavigating = true; + d->incrementCurrentIndex(); + event->accept(); + break; + case Qt::Key_Home: + d->keyNavigating = true; + if (d->isPopupVisible()) + d->setHighlightedIndex(0, Highlight); + else + d->setCurrentIndex(0, Activate); + event->accept(); + break; + case Qt::Key_End: + d->keyNavigating = true; + if (d->isPopupVisible()) + d->setHighlightedIndex(count() - 1, Highlight); + else + d->setCurrentIndex(count() - 1, Activate); + event->accept(); + break; + default: + if (!isEditable() && !event->text().isEmpty()) + d->keySearch(event->text()); + else + event->ignore(); + break; + } +} + +void QQuickComboBox::keyReleaseEvent(QKeyEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::keyReleaseEvent(event); + d->keyNavigating = false; + if (event->isAutoRepeat()) + return; + + const auto key = event->key(); + if (!isEditable()) { + const auto buttonPressKeys = QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::ButtonPressKeys).value<QList<Qt::Key>>(); + if (buttonPressKeys.contains(key)) { + if (!isEditable() && isPressed()) + d->togglePopup(true); + setPressed(false); + event->accept(); + return; + } + } + + switch (key) { + case Qt::Key_Enter: + case Qt::Key_Return: + if (!isEditable() || d->isPopupVisible()) + d->hidePopup(d->isPopupVisible()); + setPressed(false); + event->accept(); + break; + case Qt::Key_Escape: + case Qt::Key_Back: + if (d->isPopupVisible()) { + d->hidePopup(false); + setPressed(false); + event->accept(); + } + break; + default: + break; + } +} + +#if QT_CONFIG(wheelevent) +void QQuickComboBox::wheelEvent(QWheelEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::wheelEvent(event); + if (d->wheelEnabled && !d->isPopupVisible()) { + if (event->angleDelta().y() > 0) + d->decrementCurrentIndex(); + else + d->incrementCurrentIndex(); + } +} +#endif + +bool QQuickComboBox::event(QEvent *e) +{ + Q_D(QQuickComboBox); + if (e->type() == QEvent::LanguageChange) + d->updateCurrentTextAndValue(); + return QQuickControl::event(e); +} + +void QQuickComboBox::componentComplete() +{ + Q_D(QQuickComboBox); + d->executeIndicator(true); + QQuickControl::componentComplete(); + if (d->popup) + d->executePopup(true); + + if (d->delegateModel && d->ownModel) + static_cast<QQmlDelegateModel *>(d->delegateModel)->componentComplete(); + + if (count() > 0) { + if (!d->hasCurrentIndex && d->currentIndex == -1) + setCurrentIndex(0); + else + d->updateCurrentTextAndValue(); + + // If the widest text was already calculated in the call to + // QQmlDelegateModel::componentComplete() above, then we shouldn't do it here too. + if (!d->hasCalculatedWidestText) + d->maybeUpdateImplicitContentWidth(); + } +} + +void QQuickComboBox::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + Q_D(QQuickComboBox); + QQuickControl::itemChange(change, value); + if (change == ItemVisibleHasChanged && !value.boolValue) { + d->hidePopup(false); + setPressed(false); + } +} + +void QQuickComboBox::fontChange(const QFont &newFont, const QFont &oldFont) +{ + Q_D(QQuickComboBox); + QQuickControl::fontChange(newFont, oldFont); + d->maybeUpdateImplicitContentWidth(); +} + +void QQuickComboBox::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) +{ + Q_D(QQuickComboBox); + if (oldItem) { + oldItem->removeEventFilter(this); + if (QQuickTextInput *oldInput = qobject_cast<QQuickTextInput *>(oldItem)) { + QObjectPrivate::disconnect(oldInput, &QQuickTextInput::accepted, d, &QQuickComboBoxPrivate::acceptInput); + QObjectPrivate::disconnect(oldInput, &QQuickTextInput::textChanged, d, &QQuickComboBoxPrivate::updateEditText); + disconnect(oldInput, &QQuickTextInput::inputMethodComposingChanged, this, &QQuickComboBox::inputMethodComposingChanged); + QObjectPrivate::disconnect(oldInput, &QQuickTextInput::acceptableInputChanged, d, &QQuickComboBoxPrivate::updateAcceptableInput); + } + } + if (newItem && isEditable()) { + newItem->installEventFilter(this); + if (QQuickTextInput *newInput = qobject_cast<QQuickTextInput *>(newItem)) { + QObjectPrivate::connect(newInput, &QQuickTextInput::accepted, d, &QQuickComboBoxPrivate::acceptInput); + QObjectPrivate::connect(newInput, &QQuickTextInput::textChanged, d, &QQuickComboBoxPrivate::updateEditText); + connect(newInput, &QQuickTextInput::inputMethodComposingChanged, this, &QQuickComboBox::inputMethodComposingChanged); + QObjectPrivate::connect(newInput, &QQuickTextInput::acceptableInputChanged, d, &QQuickComboBoxPrivate::updateAcceptableInput); + } +#if QT_CONFIG(cursor) + newItem->setCursor(Qt::IBeamCursor); +#endif + } + + d->updateAcceptableInput(); +} + +void QQuickComboBox::localeChange(const QLocale &newLocale, const QLocale &oldLocale) +{ + QQuickControl::localeChange(newLocale, oldLocale); +#if QT_CONFIG(validator) + if (QValidator *v = validator()) + v->setLocale(newLocale); +#endif +} + +QFont QQuickComboBox::defaultFont() const +{ + return QQuickTheme::font(QQuickTheme::ComboBox); +} + +#if QT_CONFIG(accessibility) +QAccessible::Role QQuickComboBox::accessibleRole() const +{ + return QAccessible::ComboBox; +} + +void QQuickComboBox::accessibilityActiveChanged(bool active) +{ + Q_D(QQuickComboBox); + QQuickControl::accessibilityActiveChanged(active); + + if (active) { + maybeSetAccessibleName(d->hasDisplayText ? d->displayText : d->currentText); + setAccessibleProperty("editable", isEditable()); + } +} +#endif // + +QT_END_NAMESPACE + +#include "moc_qquickcombobox_p.cpp" |