/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the QtGui module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ /*! \class QCompleter \brief The QCompleter class provides completions based on an item model. \since 4.2 You can use QCompleter to provide auto completions in any Qt widget, such as QLineEdit and QComboBox. When the user starts typing a word, QCompleter suggests possible ways of completing the word, based on a word list. The word list is provided as a QAbstractItemModel. (For simple applications, where the word list is static, you can pass a QStringList to QCompleter's constructor.) \tableofcontents \section1 Basic Usage A QCompleter is used typically with a QLineEdit or QComboBox. For example, here's how to provide auto completions from a simple word list in a QLineEdit: \snippet doc/src/snippets/code/src_gui_util_qcompleter.cpp 0 A QFileSystemModel can be used to provide auto completion of file names. For example: \snippet doc/src/snippets/code/src_gui_util_qcompleter.cpp 1 To set the model on which QCompleter should operate, call setModel(). By default, QCompleter will attempt to match the \l {completionPrefix}{completion prefix} (i.e., the word that the user has started typing) against the Qt::EditRole data stored in column 0 in the model case sensitively. This can be changed using setCompletionRole(), setCompletionColumn(), and setCaseSensitivity(). If the model is sorted on the column and role that are used for completion, you can call setModelSorting() with either QCompleter::CaseSensitivelySortedModel or QCompleter::CaseInsensitivelySortedModel as the argument. On large models, this can lead to significant performance improvements, because QCompleter can then use binary search instead of linear search. The model can be a \l{QAbstractListModel}{list model}, a \l{QAbstractTableModel}{table model}, or a \l{QAbstractItemModel}{tree model}. Completion on tree models is slightly more involved and is covered in the \l{Handling Tree Models} section below. The completionMode() determines the mode used to provide completions to the user. \section1 Iterating Through Completions To retrieve a single candidate string, call setCompletionPrefix() with the text that needs to be completed and call currentCompletion(). You can iterate through the list of completions as below: \snippet doc/src/snippets/code/src_gui_util_qcompleter.cpp 2 completionCount() returns the total number of completions for the current prefix. completionCount() should be avoided when possible, since it requires a scan of the entire model. \section1 The Completion Model completionModel() return a list model that contains all possible completions for the current completion prefix, in the order in which they appear in the model. This model can be used to display the current completions in a custom view. Calling setCompletionPrefix() automatically refreshes the completion model. \section1 Handling Tree Models QCompleter can look for completions in tree models, assuming that any item (or sub-item or sub-sub-item) can be unambiguously represented as a string by specifying the path to the item. The completion is then performed one level at a time. Let's take the example of a user typing in a file system path. The model is a (hierarchical) QFileSystemModel. The completion occurs for every element in the path. For example, if the current text is \c C:\Wind, QCompleter might suggest \c Windows to complete the current path element. Similarly, if the current text is \c C:\Windows\Sy, QCompleter might suggest \c System. For this kind of completion to work, QCompleter needs to be able to split the path into a list of strings that are matched at each level. For \c C:\Windows\Sy, it needs to be split as "C:", "Windows" and "Sy". The default implementation of splitPath(), splits the completionPrefix using QDir::separator() if the model is a QFileSystemModel. To provide completions, QCompleter needs to know the path from an index. This is provided by pathFromIndex(). The default implementation of pathFromIndex(), returns the data for the \l{Qt::EditRole}{edit role} for list models and the absolute file path if the mode is a QFileSystemModel. \sa QAbstractItemModel, QLineEdit, QComboBox, {Completer Example} */ #include "qcompleter_p.h" #ifndef QT_NO_COMPLETER #include "QtGui/qscrollbar.h" #include "QtGui/qstringlistmodel.h" #include "QtGui/qdirmodel.h" #include "QtGui/qfilesystemmodel.h" #include "QtGui/qheaderview.h" #include "QtGui/qlistview.h" #include "QtGui/qapplication.h" #include "QtGui/qevent.h" #include "QtGui/qheaderview.h" #include "QtGui/qdesktopwidget.h" #include "QtGui/qlineedit.h" QT_BEGIN_NAMESPACE QCompletionModel::QCompletionModel(QCompleterPrivate *c, QObject *parent) : QAbstractProxyModel(*new QCompletionModelPrivate, parent), c(c), showAll(false) { createEngine(); } int QCompletionModel::columnCount(const QModelIndex &) const { Q_D(const QCompletionModel); return d->model->columnCount(); } void QCompletionModel::setSourceModel(QAbstractItemModel *source) { bool hadModel = (sourceModel() != 0); if (hadModel) QObject::disconnect(sourceModel(), 0, this, 0); QAbstractProxyModel::setSourceModel(source); if (source) { // TODO: Optimize updates in the source model connect(source, SIGNAL(modelReset()), this, SLOT(invalidate())); connect(source, SIGNAL(destroyed()), this, SLOT(modelDestroyed())); connect(source, SIGNAL(layoutChanged()), this, SLOT(invalidate())); connect(source, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted())); connect(source, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(invalidate())); connect(source, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(invalidate())); connect(source, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(invalidate())); connect(source, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(invalidate())); } invalidate(); } void QCompletionModel::createEngine() { bool sortedEngine = false; switch (c->sorting) { case QCompleter::UnsortedModel: sortedEngine = false; break; case QCompleter::CaseSensitivelySortedModel: sortedEngine = c->cs == Qt::CaseSensitive; break; case QCompleter::CaseInsensitivelySortedModel: sortedEngine = c->cs == Qt::CaseInsensitive; break; } if (sortedEngine) engine.reset(new QSortedModelEngine(c)); else engine.reset(new QUnsortedModelEngine(c)); } QModelIndex QCompletionModel::mapToSource(const QModelIndex& index) const { Q_D(const QCompletionModel); if (!index.isValid()) return QModelIndex(); int row; QModelIndex parent = engine->curParent; if (!showAll) { if (!engine->matchCount()) return QModelIndex(); Q_ASSERT(index.row() < engine->matchCount()); QIndexMapper& rootIndices = engine->historyMatch.indices; if (index.row() < rootIndices.count()) { row = rootIndices[index.row()]; parent = QModelIndex(); } else { row = engine->curMatch.indices[index.row() - rootIndices.count()]; } } else { row = index.row(); } return d->model->index(row, index.column(), parent); } QModelIndex QCompletionModel::mapFromSource(const QModelIndex& idx) const { if (!idx.isValid()) return QModelIndex(); int row = -1; if (!showAll) { if (!engine->matchCount()) return QModelIndex(); QIndexMapper& rootIndices = engine->historyMatch.indices; if (idx.parent().isValid()) { if (idx.parent() != engine->curParent) return QModelIndex(); } else { row = rootIndices.indexOf(idx.row()); if (row == -1 && engine->curParent.isValid()) return QModelIndex(); // source parent and our parent don't match } if (row == -1) { QIndexMapper& indices = engine->curMatch.indices; engine->filterOnDemand(idx.row() - indices.last()); row = indices.indexOf(idx.row()) + rootIndices.count(); } if (row == -1) return QModelIndex(); } else { if (idx.parent() != engine->curParent) return QModelIndex(); row = idx.row(); } return createIndex(row, idx.column()); } bool QCompletionModel::setCurrentRow(int row) { if (row < 0 || !engine->matchCount()) return false; if (row >= engine->matchCount()) engine->filterOnDemand(row + 1 - engine->matchCount()); if (row >= engine->matchCount()) // invalid row return false; engine->curRow = row; return true; } QModelIndex QCompletionModel::currentIndex(bool sourceIndex) const { if (!engine->matchCount()) return QModelIndex(); int row = engine->curRow; if (showAll) row = engine->curMatch.indices[engine->curRow]; QModelIndex idx = createIndex(row, c->column); if (!sourceIndex) return idx; return mapToSource(idx); } QModelIndex QCompletionModel::index(int row, int column, const QModelIndex& parent) const { Q_D(const QCompletionModel); if (row < 0 || column < 0 || column >= columnCount(parent) || parent.isValid()) return QModelIndex(); if (!showAll) { if (!engine->matchCount()) return QModelIndex(); if (row >= engine->historyMatch.indices.count()) { int want = row + 1 - engine->matchCount(); if (want > 0) engine->filterOnDemand(want); if (row >= engine->matchCount()) return QModelIndex(); } } else { if (row >= d->model->rowCount(engine->curParent)) return QModelIndex(); } return createIndex(row, column); } int QCompletionModel::completionCount() const { if (!engine->matchCount()) return 0; engine->filterOnDemand(INT_MAX); return engine->matchCount(); } int QCompletionModel::rowCount(const QModelIndex &parent) const { Q_D(const QCompletionModel); if (parent.isValid()) return 0; if (showAll) { // Show all items below current parent, even if we have no valid matches if (engine->curParts.count() != 1 && !engine->matchCount() && !engine->curParent.isValid()) return 0; return d->model->rowCount(engine->curParent); } return completionCount(); } void QCompletionModel::setFiltered(bool filtered) { if (showAll == !filtered) return; showAll = !filtered; resetModel(); } bool QCompletionModel::hasChildren(const QModelIndex &parent) const { Q_D(const QCompletionModel); if (parent.isValid()) return false; if (showAll) return d->model->hasChildren(mapToSource(parent)); if (!engine->matchCount()) return false; return true; } QVariant QCompletionModel::data(const QModelIndex& index, int role) const { Q_D(const QCompletionModel); return d->model->data(mapToSource(index), role); } void QCompletionModel::modelDestroyed() { QAbstractProxyModel::setSourceModel(0); // switch to static empty model invalidate(); } void QCompletionModel::rowsInserted() { invalidate(); emit rowsAdded(); } void QCompletionModel::invalidate() { engine->cache.clear(); filter(engine->curParts); } void QCompletionModel::filter(const QStringList& parts) { Q_D(QCompletionModel); engine->filter(parts); resetModel(); if (d->model->canFetchMore(engine->curParent)) d->model->fetchMore(engine->curParent); } void QCompletionModel::resetModel() { if (rowCount() == 0) { reset(); return; } emit layoutAboutToBeChanged(); QModelIndexList piList = persistentIndexList(); QModelIndexList empty; for (int i = 0; i < piList.size(); i++) empty.append(QModelIndex()); changePersistentIndexList(piList, empty); emit layoutChanged(); } ////////////////////////////////////////////////////////////////////////////// void QCompletionEngine::filter(const QStringList& parts) { const QAbstractItemModel *model = c->proxy->sourceModel(); curParts = parts; if (curParts.isEmpty()) curParts.append(QString()); curRow = -1; curParent = QModelIndex(); curMatch = QMatchData(); historyMatch = filterHistory(); if (!model) return; QModelIndex parent; for (int i = 0; i < curParts.count() - 1; i++) { QString part = curParts[i]; int emi = filter(part, parent, -1).exactMatchIndex; if (emi == -1) return; parent = model->index(emi, c->column, parent); } // Note that we set the curParent to a valid parent, even if we have no matches // When filtering is disabled, we show all the items under this parent curParent = parent; if (curParts.last().isEmpty()) curMatch = QMatchData(QIndexMapper(0, model->rowCount(curParent) - 1), -1, false); else curMatch = filter(curParts.last(), curParent, 1); // build at least one curRow = curMatch.isValid() ? 0 : -1; } QMatchData QCompletionEngine::filterHistory() { QAbstractItemModel *source = c->proxy->sourceModel(); if (curParts.count() <= 1 || c->proxy->showAll || !source) return QMatchData(); bool isDirModel = false; bool isFsModel = false; #ifndef QT_NO_DIRMODEL isDirModel = (qobject_cast(source) != 0); #endif #ifndef QT_NO_FILESYSTEMMODEL isFsModel = (qobject_cast(source) != 0); #endif QVector v; QIndexMapper im(v); QMatchData m(im, -1, true); for (int i = 0; i < source->rowCount(); i++) { QString str = source->index(i, c->column).data().toString(); if (str.startsWith(c->prefix, c->cs) #if (!defined(Q_OS_WIN) || defined(Q_OS_WINCE)) && !defined(Q_OS_SYMBIAN) && ((!isFsModel && !isDirModel) || QDir::toNativeSeparators(str) != QDir::separator()) #endif ) m.indices.append(i); } return m; } // Returns a match hint from the cache by chopping the search string bool QCompletionEngine::matchHint(QString part, const QModelIndex& parent, QMatchData *hint) { if (c->cs == Qt::CaseInsensitive) part = part.toLower(); const CacheItem& map = cache[parent]; QString key = part; while (!key.isEmpty()) { key.chop(1); if (map.contains(key)) { *hint = map[key]; return true; } } return false; } bool QCompletionEngine::lookupCache(QString part, const QModelIndex& parent, QMatchData *m) { if (c->cs == Qt::CaseInsensitive) part = part.toLower(); const CacheItem& map = cache[parent]; if (!map.contains(part)) return false; *m = map[part]; return true; } // When the cache size exceeds 1MB, it clears out about 1/2 of the cache. void QCompletionEngine::saveInCache(QString part, const QModelIndex& parent, const QMatchData& m) { QMatchData old = cache[parent].take(part); cost = cost + m.indices.cost() - old.indices.cost(); if (cost * sizeof(int) > 1024 * 1024) { QMap::iterator it1 = cache.begin(); while (it1 != cache.end()) { CacheItem& ci = it1.value(); int sz = ci.count()/2; QMap::iterator it2 = ci.begin(); int i = 0; while (it2 != ci.end() && i < sz) { cost -= it2.value().indices.cost(); it2 = ci.erase(it2); i++; } if (ci.count() == 0) { it1 = cache.erase(it1); } else { ++it1; } } } if (c->cs == Qt::CaseInsensitive) part = part.toLower(); cache[parent][part] = m; } /////////////////////////////////////////////////////////////////////////////////// QIndexMapper QSortedModelEngine::indexHint(QString part, const QModelIndex& parent, Qt::SortOrder order) { const QAbstractItemModel *model = c->proxy->sourceModel(); if (c->cs == Qt::CaseInsensitive) part = part.toLower(); const CacheItem& map = cache[parent]; // Try to find a lower and upper bound for the search from previous results int to = model->rowCount(parent) - 1; int from = 0; const CacheItem::const_iterator it = map.lowerBound(part); // look backward for first valid hint for(CacheItem::const_iterator it1 = it; it1-- != map.constBegin();) { const QMatchData& value = it1.value(); if (value.isValid()) { if (order == Qt::AscendingOrder) { from = value.indices.last() + 1; } else { to = value.indices.first() - 1; } break; } } // look forward for first valid hint for(CacheItem::const_iterator it2 = it; it2 != map.constEnd(); ++it2) { const QMatchData& value = it2.value(); if (value.isValid() && !it2.key().startsWith(part)) { if (order == Qt::AscendingOrder) { to = value.indices.first() - 1; } else { from = value.indices.first() + 1; } break; } } return QIndexMapper(from, to); } Qt::SortOrder QSortedModelEngine::sortOrder(const QModelIndex &parent) const { const QAbstractItemModel *model = c->proxy->sourceModel(); int rowCount = model->rowCount(parent); if (rowCount < 2) return Qt::AscendingOrder; QString first = model->data(model->index(0, c->column, parent), c->role).toString(); QString last = model->data(model->index(rowCount - 1, c->column, parent), c->role).toString(); return QString::compare(first, last, c->cs) <= 0 ? Qt::AscendingOrder : Qt::DescendingOrder; } QMatchData QSortedModelEngine::filter(const QString& part, const QModelIndex& parent, int) { const QAbstractItemModel *model = c->proxy->sourceModel(); QMatchData hint; if (lookupCache(part, parent, &hint)) return hint; QIndexMapper indices; Qt::SortOrder order = sortOrder(parent); if (matchHint(part, parent, &hint)) { if (!hint.isValid()) return QMatchData(); indices = hint.indices; } else { indices = indexHint(part, parent, order); } // binary search the model within 'indices' for 'part' under 'parent' int high = indices.to() + 1; int low = indices.from() - 1; int probe; QModelIndex probeIndex; QString probeData; while (high - low > 1) { probe = (high + low) / 2; probeIndex = model->index(probe, c->column, parent); probeData = model->data(probeIndex, c->role).toString(); const int cmp = QString::compare(probeData, part, c->cs); if ((order == Qt::AscendingOrder && cmp >= 0) || (order == Qt::DescendingOrder && cmp < 0)) { high = probe; } else { low = probe; } } if ((order == Qt::AscendingOrder && low == indices.to()) || (order == Qt::DescendingOrder && high == indices.from())) { // not found saveInCache(part, parent, QMatchData()); return QMatchData(); } probeIndex = model->index(order == Qt::AscendingOrder ? low+1 : high-1, c->column, parent); probeData = model->data(probeIndex, c->role).toString(); if (!probeData.startsWith(part, c->cs)) { saveInCache(part, parent, QMatchData()); return QMatchData(); } const bool exactMatch = QString::compare(probeData, part, c->cs) == 0; int emi = exactMatch ? (order == Qt::AscendingOrder ? low+1 : high-1) : -1; int from = 0; int to = 0; if (order == Qt::AscendingOrder) { from = low + 1; high = indices.to() + 1; low = from; } else { to = high - 1; low = indices.from() - 1; high = to; } while (high - low > 1) { probe = (high + low) / 2; probeIndex = model->index(probe, c->column, parent); probeData = model->data(probeIndex, c->role).toString(); const bool startsWith = probeData.startsWith(part, c->cs); if ((order == Qt::AscendingOrder && startsWith) || (order == Qt::DescendingOrder && !startsWith)) { low = probe; } else { high = probe; } } QMatchData m(order == Qt::AscendingOrder ? QIndexMapper(from, high - 1) : QIndexMapper(low+1, to), emi, false); saveInCache(part, parent, m); return m; } //////////////////////////////////////////////////////////////////////////////////////// int QUnsortedModelEngine::buildIndices(const QString& str, const QModelIndex& parent, int n, const QIndexMapper& indices, QMatchData* m) { Q_ASSERT(m->partial); Q_ASSERT(n != -1 || m->exactMatchIndex == -1); const QAbstractItemModel *model = c->proxy->sourceModel(); int i, count = 0; for (i = 0; i < indices.count() && count != n; ++i) { QModelIndex idx = model->index(indices[i], c->column, parent); QString data = model->data(idx, c->role).toString(); if (!data.startsWith(str, c->cs) || !(model->flags(idx) & Qt::ItemIsSelectable)) continue; m->indices.append(indices[i]); ++count; if (m->exactMatchIndex == -1 && QString::compare(data, str, c->cs) == 0) { m->exactMatchIndex = indices[i]; if (n == -1) return indices[i]; } } return indices[i-1]; } void QUnsortedModelEngine::filterOnDemand(int n) { Q_ASSERT(matchCount()); if (!curMatch.partial) return; Q_ASSERT(n >= -1); const QAbstractItemModel *model = c->proxy->sourceModel(); int lastRow = model->rowCount(curParent) - 1; QIndexMapper im(curMatch.indices.last() + 1, lastRow); int lastIndex = buildIndices(curParts.last(), curParent, n, im, &curMatch); curMatch.partial = (lastRow != lastIndex); saveInCache(curParts.last(), curParent, curMatch); } QMatchData QUnsortedModelEngine::filter(const QString& part, const QModelIndex& parent, int n) { QMatchData hint; QVector v; QIndexMapper im(v); QMatchData m(im, -1, true); const QAbstractItemModel *model = c->proxy->sourceModel(); bool foundInCache = lookupCache(part, parent, &m); if (!foundInCache) { if (matchHint(part, parent, &hint) && !hint.isValid()) return QMatchData(); } if (!foundInCache && !hint.isValid()) { const int lastRow = model->rowCount(parent) - 1; QIndexMapper all(0, lastRow); int lastIndex = buildIndices(part, parent, n, all, &m); m.partial = (lastIndex != lastRow); } else { if (!foundInCache) { // build from hint as much as we can buildIndices(part, parent, INT_MAX, hint.indices, &m); m.partial = hint.partial; } if (m.partial && ((n == -1 && m.exactMatchIndex == -1) || (m.indices.count() < n))) { // need more and have more const int lastRow = model->rowCount(parent) - 1; QIndexMapper rest(hint.indices.last() + 1, lastRow); int want = n == -1 ? -1 : n - m.indices.count(); int lastIndex = buildIndices(part, parent, want, rest, &m); m.partial = (lastRow != lastIndex); } } saveInCache(part, parent, m); return m; } /////////////////////////////////////////////////////////////////////////////// QCompleterPrivate::QCompleterPrivate() : widget(0), proxy(0), popup(0), cs(Qt::CaseSensitive), role(Qt::EditRole), column(0), maxVisibleItems(7), sorting(QCompleter::UnsortedModel), wrap(true), eatFocusOut(true) { } void QCompleterPrivate::init(QAbstractItemModel *m) { Q_Q(QCompleter); proxy = new QCompletionModel(this, q); QObject::connect(proxy, SIGNAL(rowsAdded()), q, SLOT(_q_autoResizePopup())); q->setModel(m); #ifdef QT_NO_LISTVIEW q->setCompletionMode(QCompleter::InlineCompletion); #else q->setCompletionMode(QCompleter::PopupCompletion); #endif // QT_NO_LISTVIEW } void QCompleterPrivate::setCurrentIndex(QModelIndex index, bool select) { Q_Q(QCompleter); if (!q->popup()) return; if (!select) { popup->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); } else { if (!index.isValid()) popup->selectionModel()->clear(); else popup->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); } index = popup->selectionModel()->currentIndex(); if (!index.isValid()) popup->scrollToTop(); else popup->scrollTo(index, QAbstractItemView::PositionAtTop); } void QCompleterPrivate::_q_completionSelected(const QItemSelection& selection) { QModelIndex index; if (!selection.indexes().isEmpty()) index = selection.indexes().first(); _q_complete(index, true); } void QCompleterPrivate::_q_complete(QModelIndex index, bool highlighted) { Q_Q(QCompleter); QString completion; if (!index.isValid() || (!proxy->showAll && (index.row() >= proxy->engine->matchCount()))) { completion = prefix; } else { if (!(index.flags() & Qt::ItemIsEnabled)) return; QModelIndex si = proxy->mapToSource(index); si = si.sibling(si.row(), column); // for clicked() completion = q->pathFromIndex(si); #ifndef QT_NO_DIRMODEL // add a trailing separator in inline if (mode == QCompleter::InlineCompletion) { if (qobject_cast(proxy->sourceModel()) && QFileInfo(completion).isDir()) completion += QDir::separator(); } #endif #ifndef QT_NO_FILESYSTEMMODEL // add a trailing separator in inline if (mode == QCompleter::InlineCompletion) { if (qobject_cast(proxy->sourceModel()) && QFileInfo(completion).isDir()) completion += QDir::separator(); } #endif } if (highlighted) { emit q->highlighted(index); emit q->highlighted(completion); } else { emit q->activated(index); emit q->activated(completion); } } void QCompleterPrivate::_q_autoResizePopup() { if (!popup || !popup->isVisible()) return; showPopup(popupRect); } void QCompleterPrivate::showPopup(const QRect& rect) { const QRect screen = QApplication::desktop()->availableGeometry(widget); Qt::LayoutDirection dir = widget->layoutDirection(); QPoint pos; int rh, w; int h = (popup->sizeHintForRow(0) * qMin(maxVisibleItems, popup->model()->rowCount()) + 3) + 3; QScrollBar *hsb = popup->horizontalScrollBar(); if (hsb && hsb->isVisible()) h += popup->horizontalScrollBar()->sizeHint().height(); if (rect.isValid()) { rh = rect.height(); w = rect.width(); pos = widget->mapToGlobal(dir == Qt::RightToLeft ? rect.bottomRight() : rect.bottomLeft()); } else { rh = widget->height(); pos = widget->mapToGlobal(QPoint(0, widget->height() - 2)); w = widget->width(); } if (w > screen.width()) w = screen.width(); if ((pos.x() + w) > (screen.x() + screen.width())) pos.setX(screen.x() + screen.width() - w); if (pos.x() < screen.x()) pos.setX(screen.x()); int top = pos.y() - rh - screen.top() + 2; int bottom = screen.bottom() - pos.y(); h = qMax(h, popup->minimumHeight()); if (h > bottom) { h = qMin(qMax(top, bottom), h); if (top > bottom) pos.setY(pos.y() - h - rh + 2); } popup->setGeometry(pos.x(), pos.y(), w, h); if (!popup->isVisible()) popup->show(); } void QCompleterPrivate::_q_fileSystemModelDirectoryLoaded(const QString &path) { Q_Q(QCompleter); QLineEdit *lineEdit = qobject_cast(widget); //the path given by QFileSystemModel does not end with / if (lineEdit && !lineEdit->text().isEmpty() && !q->completionPrefix().isEmpty() && q->completionPrefix() != path + QLatin1Char('/')) q->complete(); } /*! Constructs a completer object with the given \a parent. */ QCompleter::QCompleter(QObject *parent) : QObject(*new QCompleterPrivate(), parent) { Q_D(QCompleter); d->init(); } /*! Constructs a completer object with the given \a parent that provides completions from the specified \a model. */ QCompleter::QCompleter(QAbstractItemModel *model, QObject *parent) : QObject(*new QCompleterPrivate(), parent) { Q_D(QCompleter); d->init(model); } #ifndef QT_NO_STRINGLISTMODEL /*! Constructs a QCompleter object with the given \a parent that uses the specified \a list as a source of possible completions. */ QCompleter::QCompleter(const QStringList& list, QObject *parent) : QObject(*new QCompleterPrivate(), parent) { Q_D(QCompleter); d->init(new QStringListModel(list, this)); } #endif // QT_NO_STRINGLISTMODEL /*! Destroys the completer object. */ QCompleter::~QCompleter() { } /*! Sets the widget for which completion are provided for to \a widget. This function is automatically called when a QCompleter is set on a QLineEdit using QLineEdit::setCompleter() or on a QComboBox using QComboBox::setCompleter(). The widget needs to be set explicitly when providing completions for custom widgets. \sa widget(), setModel(), setPopup() */ void QCompleter::setWidget(QWidget *widget) { Q_D(QCompleter); if (d->widget) d->widget->removeEventFilter(this); d->widget = widget; if (d->widget) d->widget->installEventFilter(this); if (d->popup) { d->popup->hide(); d->popup->setFocusProxy(d->widget); } } /*! Returns the widget for which the completer object is providing completions. \sa setWidget() */ QWidget *QCompleter::widget() const { Q_D(const QCompleter); return d->widget; } /*! Sets the model which provides completions to \a model. The \a model can be list model or a tree model. If a model has been already previously set and it has the QCompleter as its parent, it is deleted. For convenience, if \a model is a QFileSystemModel, QCompleter switches its caseSensitivity to Qt::CaseInsensitive on Windows and Qt::CaseSensitive on other platforms. \sa completionModel(), modelSorting, {Handling Tree Models} */ void QCompleter::setModel(QAbstractItemModel *model) { Q_D(QCompleter); QAbstractItemModel *oldModel = d->proxy->sourceModel(); d->proxy->setSourceModel(model); if (d->popup) setPopup(d->popup); // set the model and make new connections if (oldModel && oldModel->QObject::parent() == this) delete oldModel; #ifndef QT_NO_DIRMODEL if (qobject_cast(model)) { #if (defined(Q_OS_WIN) && !defined(Q_OS_WINCE)) || defined(Q_OS_SYMBIAN) setCaseSensitivity(Qt::CaseInsensitive); #else setCaseSensitivity(Qt::CaseSensitive); #endif } #endif // QT_NO_DIRMODEL #ifndef QT_NO_FILESYSTEMMODEL QFileSystemModel *fsModel = qobject_cast(model); if (fsModel) { #if (defined(Q_OS_WIN) && !defined(Q_OS_WINCE)) || defined(Q_OS_SYMBIAN) setCaseSensitivity(Qt::CaseInsensitive); #else setCaseSensitivity(Qt::CaseSensitive); #endif setCompletionRole(QFileSystemModel::FileNameRole); connect(fsModel, SIGNAL(directoryLoaded(QString)), this, SLOT(_q_fileSystemModelDirectoryLoaded(QString))); } #endif // QT_NO_FILESYSTEMMODEL } /*! Returns the model that provides completion strings. \sa completionModel() */ QAbstractItemModel *QCompleter::model() const { Q_D(const QCompleter); return d->proxy->sourceModel(); } /*! \enum QCompleter::CompletionMode This enum specifies how completions are provided to the user. \value PopupCompletion Current completions are displayed in a popup window. \value InlineCompletion Completions appear inline (as selected text). \value UnfilteredPopupCompletion All possible completions are displayed in a popup window with the most likely suggestion indicated as current. \sa setCompletionMode() */ /*! \property QCompleter::completionMode \brief how the completions are provided to the user The default value is QCompleter::PopupCompletion. */ void QCompleter::setCompletionMode(QCompleter::CompletionMode mode) { Q_D(QCompleter); d->mode = mode; d->proxy->setFiltered(mode != QCompleter::UnfilteredPopupCompletion); if (mode == QCompleter::InlineCompletion) { if (d->widget) d->widget->removeEventFilter(this); if (d->popup) { d->popup->deleteLater(); d->popup = 0; } } else { if (d->widget) d->widget->installEventFilter(this); } } QCompleter::CompletionMode QCompleter::completionMode() const { Q_D(const QCompleter); return d->mode; } /*! Sets the popup used to display completions to \a popup. QCompleter takes ownership of the view. A QListView is automatically created when the completionMode() is set to QCompleter::PopupCompletion or QCompleter::UnfilteredPopupCompletion. The default popup displays the completionColumn(). Ensure that this function is called before the view settings are modified. This is required since view's properties may require that a model has been set on the view (for example, hiding columns in the view requires a model to be set on the view). \sa popup() */ void QCompleter::setPopup(QAbstractItemView *popup) { Q_D(QCompleter); Q_ASSERT(popup != 0); if (d->popup) { QObject::disconnect(d->popup->selectionModel(), 0, this, 0); QObject::disconnect(d->popup, 0, this, 0); } if (d->popup != popup) delete d->popup; if (popup->model() != d->proxy) popup->setModel(d->proxy); #if defined(Q_OS_MAC) && !defined(QT_MAC_USE_COCOA) popup->show(); #else popup->hide(); #endif Qt::FocusPolicy origPolicy = Qt::NoFocus; if (d->widget) origPolicy = d->widget->focusPolicy(); popup->setParent(0, Qt::Popup); popup->setFocusPolicy(Qt::NoFocus); if (d->widget) d->widget->setFocusPolicy(origPolicy); popup->setFocusProxy(d->widget); popup->installEventFilter(this); popup->setItemDelegate(new QCompleterItemDelegate(popup)); #ifndef QT_NO_LISTVIEW if (QListView *listView = qobject_cast(popup)) { listView->setModelColumn(d->column); } #endif QObject::connect(popup, SIGNAL(clicked(QModelIndex)), this, SLOT(_q_complete(QModelIndex))); QObject::connect(this, SIGNAL(activated(QModelIndex)), popup, SLOT(hide())); QObject::connect(popup->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(_q_completionSelected(QItemSelection))); d->popup = popup; } /*! Returns the popup used to display completions. \sa setPopup() */ QAbstractItemView *QCompleter::popup() const { Q_D(const QCompleter); #ifndef QT_NO_LISTVIEW if (!d->popup && completionMode() != QCompleter::InlineCompletion) { QListView *listView = new QListView; listView->setEditTriggers(QAbstractItemView::NoEditTriggers); listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); listView->setSelectionBehavior(QAbstractItemView::SelectRows); listView->setSelectionMode(QAbstractItemView::SingleSelection); listView->setModelColumn(d->column); QCompleter *that = const_cast(this); that->setPopup(listView); } #endif // QT_NO_LISTVIEW return d->popup; } /*! \reimp */ bool QCompleter::event(QEvent *ev) { return QObject::event(ev); } /*! \reimp */ bool QCompleter::eventFilter(QObject *o, QEvent *e) { Q_D(QCompleter); if (d->eatFocusOut && o == d->widget && e->type() == QEvent::FocusOut) { if (d->popup && d->popup->isVisible()) return true; } if (o != d->popup) return QObject::eventFilter(o, e); switch (e->type()) { case QEvent::KeyPress: { QKeyEvent *ke = static_cast(e); QModelIndex curIndex = d->popup->currentIndex(); QModelIndexList selList = d->popup->selectionModel()->selectedIndexes(); const int key = ke->key(); // In UnFilteredPopup mode, select the current item if ((key == Qt::Key_Up || key == Qt::Key_Down) && selList.isEmpty() && curIndex.isValid() && d->mode == QCompleter::UnfilteredPopupCompletion) { d->setCurrentIndex(curIndex); return true; } // Handle popup navigation keys. These are hardcoded because up/down might make the // widget do something else (lineedit cursor moves to home/end on mac, for instance) switch (key) { case Qt::Key_End: case Qt::Key_Home: if (ke->modifiers() & Qt::ControlModifier) return false; break; case Qt::Key_Up: if (!curIndex.isValid()) { int rowCount = d->proxy->rowCount(); QModelIndex lastIndex = d->proxy->index(rowCount - 1, d->column); d->setCurrentIndex(lastIndex); return true; } else if (curIndex.row() == 0) { if (d->wrap) d->setCurrentIndex(QModelIndex()); return true; } return false; case Qt::Key_Down: if (!curIndex.isValid()) { QModelIndex firstIndex = d->proxy->index(0, d->column); d->setCurrentIndex(firstIndex); return true; } else if (curIndex.row() == d->proxy->rowCount() - 1) { if (d->wrap) d->setCurrentIndex(QModelIndex()); return true; } return false; case Qt::Key_PageUp: case Qt::Key_PageDown: return false; } // Send the event to the widget. If the widget accepted the event, do nothing // If the widget did not accept the event, provide a default implementation d->eatFocusOut = false; (static_cast(d->widget))->event(ke); d->eatFocusOut = true; if (!d->widget || e->isAccepted() || !d->popup->isVisible()) { // widget lost focus, hide the popup if (d->widget && (!d->widget->hasFocus() #ifdef QT_KEYPAD_NAVIGATION || (QApplication::keypadNavigationEnabled() && !d->widget->hasEditFocus()) #endif )) d->popup->hide(); if (e->isAccepted()) return true; } // default implementation for keys not handled by the widget when popup is open switch (key) { #ifdef QT_KEYPAD_NAVIGATION case Qt::Key_Select: if (!QApplication::keypadNavigationEnabled()) break; #endif case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Tab: d->popup->hide(); if (curIndex.isValid()) d->_q_complete(curIndex); break; case Qt::Key_F4: if (ke->modifiers() & Qt::AltModifier) d->popup->hide(); break; case Qt::Key_Backtab: case Qt::Key_Escape: d->popup->hide(); break; default: break; } return true; } #ifdef QT_KEYPAD_NAVIGATION case QEvent::KeyRelease: { QKeyEvent *ke = static_cast(e); if (QApplication::keypadNavigationEnabled() && ke->key() == Qt::Key_Back) { // Send the event to the 'widget'. This is what we did for KeyPress, so we need // to do the same for KeyRelease, in case the widget's KeyPress event set // up something (such as a timer) that is relying on also receiving the // key release. I see this as a bug in Qt, and should really set it up for all // the affected keys. However, it is difficult to tell how this will affect // existing code, and I can't test for every combination! d->eatFocusOut = false; static_cast(d->widget)->event(ke); d->eatFocusOut = true; } break; } #endif case QEvent::MouseButtonPress: { #ifdef QT_KEYPAD_NAVIGATION if (QApplication::keypadNavigationEnabled()) { // if we've clicked in the widget (or its descendant), let it handle the click QWidget *source = qobject_cast(o); if (source) { QPoint pos = source->mapToGlobal((static_cast(e))->pos()); QWidget *target = QApplication::widgetAt(pos); if (target && (d->widget->isAncestorOf(target) || target == d->widget)) { d->eatFocusOut = false; static_cast(target)->event(e); d->eatFocusOut = true; return true; } } } #endif if (!d->popup->underMouse()) { d->popup->hide(); return true; } } return false; case QEvent::InputMethod: case QEvent::ShortcutOverride: QApplication::sendEvent(d->widget, e); break; default: return false; } return false; } /*! For QCompleter::PopupCompletion and QCompletion::UnfilteredPopupCompletion modes, calling this function displays the popup displaying the current completions. By default, if \a rect is not specified, the popup is displayed on the bottom of the widget(). If \a rect is specified the popup is displayed on the left edge of the rectangle. For QCompleter::InlineCompletion mode, the highlighted() signal is fired with the current completion. */ void QCompleter::complete(const QRect& rect) { Q_D(QCompleter); QModelIndex idx = d->proxy->currentIndex(false); if (d->mode == QCompleter::InlineCompletion) { if (idx.isValid()) d->_q_complete(idx, true); return; } Q_ASSERT(d->widget != 0); if ((d->mode == QCompleter::PopupCompletion && !idx.isValid()) || (d->mode == QCompleter::UnfilteredPopupCompletion && d->proxy->rowCount() == 0)) { if (d->popup) d->popup->hide(); // no suggestion, hide return; } popup(); if (d->mode == QCompleter::UnfilteredPopupCompletion) d->setCurrentIndex(idx, false); d->showPopup(rect); d->popupRect = rect; } /*! Sets the current row to the \a row specified. Returns true if successful; otherwise returns false. This function may be used along with currentCompletion() to iterate through all the possible completions. \sa currentCompletion(), completionCount() */ bool QCompleter::setCurrentRow(int row) { Q_D(QCompleter); return d->proxy->setCurrentRow(row); } /*! Returns the current row. \sa setCurrentRow() */ int QCompleter::currentRow() const { Q_D(const QCompleter); return d->proxy->currentRow(); } /*! Returns the number of completions for the current prefix. For an unsorted model with a large number of items this can be expensive. Use setCurrentRow() and currentCompletion() to iterate through all the completions. */ int QCompleter::completionCount() const { Q_D(const QCompleter); return d->proxy->completionCount(); } /*! \enum QCompleter::ModelSorting This enum specifies how the items in the model are sorted. \value UnsortedModel The model is unsorted. \value CaseSensitivelySortedModel The model is sorted case sensitively. \value CaseInsensitivelySortedModel The model is sorted case insensitively. \sa setModelSorting() */ /*! \property QCompleter::modelSorting \brief the way the model is sorted By default, no assumptions are made about the order of the items in the model that provides the completions. If the model's data for the completionColumn() and completionRole() is sorted in ascending order, you can set this property to \l CaseSensitivelySortedModel or \l CaseInsensitivelySortedModel. On large models, this can lead to significant performance improvements because the completer object can then use a binary search algorithm instead of linear search algorithm. The sort order (i.e ascending or descending order) of the model is determined dynamically by inspecting the contents of the model. \bold{Note:} The performance improvements described above cannot take place when the completer's \l caseSensitivity is different to the case sensitivity used by the model's when sorting. \sa setCaseSensitivity(), QCompleter::ModelSorting */ void QCompleter::setModelSorting(QCompleter::ModelSorting sorting) { Q_D(QCompleter); if (d->sorting == sorting) return; d->sorting = sorting; d->proxy->createEngine(); d->proxy->invalidate(); } QCompleter::ModelSorting QCompleter::modelSorting() const { Q_D(const QCompleter); return d->sorting; } /*! \property QCompleter::completionColumn \brief the column in the model in which completions are searched for. If the popup() is a QListView, it is automatically setup to display this column. By default, the match column is 0. \sa completionRole, caseSensitivity */ void QCompleter::setCompletionColumn(int column) { Q_D(QCompleter); if (d->column == column) return; #ifndef QT_NO_LISTVIEW if (QListView *listView = qobject_cast(d->popup)) listView->setModelColumn(column); #endif d->column = column; d->proxy->invalidate(); } int QCompleter::completionColumn() const { Q_D(const QCompleter); return d->column; } /*! \property QCompleter::completionRole \brief the item role to be used to query the contents of items for matching. The default role is Qt::EditRole. \sa completionColumn, caseSensitivity */ void QCompleter::setCompletionRole(int role) { Q_D(QCompleter); if (d->role == role) return; d->role = role; d->proxy->invalidate(); } int QCompleter::completionRole() const { Q_D(const QCompleter); return d->role; } /*! \property QCompleter::wrapAround \brief the completions wrap around when navigating through items \since 4.3 The default is true. */ void QCompleter::setWrapAround(bool wrap) { Q_D(QCompleter); if (d->wrap == wrap) return; d->wrap = wrap; } bool QCompleter::wrapAround() const { Q_D(const QCompleter); return d->wrap; } /*! \property QCompleter::maxVisibleItems \brief the maximum allowed size on screen of the completer, measured in items \since 4.6 By default, this property has a value of 7. */ int QCompleter::maxVisibleItems() const { Q_D(const QCompleter); return d->maxVisibleItems; } void QCompleter::setMaxVisibleItems(int maxItems) { Q_D(QCompleter); if (maxItems < 0) { qWarning("QCompleter::setMaxVisibleItems: " "Invalid max visible items (%d) must be >= 0", maxItems); return; } d->maxVisibleItems = maxItems; } /*! \property QCompleter::caseSensitivity \brief the case sensitivity of the matching The default is Qt::CaseSensitive. \sa completionColumn, completionRole, modelSorting */ void QCompleter::setCaseSensitivity(Qt::CaseSensitivity cs) { Q_D(QCompleter); if (d->cs == cs) return; d->cs = cs; d->proxy->createEngine(); d->proxy->invalidate(); } Qt::CaseSensitivity QCompleter::caseSensitivity() const { Q_D(const QCompleter); return d->cs; } /*! \property QCompleter::completionPrefix \brief the completion prefix used to provide completions. The completionModel() is updated to reflect the list of possible matches for \a prefix. */ void QCompleter::setCompletionPrefix(const QString &prefix) { Q_D(QCompleter); d->prefix = prefix; d->proxy->filter(splitPath(prefix)); } QString QCompleter::completionPrefix() const { Q_D(const QCompleter); return d->prefix; } /*! Returns the model index of the current completion in the completionModel(). \sa setCurrentRow(), currentCompletion(), model() */ QModelIndex QCompleter::currentIndex() const { Q_D(const QCompleter); return d->proxy->currentIndex(false); } /*! Returns the current completion string. This includes the \l completionPrefix. When used alongside setCurrentRow(), it can be used to iterate through all the matches. \sa setCurrentRow(), currentIndex() */ QString QCompleter::currentCompletion() const { Q_D(const QCompleter); return pathFromIndex(d->proxy->currentIndex(true)); } /*! Returns the completion model. The completion model is a read-only list model that contains all the possible matches for the current completion prefix. The completion model is auto-updated to reflect the current completions. \note The return value of this function is defined to be an QAbstractItemModel purely for generality. This actual kind of model returned is an instance of an QAbstractProxyModel subclass. \sa completionPrefix, model() */ QAbstractItemModel *QCompleter::completionModel() const { Q_D(const QCompleter); return d->proxy; } /*! Returns the path for the given \a index. The completer object uses this to obtain the completion text from the underlying model. The default implementation returns the \l{Qt::EditRole}{edit role} of the item for list models. It returns the absolute file path if the model is a QFileSystemModel. \sa splitPath() */ QString QCompleter::pathFromIndex(const QModelIndex& index) const { Q_D(const QCompleter); if (!index.isValid()) return QString(); QAbstractItemModel *sourceModel = d->proxy->sourceModel(); if (!sourceModel) return QString(); bool isDirModel = false; bool isFsModel = false; #ifndef QT_NO_DIRMODEL isDirModel = qobject_cast(d->proxy->sourceModel()) != 0; #endif #ifndef QT_NO_FILESYSTEMMODEL isFsModel = qobject_cast(d->proxy->sourceModel()) != 0; #endif if (!isDirModel && !isFsModel) return sourceModel->data(index, d->role).toString(); QModelIndex idx = index; QStringList list; do { QString t; if (isDirModel) t = sourceModel->data(idx, Qt::EditRole).toString(); #ifndef QT_NO_FILESYSTEMMODEL else t = sourceModel->data(idx, QFileSystemModel::FileNameRole).toString(); #endif list.prepend(t); QModelIndex parent = idx.parent(); idx = parent.sibling(parent.row(), index.column()); } while (idx.isValid()); #if (!defined(Q_OS_WIN) || defined(Q_OS_WINCE)) && !defined(Q_OS_SYMBIAN) if (list.count() == 1) // only the separator or some other text return list[0]; list[0].clear() ; // the join below will provide the separator #endif return list.join(QDir::separator()); } /*! Splits the given \a path into strings that are used to match at each level in the model(). The default implementation of splitPath() splits a file system path based on QDir::separator() when the sourceModel() is a QFileSystemModel. When used with list models, the first item in the returned list is used for matching. \sa pathFromIndex(), {Handling Tree Models} */ QStringList QCompleter::splitPath(const QString& path) const { bool isDirModel = false; bool isFsModel = false; #ifndef QT_NO_DIRMODEL Q_D(const QCompleter); isDirModel = qobject_cast(d->proxy->sourceModel()) != 0; #endif #ifndef QT_NO_FILESYSTEMMODEL #ifdef QT_NO_DIRMODEL Q_D(const QCompleter); #endif isFsModel = qobject_cast(d->proxy->sourceModel()) != 0; #endif if ((!isDirModel && !isFsModel) || path.isEmpty()) return QStringList(completionPrefix()); QString pathCopy = QDir::toNativeSeparators(path); QString sep = QDir::separator(); #if defined(Q_OS_SYMBIAN) if (pathCopy == QLatin1String("\\")) return QStringList(pathCopy); #elif defined(Q_OS_WIN) && !defined(Q_OS_WINCE) if (pathCopy == QLatin1String("\\") || pathCopy == QLatin1String("\\\\")) return QStringList(pathCopy); QString doubleSlash(QLatin1String("\\\\")); if (pathCopy.startsWith(doubleSlash)) pathCopy = pathCopy.mid(2); else doubleSlash.clear(); #endif QRegExp re(QLatin1Char('[') + QRegExp::escape(sep) + QLatin1Char(']')); QStringList parts = pathCopy.split(re); #if defined(Q_OS_SYMBIAN) // Do nothing #elif defined(Q_OS_WIN) && !defined(Q_OS_WINCE) if (!doubleSlash.isEmpty()) parts[0].prepend(doubleSlash); #else if (pathCopy[0] == sep[0]) // readd the "/" at the beginning as the split removed it parts[0] = QDir::fromNativeSeparators(QString(sep[0])); #endif return parts; } /*! \fn void QCompleter::activated(const QModelIndex& index) This signal is sent when an item in the popup() is activated by the user. (by clicking or pressing return). The item's \a index in the completionModel() is given. */ /*! \fn void QCompleter::activated(const QString &text) This signal is sent when an item in the popup() is activated by the user (by clicking or pressing return). The item's \a text is given. */ /*! \fn void QCompleter::highlighted(const QModelIndex& index) This signal is sent when an item in the popup() is highlighted by the user. It is also sent if complete() is called with the completionMode() set to QCompleter::InlineCompletion. The item's \a index in the completionModel() is given. */ /*! \fn void QCompleter::highlighted(const QString &text) This signal is sent when an item in the popup() is highlighted by the user. It is also sent if complete() is called with the completionMode() set to QCompleter::InlineCompletion. The item's \a text is given. */ QT_END_NAMESPACE #include "moc_qcompleter.cpp" #endif // QT_NO_COMPLETER