// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include "../../../../shared/filesystem.h" #include #include Q_DECLARE_METATYPE(QCompleter::CompletionMode) using namespace QTestPrivate; class CsvCompleter : public QCompleter { Q_OBJECT public: using QCompleter::QCompleter; QString pathFromIndex(const QModelIndex& sourceIndex) const override; void setCsvCompletion(bool set) { csv = set; } protected: QStringList splitPath(const QString &path) const override { return csv ? path.split(QLatin1Char(',')) : QCompleter::splitPath(path); } private: bool csv = true; }; QString CsvCompleter::pathFromIndex(const QModelIndex &sourceIndex) const { if (!csv) return QCompleter::pathFromIndex(sourceIndex); if (!sourceIndex.isValid()) return QString(); QModelIndex idx = sourceIndex; QStringList list; do { QString t = model()->data(idx, completionRole()).toString(); list.prepend(t); QModelIndex parent = idx.parent(); idx = parent.sibling(parent.row(), sourceIndex.column()); } while (idx.isValid()); return list.size() == 1 ? list.constFirst() : list.join(QLatin1Char(',')); } class tst_QCompleter : public QObject { Q_OBJECT public: tst_QCompleter(); ~tst_QCompleter(); private slots: void getSetCheck(); void multipleWidgets(); void focusIn(); void csMatchingOnCsSortedModel_data(); void csMatchingOnCsSortedModel(); void ciMatchingOnCiSortedModel_data(); void ciMatchingOnCiSortedModel(); void ciMatchingOnCsSortedModel_data(); void ciMatchingOnCsSortedModel(); void csMatchingOnCiSortedModel_data(); void csMatchingOnCiSortedModel(); void fileSystemModel_data(); void fileSystemModel(); void fileDialog_data(); void fileDialog(); void changingModel_data(); void changingModel(); void sortedEngineRowCount_data(); void sortedEngineRowCount(); void unsortedEngineRowCount_data(); void unsortedEngineRowCount(); void currentRow(); void sortedEngineMapFromSource(); void unsortedEngineMapFromSource(); void historySearch(); void modelDeletion(); void setters(); void dynamicSortOrder(); void disabledItems(); // task-specific tests below me void task178797_activatedOnReturn(); void task189564_omitNonSelectableItems(); void task246056_setCompletionPrefix(); void task250064_lostFocus(); void task253125_lineEditCompletion_data(); void task253125_lineEditCompletion(); void task247560_keyboardNavigation(); void QTBUG_14292_filesystem(); void QTBUG_52028_tabAutoCompletes(); void QTBUG_51889_activatedSentTwice(); void showPopupInGraphicsView(); void inheritedEventFilter(); private: void filter(bool assync = false); void testRowCount(); enum ModelType { CASE_SENSITIVELY_SORTED_MODEL, CASE_INSENSITIVELY_SORTED_MODEL, HISTORY_MODEL, FILESYSTEM_MODEL }; void setSourceModel(ModelType); CsvCompleter *completer = nullptr; QTreeWidget *treeWidget; const int completionColumn = 0; const int columnCount = 3; }; tst_QCompleter::tst_QCompleter() : treeWidget(new QTreeWidget) { treeWidget->move(100, 100); treeWidget->setColumnCount(columnCount); } tst_QCompleter::~tst_QCompleter() { delete treeWidget; delete completer; } #ifdef Q_OS_ANDROID static QString androidHomePath() { const auto homePaths = QStandardPaths::standardLocations(QStandardPaths::HomeLocation); QDir dir = homePaths.isEmpty() ? QDir() : homePaths.first(); dir.cdUp(); return dir.path(); } #endif void tst_QCompleter::setSourceModel(ModelType type) { QTreeWidgetItem *parent, *child; treeWidget->clear(); switch(type) { case CASE_SENSITIVELY_SORTED_MODEL: // Creates a tree model with top level items P0, P1, .., p0, p1,.. // Each of these items parents have children (for P0 - c0P0, c1P0,...) for (int i = 0; i < 2; i++) { for (int j = 0; j < 5; j++) { parent = new QTreeWidgetItem(treeWidget); const QString text = QLatin1Char(i == 0 ? 'P' : 'p') + QString::number(j); parent->setText(completionColumn, text); for (int k = 0; k < 5; k++) { child = new QTreeWidgetItem(parent); QString t = QLatin1Char('c') + QString::number(k) + text; child->setText(completionColumn, t); } } } completer->setModel(treeWidget->model()); completer->setCompletionColumn(completionColumn); break; case CASE_INSENSITIVELY_SORTED_MODEL: case HISTORY_MODEL: // Creates a tree model with top level items P0, p0, P1, p1,... // Each of these items have children c0p0, c1p0,.. for (int i = 0; i < 5; i++) { for (int j = 0; j < 2; j++) { parent = new QTreeWidgetItem(treeWidget); const QString text = QLatin1Char(j == 0 ? 'P' : 'p') + QString::number(i); parent->setText(completionColumn, text); for (int k = 0; k < 5; k++) { child = new QTreeWidgetItem(parent); QString t = QLatin1Char('c') + QString::number(k) + text; child->setText(completionColumn, t); } } } completer->setModel(treeWidget->model()); completer->setCompletionColumn(completionColumn); if (type == CASE_INSENSITIVELY_SORTED_MODEL) break; parent = new QTreeWidgetItem(treeWidget); parent->setText(completionColumn, QLatin1String("p3,c3p3")); parent = new QTreeWidgetItem(treeWidget); parent->setText(completionColumn, QLatin1String("p2,c4p2")); break; case FILESYSTEM_MODEL: completer->setCsvCompletion(false); { auto m = new QFileSystemModel(completer); #ifdef Q_OS_ANDROID // Android 11 and above doesn't allow accessing root filesystem as before, // so let's opt int for the app's home. m->setRootPath(androidHomePath()); #else m->setRootPath("/"); #endif completer->setModel(m); } completer->setCompletionColumn(0); break; default: qDebug() << "Invalid type"; break; } } void tst_QCompleter::filter(bool assync) { QFETCH(QString, filterText); QFETCH(const QString, step); QFETCH(QString, completion); QFETCH(QString, completionText); if (filterText.compare("FILTERING_OFF", Qt::CaseInsensitive) == 0) { completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); return; } int result = -1; const int attempts = assync ? 10 : 1; for (int times = 0; times < attempts; ++times) { completer->setCompletionPrefix(filterText); for (QChar s : step) { int row = completer->currentRow(); switch (s.toUpper().toLatin1()) { case 'P': --row; break; case 'N': ++row; break; case 'L': row = completer->completionCount() - 1; break; case 'F': row = 0; break; default: QFAIL(qPrintable(QString( "Problem with 'step' value in test data: %1 (only P, N, L and F are allowed)." ).arg(s))); } completer->setCurrentRow(row); } result = QString::compare(completer->currentCompletion(), completionText, completer->caseSensitivity()); if (result == 0) break; if (assync) QTest::qWait(50 * times); } QCOMPARE(result, 0); } // Testing get/set functions void tst_QCompleter::getSetCheck() { QStandardItemModel standardItemModel(3,3); QCompleter completer(&standardItemModel); // QString QCompleter::completionPrefix() // void QCompleter::setCompletionPrefix(QString) completer.setCompletionPrefix(QString("te")); QCOMPARE(completer.completionPrefix(), QString("te")); completer.setCompletionPrefix(QString()); QCOMPARE(completer.completionPrefix(), QString()); // ModelSorting QCompleter::modelSorting() // void QCompleter::setModelSorting(ModelSorting) completer.setModelSorting(QCompleter::CaseSensitivelySortedModel); QCOMPARE(completer.modelSorting(), QCompleter::CaseSensitivelySortedModel); completer.setModelSorting(QCompleter::CaseInsensitivelySortedModel); QCOMPARE(completer.modelSorting(), QCompleter::CaseInsensitivelySortedModel); completer.setModelSorting(QCompleter::UnsortedModel); QCOMPARE(completer.modelSorting(), QCompleter::UnsortedModel); // CompletionMode QCompleter::completionMode() // void QCompleter::setCompletionMode(CompletionMode) QCOMPARE(completer.completionMode(), QCompleter::PopupCompletion); // default value completer.setCompletionMode(QCompleter::UnfilteredPopupCompletion); QCOMPARE(completer.completionMode(), QCompleter::UnfilteredPopupCompletion); completer.setCompletionMode(QCompleter::InlineCompletion); QCOMPARE(completer.completionMode(), QCompleter::InlineCompletion); // int QCompleter::completionColumn() // void QCompleter::setCompletionColumn(int) completer.setCompletionColumn(2); QCOMPARE(completer.completionColumn(), 2); completer.setCompletionColumn(1); QCOMPARE(completer.completionColumn(), 1); // int QCompleter::completionRole() // void QCompleter::setCompletionRole(int) QCOMPARE(completer.completionRole(), static_cast(Qt::EditRole)); // default value completer.setCompletionRole(Qt::DisplayRole); QCOMPARE(completer.completionRole(), static_cast(Qt::DisplayRole)); // int QCompleter::maxVisibleItems() // void QCompleter::setMaxVisibleItems(int) QCOMPARE(completer.maxVisibleItems(), 7); // default value completer.setMaxVisibleItems(10); QCOMPARE(completer.maxVisibleItems(), 10); QTest::ignoreMessage(QtWarningMsg, "QCompleter::setMaxVisibleItems: " "Invalid max visible items (-2147483648) must be >= 0"); completer.setMaxVisibleItems(INT_MIN); QCOMPARE(completer.maxVisibleItems(), 10); // Cannot be set to something negative => old value // Qt::CaseSensitivity QCompleter::caseSensitivity() // void QCompleter::setCaseSensitivity(Qt::CaseSensitivity) QCOMPARE(completer.caseSensitivity(), Qt::CaseSensitive); // default value completer.setCaseSensitivity(Qt::CaseInsensitive); QCOMPARE(completer.caseSensitivity(), Qt::CaseInsensitive); // bool QCompleter::wrapAround() // void QCompleter::setWrapAround(bool) QCOMPARE(completer.wrapAround(), true); // default value completer.setWrapAround(false); QCOMPARE(completer.wrapAround(), false); #if QT_CONFIG(filesystemmodel) // QTBUG-54642, changing from QFileSystemModel to another model should restore role. completer.setCompletionRole(Qt::EditRole); QCOMPARE(completer.completionRole(), static_cast(Qt::EditRole)); // default value QFileSystemModel fileSystemModel; completer.setModel(&fileSystemModel); QCOMPARE(completer.completionRole(), static_cast(QFileSystemModel::FileNameRole)); completer.setModel(&standardItemModel); QCOMPARE(completer.completionRole(), static_cast(Qt::EditRole)); completer.setCompletionRole(Qt::ToolTipRole); QStandardItemModel standardItemModel2(2, 2); // Do not clobber a custom role when changing models completer.setModel(&standardItemModel2); QCOMPARE(completer.completionRole(), static_cast(Qt::ToolTipRole)); #endif // QT_CONFIG(filesystemmodel) } void tst_QCompleter::csMatchingOnCsSortedModel_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseSensitive); setSourceModel(CASE_SENSITIVELY_SORTED_MODEL); QTest::addColumn("filterText"); QTest::addColumn("step"); QTest::addColumn("completion"); QTest::addColumn("completionText"); #define ROWNAME(name) ((QByteArray(name) + ' ' + QByteArray::number(i)).constData()) for (int i = 0; i < 2; i++) { if (i == 1) QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << "" << "" << ""; // Plain text filter QTest::newRow(ROWNAME("()")) << "" << "" << "P0" << "P0"; QTest::newRow(ROWNAME("()F")) << "" << "F" << "P0" << "P0"; QTest::newRow(ROWNAME("()L")) << "" << "L" << "p4" << "p4"; QTest::newRow(ROWNAME("()N")) << "" << "N" << "P1" << "P1"; QTest::newRow(ROWNAME("(P)")) << "P" << "" << "P0" << "P0"; QTest::newRow(ROWNAME("(P)F")) << "P" << "" << "P0" << "P0"; QTest::newRow(ROWNAME("(P)L")) << "P" << "L" << "P4" << "P4"; QTest::newRow(ROWNAME("(p)")) << "p" << "" << "p0" << "p0"; QTest::newRow(ROWNAME("(p)N")) << "p" << "N" << "p1" << "p1"; QTest::newRow(ROWNAME("(p)NN")) << "p" << "NN" << "p2" << "p2"; QTest::newRow(ROWNAME("(p)NNN")) << "p" << "NNN" << "p3" << "p3"; QTest::newRow(ROWNAME("(p)NNNN")) << "p" << "NNNN" << "p4" << "p4"; QTest::newRow(ROWNAME("(p1)")) << "p1" << "" << "p1" << "p1"; QTest::newRow(ROWNAME("(p11)")) << "p11" << "" << "" << ""; // Tree filter QTest::newRow(ROWNAME("(P0,)")) << "P0," << "" << "c0P0" << "P0,c0P0"; QTest::newRow(ROWNAME("(P0,c)")) << "P0,c" << "" << "c0P0" << "P0,c0P0"; QTest::newRow(ROWNAME("(P0,c1)")) << "P0,c1" << "" << "c1P0" << "P0,c1P0"; QTest::newRow(ROWNAME("(P0,c3P0)")) << "P0,c3P0" << "" << "c3P0" << "P0,c3P0"; QTest::newRow(ROWNAME("(P3,c)F")) << "P3,c" << "F" << "c0P3" << "P3,c0P3"; QTest::newRow(ROWNAME("(P3,c)L")) << "P3,c" << "L" << "c4P3" << "P3,c4P3"; QTest::newRow(ROWNAME("(P3,c)N")) << "P3,c" << "N" << "c1P3" << "P3,c1P3"; QTest::newRow(ROWNAME("(P3,c)NN")) << "P3,c" << "NN" << "c2P3" << "P3,c2P3"; QTest::newRow(ROWNAME("(P3,,c)")) << "P3,,c" << "" << "" << ""; QTest::newRow(ROWNAME("(P3,c0P3,)")) << "P3,c0P3," << "" << "" << ""; QTest::newRow(ROWNAME("(P,)")) << "P," << "" << "" << ""; } #undef ROWNAME } void tst_QCompleter::csMatchingOnCsSortedModel() { filter(); } void tst_QCompleter::ciMatchingOnCiSortedModel_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseInsensitive); setSourceModel(CASE_INSENSITIVELY_SORTED_MODEL); QTest::addColumn("filterText"); QTest::addColumn("step"); QTest::addColumn("completion"); QTest::addColumn("completionText"); for (int i = 0; i < 2; i++) { if (i == 1) QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << "" << "" << ""; // Plain text filter QTest::newRow("()") << "" << "" << "P0" << "P0"; QTest::newRow("()F") << "" << "F" << "P0" << "P0"; QTest::newRow("()L") << "" << "L" << "p4" << "p4"; QTest::newRow("()N") << "" << "N" << "p0" << "p0"; QTest::newRow("(P)") << "P" << "" << "P0" << "P0"; QTest::newRow("(P)F") << "P" << "" << "P0" << "P0"; QTest::newRow("(P)L") << "P" << "L" << "p4" << "p4"; QTest::newRow("(p)") << "p" << "" << "P0" << "P0"; QTest::newRow("(p)N") << "p" << "N" << "p0" << "p0"; QTest::newRow("(p)NN") << "p" << "NN" << "P1" << "P1"; QTest::newRow("(p)NNN") << "p" << "NNN" << "p1" << "p1"; QTest::newRow("(p1)") << "p1" << "" << "P1" << "P1"; QTest::newRow("(p1)N") << "p1" << "N" << "p1" << "p1"; QTest::newRow("(p11)") << "p11" << "" << "" << ""; //// Tree filter QTest::newRow("(p0,)") << "p0," << "" << "c0P0" << "P0,c0P0"; QTest::newRow("(p0,c)") << "p0,c" << "" << "c0P0" << "P0,c0P0"; QTest::newRow("(p0,c1)") << "p0,c1" << "" << "c1P0" << "P0,c1P0"; QTest::newRow("(p0,c3P0)") << "p0,c3P0" << "" << "c3P0" << "P0,c3P0"; QTest::newRow("(p3,c)F") << "p3,c" << "F" << "c0P3" << "P3,c0P3"; QTest::newRow("(p3,c)L") << "p3,c" << "L" << "c4P3" << "P3,c4P3"; QTest::newRow("(p3,c)N") << "p3,c" << "N" << "c1P3" << "P3,c1P3"; QTest::newRow("(p3,c)NN") << "p3,c" << "NN" << "c2P3" << "P3,c2P3"; QTest::newRow("(p3,,c)") << "p3,,c" << "" << "" << ""; QTest::newRow("(p3,c0P3,)") << "p3,c0P3," << "" << "" << ""; QTest::newRow("(p,)") << "p," << "" << "" << ""; } } void tst_QCompleter::ciMatchingOnCiSortedModel() { filter(); } void tst_QCompleter::ciMatchingOnCsSortedModel_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); setSourceModel(CASE_SENSITIVELY_SORTED_MODEL); completer->setCaseSensitivity(Qt::CaseInsensitive); QTest::addColumn("filterText"); QTest::addColumn("step"); QTest::addColumn("completion"); QTest::addColumn("completionText"); for (int i = 0; i < 2; i++) { if (i == 1) QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << "" << "" << ""; // Plain text filter QTest::newRow("()") << "" << "" << "P0" << "P0"; QTest::newRow("()F") << "" << "F" << "P0" << "P0"; QTest::newRow("()L") << "" << "L" << "p4" << "p4"; QTest::newRow("(P)") << "P" << "" << "P0" << "P0"; QTest::newRow("(P)F") << "P" << "" << "P0" << "P0"; QTest::newRow("(P)L") << "P" << "L" << "p4" << "p4"; QTest::newRow("(p)") << "p" << "" << "P0" << "P0"; QTest::newRow("(p)N") << "p" << "N" << "P1" << "P1"; QTest::newRow("(p)NN") << "p" << "NN" << "P2" << "P2"; QTest::newRow("(p)NNN") << "p" << "NNN" << "P3" << "P3"; QTest::newRow("(p1)") << "p1" << "" << "P1" << "P1"; QTest::newRow("(p1)N") << "p1" << "N" << "p1" << "p1"; QTest::newRow("(p11)") << "p11" << "" << "" << ""; // Tree filter QTest::newRow("(p0,)") << "p0," << "" << "c0P0" << "P0,c0P0"; QTest::newRow("(p0,c)") << "p0,c" << "" << "c0P0" << "P0,c0P0"; QTest::newRow("(p0,c1)") << "p0,c1" << "" << "c1P0" << "P0,c1P0"; QTest::newRow("(p0,c3P0)") << "p0,c3P0" << "" << "c3P0" << "P0,c3P0"; QTest::newRow("(p3,c)F") << "p3,c" << "F" << "c0P3" << "P3,c0P3"; QTest::newRow("(p3,c)L") << "p3,c" << "L" << "c4P3" << "P3,c4P3"; QTest::newRow("(p3,c)N") << "p3,c" << "N" << "c1P3" << "P3,c1P3"; QTest::newRow("(p3,c)NN") << "p3,c" << "NN" << "c2P3" << "P3,c2P3"; QTest::newRow("(p3,,c)") << "p3,,c" << "" << "" << ""; QTest::newRow("(p3,c0P3,)") << "p3,c0P3," << "" << "" << ""; QTest::newRow("(p,)") << "p," << "" << "" << ""; } } void tst_QCompleter::ciMatchingOnCsSortedModel() { filter(); } void tst_QCompleter::csMatchingOnCiSortedModel_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); setSourceModel(CASE_INSENSITIVELY_SORTED_MODEL); completer->setCaseSensitivity(Qt::CaseSensitive); QTest::addColumn("filterText"); QTest::addColumn("step"); QTest::addColumn("completion"); QTest::addColumn("completionText"); for (int i = 0; i < 2; i++) { if (i == 1) QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << "" << "" << ""; // Plain text filter QTest::newRow("()") << "" << "" << "P0" << "P0"; QTest::newRow("()F") << "" << "F" << "P0" << "P0"; QTest::newRow("()L") << "" << "L" << "p4" << "p4"; QTest::newRow("()N") << "" << "N" << "p0" << "p0"; QTest::newRow("(P)") << "P" << "" << "P0" << "P0"; QTest::newRow("(P)F") << "P" << "" << "P0" << "P0"; QTest::newRow("(P)L") << "P" << "L" << "P4" << "P4"; QTest::newRow("(p)") << "p" << "" << "p0" << "p0"; QTest::newRow("(p)N") << "p" << "N" << "p1" << "p1"; QTest::newRow("(p)NN") << "p" << "NN" << "p2" << "p2"; QTest::newRow("(p)NNN") << "p" << "NNN" << "p3" << "p3"; QTest::newRow("(p1)") << "p1" << "" << "p1" << "p1"; QTest::newRow("(p11)") << "p11" << "" << "" << ""; //// Tree filter QTest::newRow("(p0,)") << "p0," << "" << "c0p0" << "p0,c0p0"; QTest::newRow("(p0,c)") << "p0,c" << "" << "c0p0" << "p0,c0p0"; QTest::newRow("(p0,c1)") << "p0,c1" << "" << "c1p0" << "p0,c1p0"; QTest::newRow("(p0,c3P0)") << "p0,c3p0" << "" << "c3p0" << "p0,c3p0"; QTest::newRow("(p3,c)F") << "p3,c" << "F" << "c0p3" << "p3,c0p3"; QTest::newRow("(p3,c)L") << "p3,c" << "L" << "c4p3" << "p3,c4p3"; QTest::newRow("(p3,c)N") << "p3,c" << "N" << "c1p3" << "p3,c1p3"; QTest::newRow("(p3,c)NN") << "p3,c" << "NN" << "c2p3" << "p3,c2p3"; QTest::newRow("(p3,,c)") << "p3,,c" << "" << "" << ""; QTest::newRow("(p3,c0P3,)") << "p3,c0P3," << "" << "" << ""; QTest::newRow("(p,)") << "p," << "" << "" << ""; QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << "" << "" << ""; } } void tst_QCompleter::csMatchingOnCiSortedModel() { filter(); } void tst_QCompleter::fileSystemModel_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); setSourceModel(FILESYSTEM_MODEL); completer->setCaseSensitivity(Qt::CaseInsensitive); QTest::addColumn("filterText"); QTest::addColumn("step"); QTest::addColumn("completion"); QTest::addColumn("completionText"); // NOTE: Add tests carefully, ensurely the paths exist on all systems // Output is the sourceText; currentCompletionText() for (int i = 0; i < 2; i++) { if (i == 1) QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << "" << "" << ""; #if defined(Q_OS_WIN) QTest::newRow("()") << "C" << "" << "C:" << "C:"; QTest::newRow("()") << "C:\\Program" << "" << "Program Files" << "C:\\Program Files"; #elif defined (Q_OS_MAC) QTest::newRow("()") << "" << "" << "/" << "/"; QTest::newRow("(/a)") << "/a" << "" << "Applications" << "/Applications"; // QTest::newRow("(/d)") << "/d" << "" << "Developer" << "/Developer"; #elif defined(Q_OS_ANDROID) QTest::newRow("()") << "" << "" << "/" << "/"; const QString androidDir = androidHomePath(); const QString tag = QStringLiteral("%1/fil").arg(androidDir); QTest::newRow(tag.toUtf8().data()) << tag << "" << "files" << androidDir + "/files"; #else QTest::newRow("()") << "" << "" << "/" << "/"; #if !defined(Q_OS_AIX) && !defined(Q_OS_HPUX) && !defined(Q_OS_QNX) QTest::newRow("(/h)") << "/h" << "" << "home" << "/home"; #endif QTest::newRow("(/et)") << "/et" << "" << "etc" << "/etc"; QTest::newRow("(/etc/passw)") << "/etc/passw" << "" << "passwd" << "/etc/passwd"; #endif } } void tst_QCompleter::fileSystemModel() { //QFileSystemModel is async. filter(true); } /*! In the file dialog, the completer uses the EditRole. See QTBUG-94799 */ void tst_QCompleter::fileDialog_data() { fileSystemModel_data(); completer->setCompletionRole(Qt::EditRole); } void tst_QCompleter::fileDialog() { //QFileSystemModel is async. filter(true); } void tst_QCompleter::changingModel_data() { } void tst_QCompleter::changingModel() { for (int i = 0; i < 2; i++) { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseSensitive); setSourceModel(CASE_SENSITIVELY_SORTED_MODEL); if (i == 1) { completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); } completer->setCompletionPrefix("p"); completer->setCurrentRow(completer->completionCount() - 1); QCOMPARE(completer->currentCompletion(), QString("p4")); // Test addition of data QTreeWidgetItem p5item; p5item.setText(completionColumn, "p5"); treeWidget->addTopLevelItem(&p5item); completer->setCompletionPrefix("p5"); QCOMPARE(completer->currentCompletion(), QString("p5")); // Test removal of data int p5index = treeWidget->indexOfTopLevelItem(&p5item); treeWidget->takeTopLevelItem(p5index); QCOMPARE(completer->currentCompletion(), QString("")); // Test clear treeWidget->clear(); QCOMPARE(completer->currentIndex(), QModelIndex()); } } void tst_QCompleter::testRowCount() { QFETCH(QString, filterText); QFETCH(bool, hasChildren); QFETCH(int, rowCount); QFETCH(int, completionCount); if (filterText.compare("FILTERING_OFF", Qt::CaseInsensitive) == 0) { completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); return; } completer->setCompletionPrefix(filterText); const QAbstractItemModel *completionModel = completer->completionModel(); QCOMPARE(completionModel->rowCount(), rowCount); QCOMPARE(completionCount, completionCount); QCOMPARE(completionModel->hasChildren(), hasChildren); QCOMPARE(completionModel->columnCount(), columnCount); } void tst_QCompleter::sortedEngineRowCount_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseInsensitive); setSourceModel(CASE_INSENSITIVELY_SORTED_MODEL); QTest::addColumn("filterText"); QTest::addColumn("hasChildren"); QTest::addColumn("rowCount"); QTest::addColumn("completionCount"); QTest::newRow("whatever") << "whatever" << false << 0 << 0; QTest::newRow("p") << "p" << true << 10 << 10; QTest::newRow("p1") << "p1" << true << 2 << 2; QTest::newRow("P1,") << "P1," << true << 5 << 5; QTest::newRow("P1,c") << "P1,c" << true << 5 << 5; QTest::newRow("P1,cc") << "P1,cc" << false << 0 << 0; QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << false << 0 << 0; QTest::newRow("whatever(filter off)") << "whatever" << true << 10 << 0; QTest::newRow("p1(filter off)") << "p1" << true << 10 << 2; QTest::newRow("p1,(filter off)") << "p1," << true << 5 << 5; QTest::newRow("p1,c(filter off)") << "p1,c" << true << 5 << 5; QTest::newRow("P1,cc(filter off)") << "P1,cc" << true << 5 << 0; } void tst_QCompleter::sortedEngineRowCount() { testRowCount(); } void tst_QCompleter::unsortedEngineRowCount_data() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseSensitive); setSourceModel(CASE_INSENSITIVELY_SORTED_MODEL); QTest::addColumn("filterText"); QTest::addColumn("hasChildren"); QTest::addColumn("rowCount"); QTest::addColumn("completionCount"); QTest::newRow("whatever") << "whatever" << false << 0 << 0; QTest::newRow("p") << "p" << true << 5 << 5; QTest::newRow("p1") << "p1" << true << 1 << 1; QTest::newRow("P1,") << "P1," << true << 5 << 5; QTest::newRow("P1,c") << "P1,c" << true << 5 << 5; QTest::newRow("P1,cc") << "P1,cc" << false << 0 << 0; QTest::newRow("FILTERING_OFF") << "FILTERING_OFF" << false << 0 << 0; QTest::newRow("whatever(filter off)") << "whatever" << true << 10 << 0; QTest::newRow("p1(filter off)") << "p1" << true << 10 << 1; QTest::newRow("p1,(filter off)") << "p1," << true << 5 << 5; QTest::newRow("p1,c(filter off)") << "p1,c" << true << 5 << 5; QTest::newRow("P1,cc(filter off)") << "P1,cc" << true << 5 << 0; } void tst_QCompleter::unsortedEngineRowCount() { testRowCount(); } void tst_QCompleter::currentRow() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseInsensitive); setSourceModel(CASE_INSENSITIVELY_SORTED_MODEL); // blank text completer->setCompletionPrefix(""); QCOMPARE(completer->currentRow(), 0); QVERIFY(completer->setCurrentRow(4)); QCOMPARE(completer->currentRow(), 4); QVERIFY(!completer->setCurrentRow(13)); QVERIFY(completer->setCurrentRow(4)); // some text completer->setCompletionPrefix("p1"); QCOMPARE(completer->currentRow(), 0); QVERIFY(completer->setCurrentRow(1)); QCOMPARE(completer->currentRow(), 1); QVERIFY(!completer->setCurrentRow(2)); QCOMPARE(completer->currentRow(), 1); // invalid text completer->setCompletionPrefix("well"); QCOMPARE(completer->currentRow(), -1); } void tst_QCompleter::sortedEngineMapFromSource() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseInsensitive); setSourceModel(CASE_INSENSITIVELY_SORTED_MODEL); QModelIndex si1, si2, pi; QAbstractItemModel *sourceModel = completer->model(); auto completionModel = qobject_cast(completer->completionModel()); // Fitering ON // empty si1 = sourceModel->index(4, completionColumn); // "P2" si2 = sourceModel->index(2, 0, si1); // "P2,c0P2" pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); // some text completer->setCompletionPrefix("p"); pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); // more text completer->setCompletionPrefix("p2"); pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); // invalid text completer->setCompletionPrefix("whatever"); pi = completionModel->mapFromSource(si1); QVERIFY(!pi.isValid()); // Fitering OFF completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); // empty si1 = sourceModel->index(4, completionColumn); // "P2" si2 = sourceModel->index(2, 0, si1); // "P2,c0P2" pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); // some text completer->setCompletionPrefix("p"); pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); // more text completer->setCompletionPrefix("p2"); pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); // invalid text completer->setCompletionPrefix("whatever"); pi = completionModel->mapFromSource(si1); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P2")); } void tst_QCompleter::unsortedEngineMapFromSource() { delete completer; completer = new CsvCompleter; completer->setCaseSensitivity(Qt::CaseInsensitive); setSourceModel(HISTORY_MODEL); // case insensitively sorted model completer->setModelSorting(QCompleter::UnsortedModel); QModelIndex si, si2, si3, pi; QAbstractItemModel *sourceModel = completer->model(); auto completionModel = qobject_cast(completer->completionModel()); si = sourceModel->index(6, completionColumn); // "P3" QCOMPARE(si.data().toString(), QLatin1String("P3")); si2 = sourceModel->index(3, completionColumn, sourceModel->index(0, completionColumn)); // "P0,c3P0" QCOMPARE(si2.data().toString(), QLatin1String("c3P0")); si3 = sourceModel->index(10, completionColumn); // "p3,c3p3" (history) QCOMPARE(si3.data().toString(), QLatin1String("p3,c3p3")); // FILTERING ON // empty pi = completionModel->mapFromSource(si); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P3")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); pi = completionModel->mapFromSource(si3); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("p3,c3p3")); // some text completer->setCompletionPrefix("P"); pi = completionModel->mapFromSource(si); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P3")); pi = completionModel->mapFromSource(si2); QCOMPARE(pi.isValid(), false); pi = completionModel->mapFromSource(si3); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("p3,c3p3")); // invalid text completer->setCompletionPrefix("whatever"); pi = completionModel->mapFromSource(si); QVERIFY(!pi.isValid()); pi = completionModel->mapFromSource(si2); QVERIFY(!pi.isValid()); // tree matching completer->setCompletionPrefix("P0,c"); pi = completionModel->mapFromSource(si); QVERIFY(!pi.isValid()); pi = completionModel->mapFromSource(si2); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("c3P0")); pi = completionModel->mapFromSource(si3); QCOMPARE(pi.isValid(), false); // more tree matching completer->setCompletionPrefix("p3,"); pi = completionModel->mapFromSource(si2); QVERIFY(!pi.isValid()); pi = completionModel->mapFromSource(si3); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("p3,c3p3")); // FILTERING OFF // empty completer->setCompletionPrefix(""); completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); pi = completionModel->mapFromSource(si); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P3")); // some text completer->setCompletionPrefix("P"); pi = completionModel->mapFromSource(si); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P3")); // more text completer->setCompletionPrefix("P3"); pi = completionModel->mapFromSource(si); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P3")); // invalid text completer->setCompletionPrefix("whatever"); pi = completionModel->mapFromSource(si); QCOMPARE(completionModel->data(pi).toString(), QLatin1String("P3")); } void tst_QCompleter::historySearch() { delete completer; completer = new CsvCompleter; completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseSensitive); setSourceModel(HISTORY_MODEL); auto completionModel = qobject_cast(completer->completionModel()); // "p3,c3p3" and "p2,c4p2" are added in the tree root // FILTERING ON // empty completer->setCurrentRow(10); QCOMPARE(completer->currentCompletion(), QLatin1String("p3,c3p3")); // more text completer->setCompletionPrefix("p2"); completer->setCurrentRow(1); QCOMPARE(completer->currentCompletion(), QLatin1String("p2,c4p2")); // comma separated text completer->setCompletionPrefix("p2,c4"); completer->setCurrentRow(1); QCOMPARE(completionModel->rowCount(), 2); QCOMPARE(completer->currentCompletion(), QLatin1String("p2,c4p2")); // invalid text completer->setCompletionPrefix("whatever"); QCOMPARE(completer->currentCompletion(), QString()); // FILTERING OFF completer->setCompletionPrefix(""); completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); completer->setCurrentRow(10); QCOMPARE(completer->currentCompletion(), QLatin1String("p3,c3p3")); // more text completer->setCompletionPrefix("p2"); completer->setCurrentRow(1); QCOMPARE(completer->currentCompletion(), QLatin1String("p2,c4p2")); // comma separated text completer->setCompletionPrefix("p2,c4"); QCOMPARE(completionModel->rowCount(), 5); // invalid text completer->setCompletionPrefix("whatever"); QCOMPARE(completer->currentCompletion(), QString()); } void tst_QCompleter::setters() { delete completer; completer = new CsvCompleter; QVERIFY(completer->popup() != nullptr); QPointer itemModel(new QStandardItemModel(1, 0, completer)); QAbstractItemModel *oldModel = completer->model(); completer->setModel(itemModel.data()); QVERIFY(completer->popup()->model() != oldModel); QCOMPARE(completer->popup()->model(), completer->completionModel()); completer->setPopup(new QListView); QCOMPARE(completer->popup()->model(), completer->completionModel()); completer->setModel(new QStringListModel(completer)); QVERIFY(itemModel.isNull()); // must have been deleted completer->setModel(nullptr); completer->setWidget(nullptr); } void tst_QCompleter::modelDeletion() { delete completer; completer = new CsvCompleter; const QStringList list = {"item1", "item2", "item3"}; std::unique_ptr listModel(new QStringListModel(list)); completer->setCompletionPrefix("i"); completer->setModel(listModel.get()); QCOMPARE(completer->completionCount(), 3); std::unique_ptr view(new QListView); view->setWindowTitle(QLatin1String(QTest::currentTestFunction())); view->setModel(completer->completionModel()); listModel.reset(); view->move(200, 200); view->show(); QCoreApplication::processEvents(); view.reset(); QCOMPARE(completer->completionCount(), 0); QCOMPARE(completer->currentRow(), -1); } void tst_QCompleter::multipleWidgets() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QStringList list; list << "item1" << "item2" << "item2"; QCompleter completer(list); completer.setCompletionMode(QCompleter::InlineCompletion); QWidget window; window.setWindowTitle(QLatin1String(QTest::currentTestFunction())); window.move(200, 200); window.show(); QApplicationPrivate::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QFocusEvent focusIn(QEvent::FocusIn); QFocusEvent focusOut(QEvent::FocusOut); auto comboBox = new QComboBox(&window); comboBox->setEditable(true); comboBox->setCompleter(&completer); comboBox->setFocus(); comboBox->show(); window.activateWindow(); QApplicationPrivate::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QCOMPARE(QApplication::focusWidget(), comboBox); comboBox->lineEdit()->setText("it"); QCOMPARE(comboBox->currentText(), QString("it")); // should not complete with setText QTest::keyPress(comboBox, 'e'); QCOMPARE(comboBox->currentText(), QString("item1")); comboBox->clearEditText(); QCOMPARE(comboBox->currentText(), QString("")); // combo box text must not change! auto lineEdit = new QLineEdit(&window); lineEdit->setCompleter(&completer); lineEdit->show(); lineEdit->setFocus(); QTRY_COMPARE(QApplication::focusWidget(), lineEdit); lineEdit->setText("it"); QCOMPARE(lineEdit->text(), QString("it")); // should not completer with setText QCOMPARE(comboBox->currentText(), QString("")); // combo box text must not change! QTest::keyPress(lineEdit, 'e'); QCOMPARE(lineEdit->text(), QString("item1")); QCOMPARE(comboBox->currentText(), QString("")); // combo box text must not change! } void tst_QCompleter::focusIn() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QCompleter completer({"item1", "item2", "item2"}); QWidget window; window.setWindowTitle(QLatin1String(QTest::currentTestFunction())); window.move(200, 200); window.show(); window.activateWindow(); QApplicationPrivate::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); auto comboBox = new QComboBox(&window); comboBox->setEditable(true); comboBox->setCompleter(&completer); comboBox->show(); comboBox->lineEdit()->setText("it"); auto lineEdit = new QLineEdit(&window); lineEdit->setCompleter(&completer); lineEdit->setText("it"); lineEdit->show(); auto lineEdit2 = new QLineEdit(&window); // has no completer! lineEdit2->show(); comboBox->setFocus(); QTRY_COMPARE(completer.widget(), comboBox); lineEdit->setFocus(); QTRY_COMPARE(completer.widget(), lineEdit); comboBox->setFocus(); QTRY_COMPARE(completer.widget(), comboBox); lineEdit2->setFocus(); QTRY_COMPARE(completer.widget(), comboBox); } void tst_QCompleter::dynamicSortOrder() { QStandardItemModel model; QCompleter completer(&model); completer.setModelSorting(QCompleter::CaseSensitivelySortedModel); QStandardItem *root = model.invisibleRootItem(); for (int i = 0; i < 20; i++) { root->appendRow(new QStandardItem(QString::number(i))); } root->appendRow(new QStandardItem("13")); root->sortChildren(0, Qt::AscendingOrder); completer.setCompletionPrefix("1"); QCOMPARE(completer.completionCount(), 12); completer.setCompletionPrefix("13"); QCOMPARE(completer.completionCount(), 2); root->sortChildren(0, Qt::DescendingOrder); completer.setCompletionPrefix("13"); QCOMPARE(completer.completionCount(), 2); completer.setCompletionPrefix("1"); QCOMPARE(completer.completionCount(), 12); } void tst_QCompleter::disabledItems() { QLineEdit lineEdit; lineEdit.setWindowTitle(QLatin1String(QTest::currentTestFunction())); auto model = new QStandardItemModel(&lineEdit); QStandardItem *suggestions = new QStandardItem("suggestions"); suggestions->setEnabled(false); model->appendRow(suggestions); model->appendRow(new QStandardItem("suggestions Enabled")); auto completer = new QCompleter(model, &lineEdit); QSignalSpy spy(completer, QOverload::of(&QCompleter::activated)); lineEdit.setCompleter(completer); lineEdit.move(200, 200); lineEdit.show(); QVERIFY(QTest::qWaitForWindowExposed(&lineEdit)); QTest::keyPress(&lineEdit, Qt::Key_S); QTest::keyPress(&lineEdit, Qt::Key_U); QAbstractItemView *view = lineEdit.completer()->popup(); QVERIFY(view->isVisible()); QTest::mouseClick(view->viewport(), Qt::LeftButton, {}, view->visualRect(view->model()->index(0, 0)).center()); QCOMPARE(spy.size(), 0); QVERIFY(view->isVisible()); QTest::mouseClick(view->viewport(), Qt::LeftButton, {}, view->visualRect(view->model()->index(1, 0)).center()); QCOMPARE(spy.size(), 1); QVERIFY(!view->isVisible()); } void tst_QCompleter::task178797_activatedOnReturn() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QLineEdit ledit; setFrameless(&ledit); auto completer = new QCompleter({"foobar1", "foobar2"}, &ledit); ledit.setCompleter(completer); QSignalSpy spy(completer, QOverload::of(&QCompleter::activated)); QCOMPARE(spy.size(), 0); ledit.move(200, 200); ledit.show(); QApplicationPrivate::setActiveWindow(&ledit); QVERIFY(QTest::qWaitForWindowActive(&ledit)); QTest::keyClick(&ledit, Qt::Key_F); QCoreApplication::processEvents(); QTRY_VERIFY(QApplication::activePopupWidget()); QTest::keyClick(QApplication::activePopupWidget(), Qt::Key_Down); QCoreApplication::processEvents(); QTest::keyClick(QApplication::activePopupWidget(), Qt::Key_Return); QCoreApplication::processEvents(); QCOMPARE(spy.size(), 1); } class task189564_StringListModel : public QStringListModel { const QString omitString; Qt::ItemFlags flags(const QModelIndex &index) const override { Qt::ItemFlags flags = Qt::ItemIsEnabled; if (data(index, Qt::DisplayRole).toString() != omitString) flags |= Qt::ItemIsSelectable; return flags; } public: explicit task189564_StringListModel(const QString &omitString, QObject *parent = nullptr) : QStringListModel(parent) , omitString(omitString) { } }; void tst_QCompleter::task189564_omitNonSelectableItems() { const QString prefix("a"); const int n = 5; QStringList strings; for (int i = 0; i < n; ++i) strings << prefix + QString::number(i); const QString omitString(strings.at(n / 2)); task189564_StringListModel model(omitString); model.setStringList(strings); QCompleter completer_(&model); completer_.setCompletionPrefix(prefix); QAbstractItemModel *completionModel = completer_.completionModel(); QModelIndexList matches1 = completionModel->match(completionModel->index(0, 0), Qt::DisplayRole, prefix, -1); QCOMPARE(matches1.size(), n - 1); QModelIndexList matches2 = completionModel->match(completionModel->index(0, 0), Qt::DisplayRole, omitString); QVERIFY(matches2.isEmpty()); } class task246056_ComboBox : public QComboBox { Q_OBJECT public: task246056_ComboBox() { setEditable(true); setInsertPolicy(NoInsert); if (completer()) { completer()->setCompletionMode(QCompleter::PopupCompletion); completer()->setCompletionRole(Qt::DisplayRole); connect(lineEdit(), &QLineEdit::editingFinished, this, &task246056_ComboBox::setCompletionPrefix); } } private slots: void setCompletionPrefix() { completer()->setCompletionPrefix(lineEdit()->text()); } }; void tst_QCompleter::task246056_setCompletionPrefix() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); task246056_ComboBox comboBox; setFrameless(&comboBox); QVERIFY(comboBox.completer()); comboBox.addItem(""); comboBox.addItem("a1"); comboBox.addItem("a2"); comboBox.move(200, 200); comboBox.show(); QApplicationPrivate::setActiveWindow(&comboBox); QVERIFY(QTest::qWaitForWindowActive(&comboBox)); QSignalSpy spy(comboBox.completer(), QOverload::of(&QCompleter::activated)); QTest::keyPress(&comboBox, 'a'); QTest::keyPress(comboBox.completer()->popup(), Qt::Key_Down); QTest::keyPress(comboBox.completer()->popup(), Qt::Key_Down); QTest::keyPress(comboBox.completer()->popup(), Qt::Key_Enter); // don't crash! QCOMPARE(spy.size(), 1); const auto index = spy.at(0).constFirst().toModelIndex(); QVERIFY(!index.isValid()); } class task250064_TextEdit : public QTextEdit { public: QCompleter *completer; task250064_TextEdit() { completer = new QCompleter(this); completer->setWidget(this); } void keyPressEvent (QKeyEvent *e) override { completer->popup(); QTextEdit::keyPressEvent(e); } }; class task250064_Widget : public QWidget { Q_OBJECT public: task250064_Widget() : m_textEdit(new task250064_TextEdit) { auto tabWidget = new QTabWidget; tabWidget->setFocusPolicy(Qt::ClickFocus); tabWidget->addTab(m_textEdit, "untitled"); auto layout = new QVBoxLayout(this); layout->addWidget(tabWidget); m_textEdit->setPlainText("bla bla bla"); m_textEdit->setFocus(); } void setCompletionModel() { m_textEdit->completer->setModel(nullptr); } QTextEdit *textEdit() const { return m_textEdit; } private: task250064_TextEdit *m_textEdit; }; void tst_QCompleter::task250064_lostFocus() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); task250064_Widget widget; widget.setWindowTitle(QLatin1String(QTest::currentTestFunction())); widget.show(); QApplicationPrivate::setActiveWindow(&widget); QVERIFY(QTest::qWaitForWindowActive(&widget)); QTest::keyPress(widget.textEdit(), 'a'); Qt::FocusPolicy origPolicy = widget.textEdit()->focusPolicy(); QVERIFY(origPolicy != Qt::NoFocus); widget.setCompletionModel(); QCOMPARE(widget.textEdit()->focusPolicy(), origPolicy); } void tst_QCompleter::task253125_lineEditCompletion_data() { QTest::addColumn("list"); QTest::addColumn("completionMode"); QStringList list = {"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi", "rho", "sigma", "tau", "upsilon", "phi", "chi", "psi", "omega"}; QTest::newRow("Inline") << list << QCompleter::InlineCompletion; QTest::newRow("Filtered") << list << QCompleter::PopupCompletion; QTest::newRow("Unfiltered") << list << QCompleter::UnfilteredPopupCompletion; } void tst_QCompleter::task253125_lineEditCompletion() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QFETCH(QStringList, list); QFETCH(QCompleter::CompletionMode, completionMode); std::unique_ptr model(new QStringListModel(list)); std::unique_ptr completer(new QCompleter(list)); completer->setModel(model.get()); completer->setCompletionMode(completionMode); QLineEdit edit; edit.setWindowTitle(QLatin1String(QTest::currentTestFunction()) + QLatin1String("::") + QLatin1String(QTest::currentDataTag())); edit.setCompleter(completer.get()); edit.move(200, 200); edit.show(); edit.setFocus(); QApplicationPrivate::setActiveWindow(&edit); QVERIFY(QTest::qWaitForWindowActive(&edit)); QTest::keyClick(&edit, 'i'); QCOMPARE(edit.completer()->currentCompletion(), QString("iota")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("iota")); edit.clear(); completer->setCompletionMode(QCompleter::PopupCompletion); completer->setFilterMode(Qt::MatchContains); QTest::keyClick(&edit, 't'); QCOMPARE(edit.completer()->currentCompletion(), QString("beta")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("beta")); edit.clear(); QTest::keyClick(&edit, 'p'); QTest::keyClick(&edit, 'p'); QCOMPARE(edit.completer()->currentCompletion(), QString("kappa")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("kappa")); edit.clear(); completer->setFilterMode(Qt::MatchStartsWith); QTest::keyClick(&edit, 't'); QCOMPARE(edit.completer()->currentCompletion(), QString("theta")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("theta")); edit.clear(); QTest::keyClick(&edit, 'p'); QTest::keyClick(&edit, 'p'); QCOMPARE(edit.completer()->currentCompletion(), QString()); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("pp")); edit.clear(); QTest::keyClick(&edit, 'u'); QTest::keyClick(&edit, 'p'); QTest::keyClick(&edit, 's'); QCOMPARE(edit.completer()->currentCompletion(), QString("upsilon")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("upsilon")); edit.clear(); completer->setFilterMode(Qt::MatchEndsWith); QTest::keyClick(&edit, 'm'); QTest::keyClick(&edit, 'm'); QTest::keyClick(&edit, 'a'); QCOMPARE(edit.completer()->currentCompletion(), QString("gamma")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("gamma")); edit.clear(); QTest::keyClick(&edit, 'g'); QTest::keyClick(&edit, 'm'); QTest::keyClick(&edit, 'a'); QCOMPARE(edit.completer()->currentCompletion(), QString("sigma")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("sigma")); edit.clear(); QTest::keyClick(&edit, 'm'); QTest::keyClick(&edit, 'm'); QCOMPARE(edit.completer()->currentCompletion(), QString()); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("mm")); edit.clear(); completer->setFilterMode(Qt::MatchStartsWith); QTest::keyClick(&edit, 'z'); QTest::keyClick(&edit, 'e'); QCOMPARE(edit.completer()->currentCompletion(), QString("zeta")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("zeta")); edit.clear(); completer->setFilterMode(Qt::MatchEndsWith); QTest::keyClick(&edit, 'e'); QTest::keyClick(&edit, 'g'); QTest::keyClick(&edit, 'a'); QCOMPARE(edit.completer()->currentCompletion(), QString("omega")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("omega")); edit.clear(); completer->setFilterMode(Qt::MatchContains); QTest::keyClick(&edit, 'c'); QTest::keyClick(&edit, 'r'); QCOMPARE(edit.completer()->currentCompletion(), QString("omicron")); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("omicron")); edit.clear(); QTest::keyClick(&edit, 'z'); QTest::keyClick(&edit, 'z'); QCOMPARE(edit.completer()->currentCompletion(), QString()); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("zz")); } void tst_QCompleter::task247560_keyboardNavigation() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QStandardItemModel model; for (int i = 0; i < 5; i++) { const QString prefix = QLatin1String("row ") + QString::number(i) + QLatin1String(" column "); for (int j = 0; j < 5; j++) model.setItem(i, j, new QStandardItem(prefix + QString::number(j))); } QCompleter completer(&model); completer.setCompletionColumn(1); QLineEdit edit; edit.setWindowTitle(QLatin1String(QTest::currentTestFunction())); edit.setCompleter(&completer); edit.move(200, 200); edit.show(); edit.setFocus(); QApplicationPrivate::setActiveWindow(&edit); QVERIFY(QTest::qWaitForWindowActive(&edit)); QTest::keyClick(&edit, 'r'); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Down); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("row 1 column 1")); edit.clear(); QTest::keyClick(&edit, 'r'); QTest::keyClick(edit.completer()->popup(), Qt::Key_Up); QTest::keyClick(edit.completer()->popup(), Qt::Key_Up); QTest::keyClick(edit.completer()->popup(), Qt::Key_Enter); QCOMPARE(edit.text(), QString("row 3 column 1")); } // Helpers for QTBUG_14292_filesystem: Recursion helper for below recurseTreeModel // Function to recurse over a tree model applying a function // taking index and depth, returning true to terminate recursion. template bool recurseTreeModel(const QAbstractItemModel &m, const QModelIndex &idx, Function f, int depth = 0) { if (idx.isValid() && f(idx, depth)) return true; const int rowCount = m.rowCount(idx); for (int row = 0; row < rowCount; ++row) if (recurseTreeModel(m, m.index(row, 0, idx), f, depth + 1)) return true; return false; } // Function applicable to the above recurseTreeModel() to search for a data item. class SearchFunction { public: SearchFunction(const QString &needle, int role = Qt::DisplayRole) : m_needle(needle), m_role(role) {} bool operator()(const QModelIndex &idx, int /* depth */) const { return idx.data(m_role).toString() == m_needle; } private: const QString m_needle; const int m_role; }; // Function applicable to the above recurseTreeModel() for debug output // of a model. class DebugFunction { public: DebugFunction(QDebug d) : m_d(d) {} bool operator()(const QModelIndex &idx, int depth) { for (int i = 0; i < 4 * depth; ++i) m_d << ' '; m_d << idx.data(QFileSystemModel::FileNameRole).toString() << '\n'; return false; } private: QDebug m_d; }; QDebug operator<<(QDebug d, const QAbstractItemModel &m) { QDebug dns = d.nospace(); dns << '\n'; recurseTreeModel(m, QModelIndex(), DebugFunction(dns)); return d; } static const char testDir1[] = "hello"; static const char testDir2[] = "holla"; // Helper for QTBUG_14292_filesystem, checking whether both // test directories are seen by the file system model for usage // with QTRY_VERIFY. static inline bool testFileSystemReady(const QAbstractItemModel &model) { return recurseTreeModel(model, QModelIndex(), SearchFunction(QLatin1String(testDir1), QFileSystemModel::FileNameRole)) && recurseTreeModel(model, QModelIndex(), SearchFunction(QLatin1String(testDir2), QFileSystemModel::FileNameRole)); } void tst_QCompleter::QTBUG_14292_filesystem() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); // This test tests whether the creation of subdirectories // does not cause completers based on file system models // to pop up the completion list due to file changed signals. FileSystem fs; QFileSystemModel model; QSignalSpy filesAddedSpy(&model, &QAbstractItemModel::rowsInserted); model.setRootPath(fs.path()); QVERIFY(fs.createDirectory(QLatin1String(testDir1))); QVERIFY(fs.createDirectory(QLatin1String(testDir2))); QLineEdit edit; edit.setWindowTitle(QLatin1String(QTest::currentTestFunction())); QCompleter comp; comp.setModel(&model); edit.setCompleter(&comp); edit.move(200, 200); edit.show(); QApplicationPrivate::setActiveWindow(&edit); QVERIFY(QTest::qWaitForWindowActive(&edit)); QCOMPARE(QApplication::activeWindow(), &edit); edit.setFocus(); QTRY_VERIFY(edit.hasFocus()); // Wait for all file system model slots/timers to trigger // until the model sees the subdirectories. QTRY_VERIFY(testFileSystemReady(model)); // But this should not cause the combo to pop up. QVERIFY(!comp.popup()->isVisible()); edit.setText(fs.path()); QTest::keyClick(&edit, '/'); QTRY_VERIFY(comp.popup()->isVisible()); QCOMPARE(comp.popup()->model()->rowCount(), 2); QApplication::processEvents(); QTest::keyClick(&edit, 'h'); QCOMPARE(comp.popup()->model()->rowCount(), 2); QTest::keyClick(&edit, 'e'); QCOMPARE(comp.popup()->model()->rowCount(), 1); QTest::keyClick(&edit, 'r'); QTRY_VERIFY(!comp.popup()->isVisible()); QVERIFY(fs.createDirectory(QStringLiteral("hero"))); if (!filesAddedSpy.wait()) QSKIP("File system model didn't notify about new directory, skipping tests"); QTRY_VERIFY(comp.popup()->isVisible()); QCOMPARE(comp.popup()->model()->rowCount(), 1); QTest::keyClick(comp.popup(), Qt::Key_Escape); QTRY_VERIFY(!comp.popup()->isVisible()); QVERIFY(fs.createDirectory(QStringLiteral("nothingThere"))); //there is no reason creating a file should open a popup, it did in Qt 4.7.0 if (!filesAddedSpy.wait()) QSKIP("File system model didn't notify about new file, skipping tests"); QVERIFY(!comp.popup()->isVisible()); QTest::keyClick(&edit, Qt::Key_Backspace); QTRY_VERIFY(comp.popup()->isVisible()); QCOMPARE(comp.popup()->model()->rowCount(), 2); QTest::keyClick(&edit, 'm'); QTRY_VERIFY(!comp.popup()->isVisible()); QWidget w; w.move(400, 200); w.show(); QApplicationPrivate::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); QVERIFY(!edit.hasFocus() && !comp.popup()->hasFocus()); QVERIFY(fs.createDirectory(QStringLiteral("hemo"))); //there is no reason creating a file should open a popup, it did in Qt 4.7.0 if (!filesAddedSpy.wait()) QSKIP("File system model didn't notify about new file, skipping tests"); QVERIFY(!comp.popup()->isVisible()); } void tst_QCompleter::QTBUG_52028_tabAutoCompletes() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QWidget w; w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.setLayout(new QVBoxLayout); QComboBox cbox; cbox.setEditable(true); cbox.setInsertPolicy(QComboBox::NoInsert); cbox.addItems({"foobar1", "foobar2", "hux"}); cbox.completer()->setCaseSensitivity(Qt::CaseInsensitive); cbox.completer()->setCompletionMode(QCompleter::PopupCompletion); w.layout()->addWidget(&cbox); // Adding a line edit is a good reason for tab to do something unrelated auto le = new QLineEdit; w.layout()->addWidget(le); const auto pos = w.screen()->availableGeometry().topLeft() + QPoint(200,200); w.move(pos); w.show(); QApplicationPrivate::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); QSignalSpy activatedSpy(&cbox, &QComboBox::activated); // Tab key will complete but not activate cbox.lineEdit()->clear(); QTest::keyClick(&cbox, Qt::Key_H); QVERIFY(cbox.completer()->popup()); QTRY_VERIFY(cbox.completer()->popup()->isVisible()); QTest::keyClick(cbox.completer()->popup(), Qt::Key_Tab); QCOMPARE(cbox.completer()->currentCompletion(), QLatin1String("hux")); QCOMPARE(activatedSpy.size(), 0); QEXPECT_FAIL("", "QTBUG-52028 will not be fixed today.", Abort); QCOMPARE(cbox.currentText(), QLatin1String("hux")); QCOMPARE(activatedSpy.size(), 0); QVERIFY(!le->hasFocus()); } void tst_QCompleter::QTBUG_51889_activatedSentTwice() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); QWidget w; w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.setLayout(new QVBoxLayout); QComboBox cbox; setFrameless(&cbox); cbox.setEditable(true); cbox.setInsertPolicy(QComboBox::NoInsert); cbox.addItems({"foobar1", "foobar2", "bar", "hux"}); cbox.completer()->setCaseSensitivity(Qt::CaseInsensitive); cbox.completer()->setCompletionMode(QCompleter::PopupCompletion); w.layout()->addWidget(&cbox); w.layout()->addWidget(new QLineEdit); const auto pos = w.screen()->availableGeometry().topLeft() + QPoint(200,200); w.move(pos); w.show(); QApplicationPrivate::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); QSignalSpy activatedSpy(&cbox, &QComboBox::activated); // Navigate + enter activates only once (first item) cbox.lineEdit()->clear(); QTest::keyClick(&cbox, Qt::Key_F); QVERIFY(cbox.completer()->popup()); QTRY_VERIFY(cbox.completer()->popup()->isVisible()); QTest::keyClick(cbox.completer()->popup(), Qt::Key_Down); QTest::keyClick(cbox.completer()->popup(), Qt::Key_Return); QTRY_COMPARE(activatedSpy.size(), 1); // Navigate + enter activates only once (non-first item) cbox.lineEdit()->clear(); activatedSpy.clear(); QTest::keyClick(&cbox, Qt::Key_H); QVERIFY(cbox.completer()->popup()); QTRY_VERIFY(cbox.completer()->popup()->isVisible()); QTest::keyClick(cbox.completer()->popup(), Qt::Key_Down); QTest::keyClick(cbox.completer()->popup(), Qt::Key_Return); QTRY_COMPARE(activatedSpy.size(), 1); // Full text + enter activates only once cbox.lineEdit()->clear(); activatedSpy.clear(); QTest::keyClicks(&cbox, "foobar1"); QVERIFY(cbox.completer()->popup()); QTRY_VERIFY(cbox.completer()->popup()->isVisible()); QTest::keyClick(&cbox, Qt::Key_Return); QTRY_COMPARE(activatedSpy.size(), 1); } void tst_QCompleter::showPopupInGraphicsView() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: Skip this test, see also QTBUG-107186"); QGraphicsView view; QGraphicsScene scene; view.setScene(&scene); QLineEdit lineEdit; lineEdit.setCompleter(new QCompleter({"alpha", "omega", "omicron", "zeta"})); scene.addWidget(&lineEdit); view.move(view.screen()->availableGeometry().topLeft() + QPoint(10, 10)); view.show(); QVERIFY(QTest::qWaitForWindowExposed(&view)); // show popup under line edit QTest::keyClick(&lineEdit, Qt::Key_A); QVERIFY(lineEdit.completer()->popup()); QVERIFY(lineEdit.completer()->popup()->isVisible()); QCOMPARE(lineEdit.completer()->popup()->geometry().x(), lineEdit.mapToGlobal(QPoint(0, 0)).x()); QVERIFY(lineEdit.completer()->popup()->geometry().top() >= (lineEdit.mapToGlobal(QPoint(0, lineEdit.height() - 1)).y() - 1)); // move widget to the bottom of screen lineEdit.clear(); int y = view.screen()->availableGeometry().height() - lineEdit.geometry().y(); view.move(view.geometry().x(), y); // show popup above line edit QTest::keyClick(&lineEdit, Qt::Key_A); QVERIFY(lineEdit.completer()->popup()->geometry().bottom() < lineEdit.mapToGlobal(QPoint(0, 0)).y()); } void tst_QCompleter::inheritedEventFilter() { class Completer : public QCompleter { public: explicit Completer(QWidget *parent) : QCompleter(parent) { Q_ASSERT(parent); setPopup(new QListView()); popup()->installEventFilter(this); } bool m_popupChildAdded = false; protected: bool eventFilter(QObject *watched, QEvent *event) override { if (watched == popup() && event->type() == QEvent::ChildAdded) m_popupChildAdded = true; return QCompleter::eventFilter(watched, event); } }; QComboBox comboBox; comboBox.setEditable(true); Completer *completer = new Completer(&comboBox); comboBox.setCompleter(completer); // comboBox.show() must not crash with an infinite loop in the event filter comboBox.show(); QVERIFY(QTest::qWaitForWindowExposed(&comboBox)); // Since event orders are platform dependent, only the the ChildAdded event is checked. QVERIFY(QTest::qWaitFor([completer](){return completer->m_popupChildAdded; })); } QTEST_MAIN(tst_QCompleter) #include "tst_qcompleter.moc"