From f462312365d4955fc82b247b72f84e1c77d8104d Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Mon, 4 Mar 2019 11:12:05 +0100 Subject: Add valueRole API to ComboBox This allows the user to conveniently manage data for a role associated with the text role. A common example of this is an enum stored in a backend with nicely formatted text displayed to the user. Before this patch, developers would have to write code like this: ComboBox { textRole: "text" onActivated: backend.modifier = model[currentIndex].value Component.onCompleted: currentIndex = findValue(backend.modifier) model: [ { value: Qt.NoModifier, text: qsTr("No modifier") }, { value: Qt.ShiftModifier, text: qsTr("Shift") }, { value: Qt.ControlModifier, text: qsTr("Control") } ] function findValue(value) { for (var i = 0; i < model.length; ++i) { if (model[i].value === value) return i } return -1 } } With this patch, the code becomes much simpler: ComboBox { textRole: "text" valueRole: "value" onActivated: backend.modifier = currentValue Component.onCompleted: currentIndex = indexOfValue(backend.modifier) model: [ { value: Qt.NoModifier, text: qsTr("No modifier") }, { value: Qt.ShiftModifier, text: qsTr("Shift") }, { value: Qt.ControlModifier, text: qsTr("Control") } ] } [ChangeLog][Controls][ComboBox] Added valueRole, currentValue and indexOfValue(). These allow convenient management of data for a role associated with the text role. Change-Id: I0ed19bd0ba9cf6b044a8113ff1a8782d43065449 Fixes: QTBUG-73491 Reviewed-by: Mitch Curtis --- src/imports/controls/ComboBox.qml | 10 +- .../qtquickcontrols2-combobox-valuerole.qml | 58 +++++++++ src/imports/controls/fusion/ComboBox.qml | 14 +-- src/imports/controls/imagine/ComboBox.qml | 12 +- src/imports/controls/material/ComboBox.qml | 14 +-- src/imports/controls/universal/ComboBox.qml | 12 +- src/imports/templates/qtquicktemplates2plugin.cpp | 3 + src/quicktemplates2/qquickcombobox.cpp | 139 +++++++++++++++++---- src/quicktemplates2/qquickcombobox_p.h | 14 +++ tests/auto/controls/data/tst_combobox.qml | 66 +++++++++- 10 files changed, 289 insertions(+), 53 deletions(-) create mode 100644 src/imports/controls/doc/snippets/qtquickcontrols2-combobox-valuerole.qml diff --git a/src/imports/controls/ComboBox.qml b/src/imports/controls/ComboBox.qml index 3bca9c02..8eefc686 100644 --- a/src/imports/controls/ComboBox.qml +++ b/src/imports/controls/ComboBox.qml @@ -34,11 +34,11 @@ ** ****************************************************************************/ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.impl 2.12 -import QtQuick.Templates 2.12 as T +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Controls.impl 2.14 +import QtQuick.Templates 2.14 as T T.ComboBox { id: control diff --git a/src/imports/controls/doc/snippets/qtquickcontrols2-combobox-valuerole.qml b/src/imports/controls/doc/snippets/qtquickcontrols2-combobox-valuerole.qml new file mode 100644 index 00000000..4d7ae3d3 --- /dev/null +++ b/src/imports/controls/doc/snippets/qtquickcontrols2-combobox-valuerole.qml @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 + +//! [file] +ApplicationWindow { + width: 640 + height: 480 + visible: true + + // Used as an example of a backend - this would usually be + // e.g. a C++ type exposed to QML. + QtObject { + id: backend + property int modifier + } + + ComboBox { + textRole: "text" + valueRole: "value" + // When an item is selected, update the backend. + onActivated: backend.modifier = currentValue + // Set the initial currentIndex to the value stored in the backend. + Component.onCompleted: currentIndex = indexOfValue(backend.modifier) + model: [ + { value: Qt.NoModifier, text: qsTr("No modifier") }, + { value: Qt.ShiftModifier, text: qsTr("Shift") }, + { value: Qt.ControlModifier, text: qsTr("Control") } + ] + } +} +//! [file] diff --git a/src/imports/controls/fusion/ComboBox.qml b/src/imports/controls/fusion/ComboBox.qml index 3ecb0cf2..58764809 100644 --- a/src/imports/controls/fusion/ComboBox.qml +++ b/src/imports/controls/fusion/ComboBox.qml @@ -34,13 +34,13 @@ ** ****************************************************************************/ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls 2.12 -import QtQuick.Controls.impl 2.12 -import QtQuick.Controls.Fusion 2.12 -import QtQuick.Controls.Fusion.impl 2.12 +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Templates 2.14 as T +import QtQuick.Controls 2.14 +import QtQuick.Controls.impl 2.14 +import QtQuick.Controls.Fusion 2.14 +import QtQuick.Controls.Fusion.impl 2.14 T.ComboBox { id: control diff --git a/src/imports/controls/imagine/ComboBox.qml b/src/imports/controls/imagine/ComboBox.qml index 3a3ae682..2d582e98 100644 --- a/src/imports/controls/imagine/ComboBox.qml +++ b/src/imports/controls/imagine/ComboBox.qml @@ -34,12 +34,12 @@ ** ****************************************************************************/ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls 2.12 -import QtQuick.Controls.Imagine 2.12 -import QtQuick.Controls.Imagine.impl 2.12 +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Templates 2.14 as T +import QtQuick.Controls 2.14 +import QtQuick.Controls.Imagine 2.14 +import QtQuick.Controls.Imagine.impl 2.14 T.ComboBox { id: control diff --git a/src/imports/controls/material/ComboBox.qml b/src/imports/controls/material/ComboBox.qml index 223f8fca..7d635902 100644 --- a/src/imports/controls/material/ComboBox.qml +++ b/src/imports/controls/material/ComboBox.qml @@ -34,13 +34,13 @@ ** ****************************************************************************/ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.impl 2.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls.Material 2.12 -import QtQuick.Controls.Material.impl 2.12 +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Controls.impl 2.14 +import QtQuick.Templates 2.14 as T +import QtQuick.Controls.Material 2.14 +import QtQuick.Controls.Material.impl 2.14 T.ComboBox { id: control diff --git a/src/imports/controls/universal/ComboBox.qml b/src/imports/controls/universal/ComboBox.qml index 3ec7e98b..9a4e119b 100644 --- a/src/imports/controls/universal/ComboBox.qml +++ b/src/imports/controls/universal/ComboBox.qml @@ -34,12 +34,12 @@ ** ****************************************************************************/ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.impl 2.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls.Universal 2.12 +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Controls.impl 2.14 +import QtQuick.Templates 2.14 as T +import QtQuick.Controls.Universal 2.14 T.ComboBox { id: control diff --git a/src/imports/templates/qtquicktemplates2plugin.cpp b/src/imports/templates/qtquicktemplates2plugin.cpp index 10f9b8dd..008293a2 100644 --- a/src/imports/templates/qtquicktemplates2plugin.cpp +++ b/src/imports/templates/qtquicktemplates2plugin.cpp @@ -355,6 +355,9 @@ void QtQuickTemplates2Plugin::registerTypes(const char *uri) qmlRegisterUncreatableType(uri, 2, 13, "SplitHandle", QStringLiteral("SplitHandle is only available as an attached property.")); qmlRegisterType(); + + // QtQuick.Templates 2.14 (new types and revisions in Qt 5.14) + qmlRegisterType(uri, 2, 14, "ComboBox"); } QT_END_NAMESPACE diff --git a/src/quicktemplates2/qquickcombobox.cpp b/src/quicktemplates2/qquickcombobox.cpp index 0ffd76ab..9286da81 100644 --- a/src/quicktemplates2/qquickcombobox.cpp +++ b/src/quicktemplates2/qquickcombobox.cpp @@ -117,18 +117,14 @@ QT_BEGIN_NAMESPACE 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. + 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. - \code - ComboBox { - textRole: "key" - model: ListModel { - ListElement { key: "First"; value: 123 } - ListElement { key: "Second"; value: 456 } - ListElement { key: "Third"; value: 789 } - } - } - \endcode + For example: + + \snippet qtquickcontrols2-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 @@ -184,7 +180,7 @@ class QQuickComboBoxDelegateModel : public QQmlDelegateModel { public: explicit QQuickComboBoxDelegateModel(QQuickComboBox *combo); - QString stringValue(int index, const QString &role); + QVariant variantValue(int index, const QString &role) override; private: QQuickComboBox *combo = nullptr; @@ -196,23 +192,23 @@ QQuickComboBoxDelegateModel::QQuickComboBoxDelegateModel(QQuickComboBox *combo) { } -QString QQuickComboBoxDelegateModel::stringValue(int index, const QString &role) +QVariant QQuickComboBoxDelegateModel::variantValue(int index, const QString &role) { - QVariant model = combo->model(); + const QVariant model = combo->model(); if (model.userType() == QMetaType::QVariantList) { QVariant object = model.toList().value(index); if (object.userType() == QMetaType::QVariantMap) { const QVariantMap data = object.toMap(); if (data.count() == 1 && role == QLatin1String("modelData")) - return data.first().toString(); - return data.value(role).toString(); + return data.first(); + return data.value(role); } else if (object.userType() == QMetaType::QObjectStar) { const QObject *data = object.value(); if (data && role != QLatin1String("modelData")) - return data->property(role.toUtf8()).toString(); + return data->property(role.toUtf8()); } } - return QQmlDelegateModel::stringValue(index, role); + return QQmlDelegateModel::variantValue(index, role); } class QQuickComboBoxPrivate : public QQuickControlPrivate @@ -235,6 +231,7 @@ public: void updateEditText(); void updateCurrentText(); + void updateCurrentValue(); void acceptInput(); QString tryComplete(const QString &inputText); @@ -278,6 +275,8 @@ public: QString textRole; QString currentText; QString displayText; + QString valueRole; + QVariant currentValue; QQuickItem *pressedItem = nullptr; QQmlInstanceModel *delegateModel = nullptr; QQmlComponent *delegate = nullptr; @@ -453,6 +452,17 @@ void QQuickComboBoxPrivate::updateCurrentText() 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::acceptInput() { Q_Q(QQuickComboBox); @@ -499,8 +509,10 @@ void QQuickComboBoxPrivate::setCurrentIndex(int index, Activation activate) currentIndex = index; emit q->currentIndexChanged(); - if (componentComplete) + if (componentComplete) { updateCurrentText(); + updateCurrentValue(); + } if (activate) emit q->activated(index); @@ -1007,6 +1019,35 @@ void QQuickComboBox::setTextRole(const QString &role) 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 @@ -1444,6 +1485,60 @@ qreal QQuickComboBox::implicitIndicatorHeight() const return d->indicator->implicitHeight(); } +/*! + \readonly + \since QtQuick.Controls 2.14 (Qt 5.14) + \qmlproperty string 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; +} + +QVariant QQuickComboBox::valueAt(int index) const +{ + Q_D(const QQuickComboBox); + if (!d->delegateModel || index < 0 || index >= d->delegateModel->count()) + return QVariant(); + + // We use QVariant because the model API uses QVariant. + QVariant value; + QObject *object = d->delegateModel->object(index); + if (object) { + const QString role = d->valueRole.isEmpty() ? QStringLiteral("modelData") : d->valueRole; + value = d->delegateModel->variantValue(index, role); + d->delegateModel->release(object); + } + return value; +} + +/*! + \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}. + + \sa find(), currentValue, currentIndex, valueRole +*/ +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; +} + /*! \qmlmethod string QtQuick.Controls::ComboBox::textAt(int index) @@ -1728,10 +1823,12 @@ void QQuickComboBox::componentComplete() static_cast(d->delegateModel)->componentComplete(); if (count() > 0) { - if (!d->hasCurrentIndex && d->currentIndex == -1) + if (!d->hasCurrentIndex && d->currentIndex == -1) { setCurrentIndex(0); - else + } else { d->updateCurrentText(); + d->updateCurrentValue(); + } } } diff --git a/src/quicktemplates2/qquickcombobox_p.h b/src/quicktemplates2/qquickcombobox_p.h index 75e535a9..a55541d4 100644 --- a/src/quicktemplates2/qquickcombobox_p.h +++ b/src/quicktemplates2/qquickcombobox_p.h @@ -86,6 +86,9 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickComboBox : public QQuickControl Q_PROPERTY(qreal implicitIndicatorWidth READ implicitIndicatorWidth NOTIFY implicitIndicatorWidthChanged FINAL REVISION 5) Q_PROPERTY(qreal implicitIndicatorHeight READ implicitIndicatorHeight NOTIFY implicitIndicatorHeightChanged FINAL REVISION 5) Q_CLASSINFO("DeferredPropertyNames", "background,contentItem,indicator,popup") + // 2.14 (Qt 5.14) + Q_PROPERTY(QVariant currentValue READ currentValue NOTIFY currentValueChanged FINAL REVISION 14) + Q_PROPERTY(QString valueRole READ valueRole WRITE setValueRole NOTIFY valueRoleChanged FINAL REVISION 14) public: explicit QQuickComboBox(QQuickItem *parent = nullptr); @@ -114,6 +117,9 @@ public: QString textRole() const; void setTextRole(const QString &role); + QString valueRole() const; + void setValueRole(const QString &role); + QQmlComponent *delegate() const; void setDelegate(QQmlComponent *delegate); @@ -155,6 +161,11 @@ public: qreal implicitIndicatorWidth() const; qreal implicitIndicatorHeight() const; + // 2.14 (Qt 5.14) + QVariant currentValue() const; + Q_INVOKABLE QVariant valueAt(int index) const; + Q_INVOKABLE int indexOfValue(const QVariant &value) const; + public Q_SLOTS: void incrementCurrentIndex(); void decrementCurrentIndex(); @@ -189,6 +200,9 @@ Q_SIGNALS: // 2.5 (Qt 5.12) Q_REVISION(5) void implicitIndicatorWidthChanged(); Q_REVISION(5) void implicitIndicatorHeightChanged(); + // 2.14 (Qt 5.14) + Q_REVISION(14) void valueRoleChanged(); + Q_REVISION(14) void currentValueChanged(); protected: bool eventFilter(QObject *object, QEvent *event) override; diff --git a/tests/auto/controls/data/tst_combobox.qml b/tests/auto/controls/data/tst_combobox.qml index 7f061a2b..7d65b698 100644 --- a/tests/auto/controls/data/tst_combobox.qml +++ b/tests/auto/controls/data/tst_combobox.qml @@ -351,9 +351,73 @@ TestCase { compare(control.find(data.term, data.flags), data.index) } + function test_valueRole_data() { + return [ + { tag: "ListModel", model: fruitmodel }, + { tag: "ObjectArray", model: fruitarray } + ] + } + + function test_valueRole(data) { + var control = createTemporaryObject(emptyBox, testCase, + { model: data.model, valueRole: "color" }) + verify(control) + compare(control.count, 3) + compare(control.currentIndex, 0) + compare(control.currentValue, "red") + + control.valueRole = "name" + compare(control.currentValue, "Apple") + + control.currentIndex = 1 + compare(control.currentIndex, 1) + compare(control.currentValue, "Orange") + + control.valueRole = "color" + compare(control.currentValue, "orange") + + control.model = null + compare(control.currentIndex, -1) + // An invalid QVariant is represented as undefined. + compare(control.currentValue, undefined) + + control.valueRole = "" + compare(control.currentValue, undefined) + } + + function test_valueAt() { + var control = createTemporaryObject(comboBox, testCase, + { model: fruitmodel, textRole: "name", valueRole: "color" }) + verify(control) + + compare(control.valueAt(0), "red") + compare(control.valueAt(1), "orange") + compare(control.valueAt(2), "yellow") + compare(control.valueAt(-1), undefined) + compare(control.valueAt(5), undefined) + } + + function test_indexOfValue_data() { + return [ + { tag: "red", expectedIndex: 0 }, + { tag: "orange", expectedIndex: 1 }, + { tag: "yellow", expectedIndex: 2 }, + { tag: "brown", expectedIndex: -1 }, + ] + } + + function test_indexOfValue(data) { + var control = createTemporaryObject(comboBox, testCase, + { model: fruitmodel, textRole: "name", valueRole: "color" }) + verify(control) + + compare(control.indexOfValue(data.tag), data.expectedIndex) + } + function test_arrowKeys() { - var control = createTemporaryObject(comboBox, testCase, {model: 3}) + var control = createTemporaryObject(comboBox, testCase, + { model: fruitmodel, textRole: "name", valueRole: "color" }) verify(control) var activatedSpy = signalSpy.createObject(control, {target: control, signalName: "activated"}) -- cgit v1.2.3