diff --git a/autotests/kcombobox_unittest.cpp b/autotests/kcombobox_unittest.cpp index 20167b8..0804c75 100644 --- a/autotests/kcombobox_unittest.cpp +++ b/autotests/kcombobox_unittest.cpp @@ -1,153 +1,168 @@ /* This file is part of the KDE libraries Copyright (c) 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include class KTestComboBox : public KComboBox { public: KTestComboBox(bool rw, QWidget *parent = nullptr) : KComboBox(rw, parent) {} KCompletionBase *delegate() const { return KCompletionBase::delegate(); } }; class KComboBox_UnitTest : public QObject { Q_OBJECT private: void testComboReturnPressed(bool ctorArg) { KComboBox w(ctorArg /*initial value for editable*/); w.setEditable(true); w.setCompletionMode(KCompletion::CompletionPopup); w.addItem(QStringLiteral("Hello world")); QVERIFY(w.lineEdit()); QVERIFY(qobject_cast(w.lineEdit())); // KLineEdit signals QSignalSpy qReturnPressedSpy(w.lineEdit(), SIGNAL(returnPressed())); QSignalSpy kReturnPressedSpy(w.lineEdit(), SIGNAL(returnPressed(QString))); // KComboBox signals QSignalSpy comboReturnPressedSpy(&w, SIGNAL(returnPressed())); QSignalSpy comboReturnPressedStringSpy(&w, SIGNAL(returnPressed(QString))); QSignalSpy comboActivatedSpy(&w, SIGNAL(activated(QString))); QTest::keyClick(&w, Qt::Key_Return); QCOMPARE(qReturnPressedSpy.count(), 1); QCOMPARE(kReturnPressedSpy.count(), 1); QCOMPARE(kReturnPressedSpy[0][0].toString(), QString("Hello world")); QCOMPARE(comboReturnPressedSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy[0][0].toString(), QString("Hello world")); QCOMPARE(comboActivatedSpy.count(), 1); QCOMPARE(comboActivatedSpy[0][0].toString(), QString("Hello world")); } private Q_SLOTS: void testComboReturnPressed() { testComboReturnPressed(false); testComboReturnPressed(true); } void testHistoryComboReturnPressed() { KHistoryComboBox w; QVERIFY(qobject_cast(w.lineEdit())); connect(&w, SIGNAL(activated(QString)), &w, SLOT(addToHistory(QString))); QSignalSpy comboReturnPressedSpy(&w, SIGNAL(returnPressed())); QSignalSpy comboReturnPressedStringSpy(&w, SIGNAL(returnPressed(QString))); QSignalSpy comboActivatedSpy(&w, SIGNAL(activated(QString))); QTest::keyClicks(&w, QStringLiteral("Hello world")); QTest::keyClick(&w, Qt::Key_Return); qApp->processEvents(); // QueuedConnection in KHistoryComboBox QCOMPARE(comboReturnPressedSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy.count(), 1); QCOMPARE(comboReturnPressedStringSpy[0][0].toString(), QString("Hello world")); QCOMPARE(comboActivatedSpy.count(), 1); QCOMPARE(comboActivatedSpy[0][0].toString(), QString("Hello world")); } void testHistoryComboKeyUp() { KHistoryComboBox w; QStringList items; items << QStringLiteral("One") << QStringLiteral("Two") << QStringLiteral("Three") << QStringLiteral("Four"); w.addItems(items); QSignalSpy currentIndexChangedSpy(&w, SIGNAL(currentIndexChanged(int))); w.completionObject()->setItems(items); QCOMPARE(w.currentIndex(), 0); QTest::keyClick(&w, Qt::Key_Up); QCOMPARE(w.currentIndex(), 1); QCOMPARE(currentIndexChangedSpy.count(), 1); QCOMPARE(currentIndexChangedSpy[0][0].toInt(), 1); } void testHistoryComboInsertItems() { KHistoryComboBox combo; // uic generates code like this, let's make sure it compiles combo.insertItems(0, QStringList() << QStringLiteral("foo")); } void testHistoryComboReset() { //It only tests that it doesn't crash //TODO: Finish KHistoryComboBox combo; QStringList items; items << QStringLiteral("One") << QStringLiteral("Two"); combo.addItems(items); combo.reset(); } void testDeleteLineEdit() { // Test for KCombo's KLineEdit destruction KTestComboBox *testCombo = new KTestComboBox(true, nullptr); // rw, with KLineEdit testCombo->setEditable(false); // destroys our KLineEdit, with deleteLater qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); QVERIFY(testCombo->KTestComboBox::delegate() == nullptr); delete testCombo; // not needed anymore } + void testLineEditCompletion() + { + // Test for KCombo's KLineEdit inheriting the completion object of the parent + KTestComboBox testCombo(false, nullptr); + QVERIFY(!testCombo.lineEdit()); + auto completion = testCombo.completionObject(); + QVERIFY(completion); + testCombo.setEditable(true); + auto lineEdit = qobject_cast(testCombo.lineEdit()); + QVERIFY(lineEdit); + QVERIFY(lineEdit->compObj()); + QCOMPARE(lineEdit->compObj(), completion); + QCOMPARE(testCombo.completionObject(), completion); + } + void testSelectionResetOnReturn() { // void QComboBoxPrivate::_q_returnPressed() calls lineEdit->deselect() KHistoryComboBox *testCombo = new KHistoryComboBox(true, nullptr); QCOMPARE(testCombo->insertPolicy(), QComboBox::NoInsert); // not the Qt default; KHistoryComboBox changes that QTest::keyClicks(testCombo, QStringLiteral("Hello world")); testCombo->lineEdit()->setSelection(5, 3); QVERIFY(testCombo->lineEdit()->hasSelectedText()); QTest::keyClick(testCombo, Qt::Key_Return); // Changed in Qt5: it only does that if insertionPolicy isn't NoInsert. // Should we add a lineEdit()->deselect() in KHistoryComboBox? Why does this matter? QEXPECT_FAIL("", "Qt5: QComboBox doesn't deselect text anymore on returnPressed", Continue); QVERIFY(!testCombo->lineEdit()->hasSelectedText()); } }; QTEST_MAIN(KComboBox_UnitTest) #include "kcombobox_unittest.moc" diff --git a/src/kcombobox.cpp b/src/kcombobox.cpp index 22a3281..99ebbc8 100644 --- a/src/kcombobox.cpp +++ b/src/kcombobox.cpp @@ -1,397 +1,404 @@ /* This file is part of the KDE libraries Copyright (c) 2000,2001 Dawit Alemayehu Copyright (c) 2000,2001 Carsten Pfeiffer Copyright (c) 2000 Stefan Schimanski <1Stein@gmx.de> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcombobox.h" #include #include #include class KComboBoxPrivate { public: KComboBoxPrivate(KComboBox *parent) : klineEdit(nullptr), trapReturnKey(false), q_ptr(parent) { } ~KComboBoxPrivate() { } /** * Initializes the variables upon construction. */ void init(); void _k_lineEditDeleted(); KLineEdit *klineEdit; bool trapReturnKey; KComboBox * const q_ptr; Q_DECLARE_PUBLIC(KComboBox) }; void KComboBoxPrivate::init() { Q_Q(KComboBox); q->setCompleter(nullptr); q->QComboBox::setAutoCompletion(false); // otherwise setLineEdit will create a completer... if (q->isEditable()) { q->lineEdit()->setContextMenuPolicy(Qt::DefaultContextMenu); } } void KComboBoxPrivate::_k_lineEditDeleted() { Q_Q(KComboBox); // yes, we need those ugly casts due to the multiple inheritance // sender() is guaranteed to be a KLineEdit (see the connect() to the // destroyed() signal const KCompletionBase *base = static_cast(static_cast(q->sender())); // is it our delegate, that is destroyed? if (base == q->delegate()) { q->setDelegate(nullptr); } } KComboBox::KComboBox(QWidget *parent) : QComboBox(parent), d_ptr(new KComboBoxPrivate(this)) { Q_D(KComboBox); d->init(); } KComboBox::KComboBox(bool rw, QWidget *parent) : QComboBox(parent), d_ptr(new KComboBoxPrivate(this)) { Q_D(KComboBox); d->init(); setEditable(rw); } KComboBox::~KComboBox() { } bool KComboBox::contains(const QString &text) const { if (text.isEmpty()) { return false; } const int itemCount = count(); for (int i = 0; i < itemCount; ++i) { if (itemText(i) == text) { return true; } } return false; } int KComboBox::cursorPosition() const { return (isEditable()) ? lineEdit()->cursorPosition() : -1; } void KComboBox::setAutoCompletion(bool autocomplete) { Q_D(KComboBox); if (d->klineEdit) { if (autocomplete) { d->klineEdit->setCompletionMode(KCompletion::CompletionAuto); setCompletionMode(KCompletion::CompletionAuto); } else { d->klineEdit->setCompletionMode(KCompletion::CompletionPopup); setCompletionMode(KCompletion::CompletionPopup); } } } bool KComboBox::autoCompletion() const { return completionMode() == KCompletion::CompletionAuto; } #ifndef KCOMPLETION_NO_DEPRECATED void KComboBox::setContextMenuEnabled(bool showMenu) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setContextMenuPolicy(showMenu ? Qt::DefaultContextMenu : Qt::NoContextMenu); } } void KComboBox::setUrlDropsEnabled(bool enable) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setUrlDropsEnabled(enable); } } #endif bool KComboBox::urlDropsEnabled() const { Q_D(const KComboBox); return d->klineEdit && d->klineEdit->urlDropsEnabled(); } void KComboBox::setCompletedText(const QString &text, bool marked) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setCompletedText(text, marked); } } void KComboBox::setCompletedText(const QString &text) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setCompletedText(text); } } void KComboBox::makeCompletion(const QString &text) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->makeCompletion(text); } else { // read-only combo completion if (text.isNull() || !view()) { return; } view()->keyboardSearch(text); } } void KComboBox::rotateText(KCompletionBase::KeyBindingType type) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->rotateText(type); } } void KComboBox::setTrapReturnKey(bool trap) { Q_D(KComboBox); d->trapReturnKey = trap; if (d->klineEdit) { d->klineEdit->setTrapReturnKey(trap); } else { qWarning("KComboBox::setTrapReturnKey not supported with a non-KLineEdit."); } } bool KComboBox::trapReturnKey() const { Q_D(const KComboBox); return d->trapReturnKey; } void KComboBox::setEditUrl(const QUrl &url) { QComboBox::setEditText(url.toDisplayString()); } void KComboBox::addUrl(const QUrl &url) { QComboBox::addItem(url.toDisplayString()); } void KComboBox::addUrl(const QIcon &icon, const QUrl &url) { QComboBox::addItem(icon, url.toDisplayString()); } void KComboBox::insertUrl(int index, const QUrl &url) { QComboBox::insertItem(index, url.toDisplayString()); } void KComboBox::insertUrl(int index, const QIcon &icon, const QUrl &url) { QComboBox::insertItem(index, icon, url.toDisplayString()); } void KComboBox::changeUrl(int index, const QUrl &url) { QComboBox::setItemText(index, url.toDisplayString()); } void KComboBox::changeUrl(int index, const QIcon &icon, const QUrl &url) { QComboBox::setItemIcon(index, icon); QComboBox::setItemText(index, url.toDisplayString()); } void KComboBox::setCompletedItems(const QStringList &items, bool autosubject) { Q_D(KComboBox); if (d->klineEdit) { d->klineEdit->setCompletedItems(items, autosubject); } } KCompletionBox *KComboBox::completionBox(bool create) { Q_D(KComboBox); if (d->klineEdit) { return d->klineEdit->completionBox(create); } return nullptr; } QSize KComboBox::minimumSizeHint() const { Q_D(const KComboBox); QSize size = QComboBox::minimumSizeHint(); if (isEditable() && d->klineEdit) { // if it's a KLineEdit and it's editable add the clear button size // to the minimum size hint, otherwise looks ugly because the // clear button will cover the last 2/3 letters of the biggest entry QSize bs = d->klineEdit->clearButtonUsedSize(); if (bs.isValid()) { size.rwidth() += bs.width(); size.rheight() = qMax(size.height(), bs.height()); } } return size; } void KComboBox::setLineEdit(QLineEdit *edit) { Q_D(KComboBox); if (!isEditable() && edit && !qstrcmp(edit->metaObject()->className(), "QLineEdit")) { // uic generates code that creates a read-only KComboBox and then // calls combo->setEditable(true), which causes QComboBox to set up // a dumb QLineEdit instead of our nice KLineEdit. // As some KComboBox features rely on the KLineEdit, we reject // this order here. delete edit; KLineEdit *kedit = new KLineEdit(this); if (isEditable()) { kedit->setClearButtonShown(true); } edit = kedit; } + // reuse an existing completion object, if it was configured already + auto completion = compObj(); + QComboBox::setLineEdit(edit); d->klineEdit = qobject_cast(edit); setDelegate(d->klineEdit); + if (completion && d->klineEdit) { + d->klineEdit->setCompletionObject(completion); + } + // Connect the returnPressed signal for both Q[K]LineEdits' if (edit) { connect(edit, SIGNAL(returnPressed()), SIGNAL(returnPressed())); } if (d->klineEdit) { // someone calling KComboBox::setEditable(false) destroys our // line edit without us noticing. And KCompletionBase::delegate would // be a dangling pointer then, so prevent that. Note: only do this // when it is a KLineEdit! connect(edit, SIGNAL(destroyed()), SLOT(_k_lineEditDeleted())); connect(d->klineEdit, SIGNAL(returnPressed(QString)), SIGNAL(returnPressed(QString))); connect(d->klineEdit, SIGNAL(completion(QString)), SIGNAL(completion(QString))); connect(d->klineEdit, SIGNAL(substringCompletion(QString)), SIGNAL(substringCompletion(QString))); connect(d->klineEdit, SIGNAL(textRotation(KCompletionBase::KeyBindingType)), SIGNAL(textRotation(KCompletionBase::KeyBindingType))); connect(d->klineEdit, SIGNAL(completionModeChanged(KCompletion::CompletionMode)), SIGNAL(completionModeChanged(KCompletion::CompletionMode))); connect(d->klineEdit, SIGNAL(aboutToShowContextMenu(QMenu*)), SIGNAL(aboutToShowContextMenu(QMenu*))); connect(d->klineEdit, SIGNAL(completionBoxActivated(QString)), SIGNAL(activated(QString))); d->klineEdit->setTrapReturnKey(d->trapReturnKey); } } void KComboBox::setCurrentItem(const QString &item, bool insert, int index) { int sel = -1; const int itemCount = count(); for (int i = 0; i < itemCount; ++i) { if (itemText(i) == item) { sel = i; break; } } if (sel == -1 && insert) { if (index >= 0) { insertItem(index, item); sel = index; } else { addItem(item); sel = count() - 1; } } setCurrentIndex(sel); } void KComboBox::setEditable(bool editable) { if (editable) { // Create a KLineEdit instead of a QLineEdit // Compared to QComboBox::setEditable, we might be missing the SH_ComboBox_Popup code though... // If a style needs this, then we'll need to call QComboBox::setEditable and then setLineEdit again KLineEdit *edit = new KLineEdit(this); edit->setClearButtonShown(true); setLineEdit(edit); } else { QComboBox::setEditable(editable); } } #include "moc_kcombobox.cpp"