diff options
Diffstat (limited to 'src/quicktemplates2/qquickcombobox.cpp')
-rw-r--r-- | src/quicktemplates2/qquickcombobox.cpp | 430 |
1 files changed, 424 insertions, 6 deletions
diff --git a/src/quicktemplates2/qquickcombobox.cpp b/src/quicktemplates2/qquickcombobox.cpp index 5f18fb40..0ced47d1 100644 --- a/src/quicktemplates2/qquickcombobox.cpp +++ b/src/quicktemplates2/qquickcombobox.cpp @@ -41,11 +41,15 @@ #include <QtCore/qregexp.h> #include <QtCore/qabstractitemmodel.h> +#include <QtGui/qinputmethod.h> +#include <QtGui/qguiapplication.h> #include <QtGui/qpa/qplatformtheme.h> #include <QtQml/qjsvalue.h> #include <QtQml/qqmlcontext.h> +#include <QtQml/private/qlazilyallocated_p.h> #include <QtQml/private/qqmldelegatemodel_p.h> #include <QtQuick/private/qquickevents_p_p.h> +#include <QtQuick/private/qquicktextinput_p.h> QT_BEGIN_NAMESPACE @@ -74,6 +78,30 @@ QT_BEGIN_NAMESPACE } \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. + + \code + ComboBox { + editable: true + model: ListModel { + id: model + ListElement { text: "Banana" } + ListElement { text: "Apple" } + ListElement { text: "Coconut" } + } + onAccepted: { + if (find(editText) === -1) + model.append({text: editText}) + } + } + \endcode + \section1 ComboBox Model Roles ComboBox is able to visualize standard \l {qml-data-models}{data models} @@ -130,6 +158,19 @@ QT_BEGIN_NAMESPACE \sa highlightedIndex */ +/*! + \since QtQuick.Controls 2.2 + \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. If the confirmed string is not currently in the model, + the \l currentIndex will be set to \c -1 and the \c currentText will be updated + accordingly. + + \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 }; @@ -181,8 +222,15 @@ public: void itemClicked(); void createdItem(int index, QObject *object); + void modelUpdated(); void countChanged(); + + void updateEditText(); void updateCurrentText(); + + void acceptInput(); + QString tryComplete(const QString &inputText); + void incrementCurrentIndex(); void decrementCurrentIndex(); void setCurrentIndex(int index, Activation activate); @@ -212,6 +260,23 @@ public: QQmlComponent *delegate; QQuickItem *indicator; QQuickPopup *popup; + + struct ExtraData { + ExtraData() + : editable(false), + accepting(false), + allowComplete(false), + inputMethodHints(Qt::ImhNone), + validator(nullptr) { } + + bool editable; + bool accepting; + bool allowComplete; + Qt::InputMethodHints inputMethodHints; + QString editText; + QValidator *validator; + }; + QLazilyAllocated<ExtraData> extra; }; QQuickComboBoxPrivate::QQuickComboBoxPrivate() @@ -267,6 +332,9 @@ void QQuickComboBoxPrivate::togglePopup(bool accept) void QQuickComboBoxPrivate::popupVisibleChanged() { Q_Q(QQuickComboBox); + if (isPopupVisible()) + QGuiApplication::inputMethod()->reset(); + updateHighlightedIndex(); if (!hasDown) { q->setDown(pressed || isPopupVisible()); @@ -286,6 +354,7 @@ void QQuickComboBoxPrivate::itemClicked() void QQuickComboBoxPrivate::createdItem(int index, QObject *object) { + Q_Q(QQuickComboBox); QQuickItem *item = qobject_cast<QQuickItem *>(object); if (popup && item && !item->parentItem()) { item->setParentItem(popup->contentItem()); @@ -298,7 +367,13 @@ void QQuickComboBoxPrivate::createdItem(int index, QObject *object) connect(button, &QQuickAbstractButton::clicked, this, &QQuickComboBoxPrivate::itemClicked); } - if (index == currentIndex) + if (index == currentIndex && !q->isEditable()) + updateCurrentText(); +} + +void QQuickComboBoxPrivate::modelUpdated() +{ + if (!extra.isAllocated() || !extra->accepting) updateCurrentText(); } @@ -310,6 +385,26 @@ void QQuickComboBoxPrivate::countChanged() emit q->countChanged(); } +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.length() > text.length()) { + input->setText(completed); + input->select(completed.length(), text.length()); + return; + } + } + q->setEditText(text); +} + void QQuickComboBoxPrivate::updateCurrentText() { Q_Q(QQuickComboBox); @@ -324,6 +419,45 @@ void QQuickComboBoxPrivate::updateCurrentText() displayText = text; emit q->displayTextChanged(); } + if (!extra.isAllocated() || !extra->accepting) + q->setEditText(currentText); +} + +void QQuickComboBoxPrivate::acceptInput() +{ + Q_Q(QQuickComboBox); + int idx = q->find(extra.value().editText, Qt::MatchFixedString); + if (idx > -1) + q->setCurrentIndex(idx); + + extra.value().accepting = true; + emit q->accepted(); + + 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.length() < match.length()) + match = text; + } + + if (match.isEmpty()) + return input; + + return input + match.mid(input.length()); } void QQuickComboBoxPrivate::setCurrentIndex(int index, Activation activate) @@ -345,6 +479,8 @@ void QQuickComboBoxPrivate::setCurrentIndex(int index, Activation activate) void QQuickComboBoxPrivate::incrementCurrentIndex() { Q_Q(QQuickComboBox); + if (extra.isAllocated()) + extra->allowComplete = false; if (isPopupVisible()) { if (highlightedIndex < q->count() - 1) setHighlightedIndex(highlightedIndex + 1, Highlight); @@ -352,10 +488,14 @@ void QQuickComboBoxPrivate::incrementCurrentIndex() 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); @@ -363,6 +503,8 @@ void QQuickComboBoxPrivate::decrementCurrentIndex() if (currentIndex > 0) setCurrentIndex(currentIndex - 1, Activate); } + if (extra.isAllocated()) + extra->allowComplete = true; } void QQuickComboBoxPrivate::updateHighlightedIndex() @@ -449,7 +591,7 @@ void QQuickComboBoxPrivate::createDelegateModel() QQmlInstanceModel* oldModel = delegateModel; if (oldModel) { disconnect(delegateModel, &QQmlInstanceModel::countChanged, this, &QQuickComboBoxPrivate::countChanged); - disconnect(delegateModel, &QQmlInstanceModel::modelUpdated, this, &QQuickComboBoxPrivate::updateCurrentText); + disconnect(delegateModel, &QQmlInstanceModel::modelUpdated, this, &QQuickComboBoxPrivate::modelUpdated); disconnect(delegateModel, &QQmlInstanceModel::createdItem, this, &QQuickComboBoxPrivate::createdItem); } @@ -469,7 +611,7 @@ void QQuickComboBoxPrivate::createDelegateModel() if (delegateModel) { connect(delegateModel, &QQmlInstanceModel::countChanged, this, &QQuickComboBoxPrivate::countChanged); - connect(delegateModel, &QQmlInstanceModel::modelUpdated, this, &QQuickComboBoxPrivate::updateCurrentText); + connect(delegateModel, &QQmlInstanceModel::modelUpdated, this, &QQuickComboBoxPrivate::modelUpdated); connect(delegateModel, &QQmlInstanceModel::createdItem, this, &QQuickComboBoxPrivate::createdItem); } @@ -485,6 +627,7 @@ QQuickComboBox::QQuickComboBox(QQuickItem *parent) : setFocusPolicy(Qt::StrongFocus); setFlag(QQuickItem::ItemIsFocusScope); setAcceptedMouseButtons(Qt::LeftButton); + setInputMethodHints(Qt::ImhNoPredictiveText); } QQuickComboBox::~QQuickComboBox() @@ -567,6 +710,48 @@ QQmlInstanceModel *QQuickComboBox::delegateModel() const } /*! + \since QtQuick.Controls 2.2 + \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); + } + } 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); + } + } + } + + d->extra.value().editable = editable; + emit editableChanged(); +} + +/*! \since QtQuick.Controls 2.1 \qmlproperty bool QtQuick.Controls::ComboBox::flat @@ -765,6 +950,35 @@ void QQuickComboBox::resetDisplayText() } /*! + \since QtQuick.Controls 2.2 + \qmlproperty string QtQuick.Controls::ComboBox::editText + + This property holds the text in the text field of an editable combo box. + + \sa editable +*/ +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()); +} + +/*! \qmlproperty string QtQuick.Controls::ComboBox::textRole This property holds the model role used for populating the combo box. @@ -901,6 +1115,115 @@ void QQuickComboBox::setPopup(QQuickPopup *popup) } /*! + \since QtQuick.Controls 2.2 + \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]{RegExpValidator}. 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 (validator) + validator->setLocale(d->locale); + emit validatorChanged(); +} + +/*! + \since QtQuick.Controls 2.2 + \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); + if (hints == inputMethodHints()) + return; + + d->extra.value().inputMethodHints = hints; + emit inputMethodHintsChanged(); +} + +/*! + \since QtQuick.Controls 2.2 + \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 + \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->contentItem && d->contentItem->property("acceptableInput").toBool(); +} + +/*! \qmlmethod string QtQuick.Controls::ComboBox::textAt(int index) Returns the text for the specified \a index, or an empty string @@ -977,6 +1300,59 @@ void QQuickComboBox::decrementCurrentIndex() d->decrementCurrentIndex(); } +/*! + \since QtQuick.Controls 2.2 + \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: { + const int key = static_cast<QKeyEvent *>(event)->key(); + if (d->extra.isAllocated()) + d->extra->allowComplete = key != Qt::Key_Backspace && key != Qt::Key_Delete; + break; + } + case QEvent::FocusOut: + d->hidePopup(false); + setPressed(false); + break; + case QEvent::InputMethod: + if (d->extra.isAllocated()) + d->extra->allowComplete = !static_cast<QInputMethodEvent*>(event)->commitString().isEmpty(); + break; + default: + break; + } + return QQuickControl::eventFilter(object, event); +} + +void QQuickComboBox::focusInEvent(QFocusEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::focusInEvent(event); + if (d->contentItem && isEditable()) + d->contentItem->forceActiveFocus(event->reason()); +} + void QQuickComboBox::focusOutEvent(QFocusEvent *event) { Q_D(QQuickComboBox); @@ -985,6 +1361,16 @@ void QQuickComboBox::focusOutEvent(QFocusEvent *event) setPressed(false); } +void QQuickComboBox::inputMethodEvent(QInputMethodEvent *event) +{ + Q_D(QQuickComboBox); + QQuickControl::inputMethodEvent(event); + if (!isEditable() && !event->commitString().isEmpty()) + d->keySearch(event->commitString()); + else + event->ignore(); +} + void QQuickComboBox::keyPressEvent(QKeyEvent *event) { Q_D(QQuickComboBox); @@ -1030,7 +1416,7 @@ void QQuickComboBox::keyPressEvent(QKeyEvent *event) event->accept(); break; default: - if (!event->text().isEmpty()) + if (!isEditable() && !event->text().isEmpty()) d->keySearch(event->text()); else event->ignore(); @@ -1047,13 +1433,15 @@ void QQuickComboBox::keyReleaseEvent(QKeyEvent *event) switch (event->key()) { case Qt::Key_Space: - d->togglePopup(true); + if (!isEditable()) + d->togglePopup(true); setPressed(false); event->accept(); break; case Qt::Key_Enter: case Qt::Key_Return: - d->hidePopup(d->isPopupVisible()); + if (!isEditable() || d->isPopupVisible()) + d->hidePopup(d->isPopupVisible()); setPressed(false); event->accept(); break; @@ -1126,6 +1514,36 @@ void QQuickComboBox::componentComplete() } } +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); + disconnect(oldInput, &QQuickTextInput::acceptableInputChanged, this, &QQuickComboBox::acceptableInputChanged); + } + } + 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); + connect(newInput, &QQuickTextInput::acceptableInputChanged, this, &QQuickComboBox::acceptableInputChanged); + } + } +} + +void QQuickComboBox::localeChange(const QLocale &newLocale, const QLocale &oldLocale) +{ + QQuickControl::localeChange(newLocale, oldLocale); + if (QValidator *v = validator()) + v->setLocale(newLocale); +} + QFont QQuickComboBox::defaultFont() const { return QQuickControlPrivate::themeFont(QPlatformTheme::ComboMenuItemFont); |