1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | /*! |
5 | \class QCompleter |
6 | \brief The QCompleter class provides completions based on an item model. |
7 | \since 4.2 |
8 | |
9 | \inmodule QtWidgets |
10 | |
11 | You can use QCompleter to provide auto completions in any Qt |
12 | widget, such as QLineEdit and QComboBox. |
13 | When the user starts typing a word, QCompleter suggests possible ways of |
14 | completing the word, based on a word list. The word list is |
15 | provided as a QAbstractItemModel. (For simple applications, where |
16 | the word list is static, you can pass a QStringList to |
17 | QCompleter's constructor.) |
18 | |
19 | \tableofcontents |
20 | |
21 | \section1 Basic Usage |
22 | |
23 | A QCompleter is used typically with a QLineEdit or QComboBox. |
24 | For example, here's how to provide auto completions from a simple |
25 | word list in a QLineEdit: |
26 | |
27 | \snippet code/src_gui_util_qcompleter.cpp 0 |
28 | |
29 | A QFileSystemModel can be used to provide auto completion of file names. |
30 | For example: |
31 | |
32 | \snippet code/src_gui_util_qcompleter.cpp 1 |
33 | |
34 | To set the model on which QCompleter should operate, call |
35 | setModel(). By default, QCompleter will attempt to match the \l |
36 | {completionPrefix}{completion prefix} (i.e., the word that the |
37 | user has started typing) against the Qt::EditRole data stored in |
38 | column 0 in the model case sensitively. This can be changed |
39 | using setCompletionRole(), setCompletionColumn(), and |
40 | setCaseSensitivity(). |
41 | |
42 | If the model is sorted on the column and role that are used for completion, |
43 | you can call setModelSorting() with either |
44 | QCompleter::CaseSensitivelySortedModel or |
45 | QCompleter::CaseInsensitivelySortedModel as the argument. On large models, |
46 | this can lead to significant performance improvements, because QCompleter |
47 | can then use binary search instead of linear search. The binary search only |
48 | works when the filterMode is Qt::MatchStartsWith. |
49 | |
50 | The model can be a \l{QAbstractListModel}{list model}, |
51 | a \l{QAbstractTableModel}{table model}, or a |
52 | \l{QAbstractItemModel}{tree model}. Completion on tree models |
53 | is slightly more involved and is covered in the \l{Handling |
54 | Tree Models} section below. |
55 | |
56 | The completionMode() determines the mode used to provide completions to |
57 | the user. |
58 | |
59 | \section1 Iterating Through Completions |
60 | |
61 | To retrieve a single candidate string, call setCompletionPrefix() |
62 | with the text that needs to be completed and call |
63 | currentCompletion(). You can iterate through the list of |
64 | completions as below: |
65 | |
66 | \snippet code/src_gui_util_qcompleter.cpp 2 |
67 | |
68 | completionCount() returns the total number of completions for the |
69 | current prefix. completionCount() should be avoided when possible, |
70 | since it requires a scan of the entire model. |
71 | |
72 | \section1 The Completion Model |
73 | |
74 | completionModel() return a list model that contains all possible |
75 | completions for the current completion prefix, in the order in which |
76 | they appear in the model. This model can be used to display the current |
77 | completions in a custom view. Calling setCompletionPrefix() automatically |
78 | refreshes the completion model. |
79 | |
80 | \section1 Handling Tree Models |
81 | |
82 | QCompleter can look for completions in tree models, assuming |
83 | that any item (or sub-item or sub-sub-item) can be unambiguously |
84 | represented as a string by specifying the path to the item. The |
85 | completion is then performed one level at a time. |
86 | |
87 | Let's take the example of a user typing in a file system path. |
88 | The model is a (hierarchical) QFileSystemModel. The completion |
89 | occurs for every element in the path. For example, if the current |
90 | text is \c C:\Wind, QCompleter might suggest \c Windows to |
91 | complete the current path element. Similarly, if the current text |
92 | is \c C:\Windows\Sy, QCompleter might suggest \c System. |
93 | |
94 | For this kind of completion to work, QCompleter needs to be able to |
95 | split the path into a list of strings that are matched at each level. |
96 | For \c C:\Windows\Sy, it needs to be split as "C:", "Windows" and "Sy". |
97 | The default implementation of splitPath(), splits the completionPrefix |
98 | using QDir::separator() if the model is a QFileSystemModel. |
99 | |
100 | To provide completions, QCompleter needs to know the path from an index. |
101 | This is provided by pathFromIndex(). The default implementation of |
102 | pathFromIndex(), returns the data for the \l{Qt::EditRole}{edit role} |
103 | for list models and the absolute file path if the mode is a QFileSystemModel. |
104 | |
105 | \sa QAbstractItemModel, QLineEdit, QComboBox, {Completer Example} |
106 | */ |
107 | |
108 | #include "qcompleter_p.h" |
109 | |
110 | #include "QtWidgets/qscrollbar.h" |
111 | #include "QtCore/qdir.h" |
112 | #if QT_CONFIG(stringlistmodel) |
113 | #include "QtCore/qstringlistmodel.h" |
114 | #endif |
115 | #if QT_CONFIG(filesystemmodel) |
116 | #include "QtGui/qfilesystemmodel.h" |
117 | #endif |
118 | #include "QtWidgets/qheaderview.h" |
119 | #if QT_CONFIG(listview) |
120 | #include "QtWidgets/qlistview.h" |
121 | #endif |
122 | #include "QtWidgets/qapplication.h" |
123 | #include "QtGui/qevent.h" |
124 | #include <private/qapplication_p.h> |
125 | #include <private/qwidget_p.h> |
126 | #if QT_CONFIG(lineedit) |
127 | #include "QtWidgets/qlineedit.h" |
128 | #endif |
129 | #include "QtCore/qdir.h" |
130 | |
131 | QT_BEGIN_NAMESPACE |
132 | |
133 | using namespace Qt::StringLiterals; |
134 | |
135 | QCompletionModel::QCompletionModel(QCompleterPrivate *c, QObject *parent) |
136 | : QAbstractProxyModel(*new QCompletionModelPrivate, parent), |
137 | c(c), showAll(false) |
138 | { |
139 | createEngine(); |
140 | } |
141 | |
142 | int QCompletionModel::columnCount(const QModelIndex &) const |
143 | { |
144 | Q_D(const QCompletionModel); |
145 | return d->model->columnCount(); |
146 | } |
147 | |
148 | void QCompletionModel::setSourceModel(QAbstractItemModel *source) |
149 | { |
150 | bool hadModel = (sourceModel() != nullptr); |
151 | |
152 | if (hadModel) |
153 | QObject::disconnect(sender: sourceModel(), signal: nullptr, receiver: this, member: nullptr); |
154 | |
155 | QAbstractProxyModel::setSourceModel(source); |
156 | |
157 | if (source) { |
158 | // TODO: Optimize updates in the source model |
159 | connect(sender: source, SIGNAL(modelReset()), receiver: this, SLOT(invalidate())); |
160 | connect(sender: source, SIGNAL(destroyed()), receiver: this, SLOT(modelDestroyed())); |
161 | connect(sender: source, SIGNAL(layoutChanged()), receiver: this, SLOT(invalidate())); |
162 | connect(sender: source, SIGNAL(rowsInserted(QModelIndex,int,int)), receiver: this, SLOT(rowsInserted())); |
163 | connect(sender: source, SIGNAL(rowsRemoved(QModelIndex,int,int)), receiver: this, SLOT(invalidate())); |
164 | connect(sender: source, SIGNAL(columnsInserted(QModelIndex,int,int)), receiver: this, SLOT(invalidate())); |
165 | connect(sender: source, SIGNAL(columnsRemoved(QModelIndex,int,int)), receiver: this, SLOT(invalidate())); |
166 | connect(sender: source, SIGNAL(dataChanged(QModelIndex,QModelIndex)), receiver: this, SLOT(invalidate())); |
167 | } |
168 | |
169 | invalidate(); |
170 | } |
171 | |
172 | void QCompletionModel::createEngine() |
173 | { |
174 | bool sortedEngine = false; |
175 | if (c->filterMode == Qt::MatchStartsWith) { |
176 | switch (c->sorting) { |
177 | case QCompleter::UnsortedModel: |
178 | sortedEngine = false; |
179 | break; |
180 | case QCompleter::CaseSensitivelySortedModel: |
181 | sortedEngine = c->cs == Qt::CaseSensitive; |
182 | break; |
183 | case QCompleter::CaseInsensitivelySortedModel: |
184 | sortedEngine = c->cs == Qt::CaseInsensitive; |
185 | break; |
186 | } |
187 | } |
188 | |
189 | if (sortedEngine) |
190 | engine.reset(other: new QSortedModelEngine(c)); |
191 | else |
192 | engine.reset(other: new QUnsortedModelEngine(c)); |
193 | } |
194 | |
195 | QModelIndex QCompletionModel::mapToSource(const QModelIndex& index) const |
196 | { |
197 | Q_D(const QCompletionModel); |
198 | if (!index.isValid()) |
199 | return engine->curParent; |
200 | |
201 | int row; |
202 | QModelIndex parent = engine->curParent; |
203 | if (!showAll) { |
204 | if (!engine->matchCount()) |
205 | return QModelIndex(); |
206 | Q_ASSERT(index.row() < engine->matchCount()); |
207 | QIndexMapper& rootIndices = engine->historyMatch.indices; |
208 | if (index.row() < rootIndices.count()) { |
209 | row = rootIndices[index.row()]; |
210 | parent = QModelIndex(); |
211 | } else { |
212 | row = engine->curMatch.indices[index.row() - rootIndices.count()]; |
213 | } |
214 | } else { |
215 | row = index.row(); |
216 | } |
217 | |
218 | return d->model->index(row, column: index.column(), parent); |
219 | } |
220 | |
221 | QModelIndex QCompletionModel::mapFromSource(const QModelIndex& idx) const |
222 | { |
223 | if (!idx.isValid()) |
224 | return QModelIndex(); |
225 | |
226 | int row = -1; |
227 | if (!showAll) { |
228 | if (!engine->matchCount()) |
229 | return QModelIndex(); |
230 | |
231 | QIndexMapper& rootIndices = engine->historyMatch.indices; |
232 | if (idx.parent().isValid()) { |
233 | if (idx.parent() != engine->curParent) |
234 | return QModelIndex(); |
235 | } else { |
236 | row = rootIndices.indexOf(x: idx.row()); |
237 | if (row == -1 && engine->curParent.isValid()) |
238 | return QModelIndex(); // source parent and our parent don't match |
239 | } |
240 | |
241 | if (row == -1) { |
242 | QIndexMapper& indices = engine->curMatch.indices; |
243 | engine->filterOnDemand(idx.row() - indices.last()); |
244 | row = indices.indexOf(x: idx.row()) + rootIndices.count(); |
245 | } |
246 | |
247 | if (row == -1) |
248 | return QModelIndex(); |
249 | } else { |
250 | if (idx.parent() != engine->curParent) |
251 | return QModelIndex(); |
252 | row = idx.row(); |
253 | } |
254 | |
255 | return createIndex(arow: row, acolumn: idx.column()); |
256 | } |
257 | |
258 | bool QCompletionModel::setCurrentRow(int row) |
259 | { |
260 | if (row < 0 || !engine->matchCount()) |
261 | return false; |
262 | |
263 | if (row >= engine->matchCount()) |
264 | engine->filterOnDemand(row + 1 - engine->matchCount()); |
265 | |
266 | if (row >= engine->matchCount()) // invalid row |
267 | return false; |
268 | |
269 | engine->curRow = row; |
270 | return true; |
271 | } |
272 | |
273 | QModelIndex QCompletionModel::currentIndex(bool sourceIndex) const |
274 | { |
275 | if (!engine->matchCount()) |
276 | return QModelIndex(); |
277 | |
278 | int row = engine->curRow; |
279 | if (showAll) |
280 | row = engine->curMatch.indices[engine->curRow]; |
281 | |
282 | QModelIndex idx = createIndex(arow: row, acolumn: c->column); |
283 | if (!sourceIndex) |
284 | return idx; |
285 | return mapToSource(index: idx); |
286 | } |
287 | |
288 | QModelIndex QCompletionModel::index(int row, int column, const QModelIndex& parent) const |
289 | { |
290 | Q_D(const QCompletionModel); |
291 | if (row < 0 || column < 0 || column >= columnCount(parent) || parent.isValid()) |
292 | return QModelIndex(); |
293 | |
294 | if (!showAll) { |
295 | if (!engine->matchCount()) |
296 | return QModelIndex(); |
297 | if (row >= engine->historyMatch.indices.count()) { |
298 | int want = row + 1 - engine->matchCount(); |
299 | if (want > 0) |
300 | engine->filterOnDemand(want); |
301 | if (row >= engine->matchCount()) |
302 | return QModelIndex(); |
303 | } |
304 | } else { |
305 | if (row >= d->model->rowCount(parent: engine->curParent)) |
306 | return QModelIndex(); |
307 | } |
308 | |
309 | return createIndex(arow: row, acolumn: column); |
310 | } |
311 | |
312 | int QCompletionModel::completionCount() const |
313 | { |
314 | if (!engine->matchCount()) |
315 | return 0; |
316 | |
317 | engine->filterOnDemand(INT_MAX); |
318 | return engine->matchCount(); |
319 | } |
320 | |
321 | int QCompletionModel::rowCount(const QModelIndex &parent) const |
322 | { |
323 | Q_D(const QCompletionModel); |
324 | if (parent.isValid()) |
325 | return 0; |
326 | |
327 | if (showAll) { |
328 | // Show all items below current parent, even if we have no valid matches |
329 | if (engine->curParts.size() != 1 && !engine->matchCount() |
330 | && !engine->curParent.isValid()) |
331 | return 0; |
332 | return d->model->rowCount(parent: engine->curParent); |
333 | } |
334 | |
335 | return completionCount(); |
336 | } |
337 | |
338 | void QCompletionModel::setFiltered(bool filtered) |
339 | { |
340 | if (showAll == !filtered) |
341 | return; |
342 | beginResetModel(); |
343 | showAll = !filtered; |
344 | endResetModel(); |
345 | } |
346 | |
347 | bool QCompletionModel::hasChildren(const QModelIndex &parent) const |
348 | { |
349 | Q_D(const QCompletionModel); |
350 | if (parent.isValid()) |
351 | return false; |
352 | |
353 | if (showAll) |
354 | return d->model->hasChildren(parent: mapToSource(index: parent)); |
355 | |
356 | if (!engine->matchCount()) |
357 | return false; |
358 | |
359 | return true; |
360 | } |
361 | |
362 | QVariant QCompletionModel::data(const QModelIndex& index, int role) const |
363 | { |
364 | Q_D(const QCompletionModel); |
365 | return d->model->data(index: mapToSource(index), role); |
366 | } |
367 | |
368 | void QCompletionModel::modelDestroyed() |
369 | { |
370 | QAbstractProxyModel::setSourceModel(nullptr); // switch to static empty model |
371 | invalidate(); |
372 | } |
373 | |
374 | void QCompletionModel::rowsInserted() |
375 | { |
376 | invalidate(); |
377 | emit rowsAdded(); |
378 | } |
379 | |
380 | void QCompletionModel::invalidate() |
381 | { |
382 | engine->cache.clear(); |
383 | filter(parts: engine->curParts); |
384 | } |
385 | |
386 | void QCompletionModel::filter(const QStringList& parts) |
387 | { |
388 | Q_D(QCompletionModel); |
389 | beginResetModel(); |
390 | engine->filter(parts); |
391 | endResetModel(); |
392 | |
393 | if (d->model->canFetchMore(parent: engine->curParent)) |
394 | d->model->fetchMore(parent: engine->curParent); |
395 | } |
396 | |
397 | ////////////////////////////////////////////////////////////////////////////// |
398 | void QCompletionEngine::filter(const QStringList& parts) |
399 | { |
400 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
401 | curParts = parts; |
402 | if (curParts.isEmpty()) |
403 | curParts.append(t: QString()); |
404 | |
405 | curRow = -1; |
406 | curParent = QModelIndex(); |
407 | curMatch = QMatchData(); |
408 | historyMatch = filterHistory(); |
409 | |
410 | if (!model) |
411 | return; |
412 | |
413 | QModelIndex parent; |
414 | for (int i = 0; i < curParts.size() - 1; i++) { |
415 | QString part = curParts.at(i); |
416 | int emi = filter(part, parent, -1).exactMatchIndex; |
417 | if (emi == -1) |
418 | return; |
419 | parent = model->index(row: emi, column: c->column, parent); |
420 | } |
421 | |
422 | // Note that we set the curParent to a valid parent, even if we have no matches |
423 | // When filtering is disabled, we show all the items under this parent |
424 | curParent = parent; |
425 | if (curParts.constLast().isEmpty()) |
426 | curMatch = QMatchData(QIndexMapper(0, model->rowCount(parent: curParent) - 1), -1, false); |
427 | else |
428 | curMatch = filter(curParts.constLast(), curParent, 1); // build at least one |
429 | curRow = curMatch.isValid() ? 0 : -1; |
430 | } |
431 | |
432 | QMatchData QCompletionEngine::filterHistory() |
433 | { |
434 | QAbstractItemModel *source = c->proxy->sourceModel(); |
435 | if (curParts.size() <= 1 || c->proxy->showAll || !source) |
436 | return QMatchData(); |
437 | |
438 | #if QT_CONFIG(filesystemmodel) |
439 | const bool isFsModel = (qobject_cast<QFileSystemModel *>(object: source) != nullptr); |
440 | #else |
441 | const bool isFsModel = false; |
442 | #endif |
443 | Q_UNUSED(isFsModel); |
444 | QList<int> v; |
445 | QIndexMapper im(v); |
446 | QMatchData m(im, -1, true); |
447 | |
448 | for (int i = 0; i < source->rowCount(); i++) { |
449 | QString str = source->index(row: i, column: c->column).data().toString(); |
450 | if (str.startsWith(s: c->prefix, cs: c->cs) |
451 | #if !defined(Q_OS_WIN) |
452 | && (!isFsModel || QDir::toNativeSeparators(pathName: str) != QDir::separator()) |
453 | #endif |
454 | ) |
455 | m.indices.append(x: i); |
456 | } |
457 | return m; |
458 | } |
459 | |
460 | // Returns a match hint from the cache by chopping the search string |
461 | bool QCompletionEngine::matchHint(const QString &part, const QModelIndex &parent, QMatchData *hint) const |
462 | { |
463 | if (part.isEmpty()) |
464 | return false; // early out to avoid cache[parent] lookup costs |
465 | |
466 | const auto cit = cache.find(key: parent); |
467 | if (cit == cache.end()) |
468 | return false; |
469 | |
470 | const CacheItem& map = *cit; |
471 | const auto mapEnd = map.end(); |
472 | |
473 | QString key = c->cs == Qt::CaseInsensitive ? part.toLower() : part; |
474 | |
475 | while (!key.isEmpty()) { |
476 | key.chop(n: 1); |
477 | const auto it = map.find(key); |
478 | if (it != mapEnd) { |
479 | *hint = *it; |
480 | return true; |
481 | } |
482 | } |
483 | |
484 | return false; |
485 | } |
486 | |
487 | bool QCompletionEngine::lookupCache(const QString &part, const QModelIndex &parent, QMatchData *m) const |
488 | { |
489 | if (part.isEmpty()) |
490 | return false; // early out to avoid cache[parent] lookup costs |
491 | |
492 | const auto cit = cache.find(key: parent); |
493 | if (cit == cache.end()) |
494 | return false; |
495 | |
496 | const CacheItem& map = *cit; |
497 | |
498 | const QString key = c->cs == Qt::CaseInsensitive ? part.toLower() : part; |
499 | |
500 | const auto it = map.find(key); |
501 | if (it == map.end()) |
502 | return false; |
503 | |
504 | *m = it.value(); |
505 | return true; |
506 | } |
507 | |
508 | // When the cache size exceeds 1MB, it clears out about 1/2 of the cache. |
509 | void QCompletionEngine::saveInCache(QString part, const QModelIndex& parent, const QMatchData& m) |
510 | { |
511 | if (c->filterMode == Qt::MatchEndsWith) |
512 | return; |
513 | QMatchData old = cache[parent].take(key: part); |
514 | cost = cost + m.indices.cost() - old.indices.cost(); |
515 | if (cost * sizeof(int) > 1024 * 1024) { |
516 | QMap<QModelIndex, CacheItem>::iterator it1 = cache.begin(); |
517 | while (it1 != cache.end()) { |
518 | CacheItem& ci = it1.value(); |
519 | int sz = ci.size()/2; |
520 | QMap<QString, QMatchData>::iterator it2 = ci.begin(); |
521 | int i = 0; |
522 | while (it2 != ci.end() && i < sz) { |
523 | cost -= it2.value().indices.cost(); |
524 | it2 = ci.erase(it: it2); |
525 | i++; |
526 | } |
527 | if (ci.size() == 0) { |
528 | it1 = cache.erase(it: it1); |
529 | } else { |
530 | ++it1; |
531 | } |
532 | } |
533 | } |
534 | |
535 | if (c->cs == Qt::CaseInsensitive) |
536 | part = std::move(part).toLower(); |
537 | cache[parent][part] = m; |
538 | } |
539 | |
540 | /////////////////////////////////////////////////////////////////////////////////// |
541 | QIndexMapper QSortedModelEngine::indexHint(QString part, const QModelIndex& parent, Qt::SortOrder order) |
542 | { |
543 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
544 | |
545 | if (c->cs == Qt::CaseInsensitive) |
546 | part = std::move(part).toLower(); |
547 | |
548 | const CacheItem& map = cache[parent]; |
549 | |
550 | // Try to find a lower and upper bound for the search from previous results |
551 | int to = model->rowCount(parent) - 1; |
552 | int from = 0; |
553 | const CacheItem::const_iterator it = map.lowerBound(key: part); |
554 | |
555 | // look backward for first valid hint |
556 | for (CacheItem::const_iterator it1 = it; it1 != map.constBegin();) { |
557 | --it1; |
558 | const QMatchData& value = it1.value(); |
559 | if (value.isValid()) { |
560 | if (order == Qt::AscendingOrder) { |
561 | from = value.indices.last() + 1; |
562 | } else { |
563 | to = value.indices.first() - 1; |
564 | } |
565 | break; |
566 | } |
567 | } |
568 | |
569 | // look forward for first valid hint |
570 | for(CacheItem::const_iterator it2 = it; it2 != map.constEnd(); ++it2) { |
571 | const QMatchData& value = it2.value(); |
572 | if (value.isValid() && !it2.key().startsWith(s: part)) { |
573 | if (order == Qt::AscendingOrder) { |
574 | to = value.indices.first() - 1; |
575 | } else { |
576 | from = value.indices.first() + 1; |
577 | } |
578 | break; |
579 | } |
580 | } |
581 | |
582 | return QIndexMapper(from, to); |
583 | } |
584 | |
585 | Qt::SortOrder QSortedModelEngine::sortOrder(const QModelIndex &parent) const |
586 | { |
587 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
588 | |
589 | int rowCount = model->rowCount(parent); |
590 | if (rowCount < 2) |
591 | return Qt::AscendingOrder; |
592 | QString first = model->data(index: model->index(row: 0, column: c->column, parent), role: c->role).toString(); |
593 | QString last = model->data(index: model->index(row: rowCount - 1, column: c->column, parent), role: c->role).toString(); |
594 | return QString::compare(s1: first, s2: last, cs: c->cs) <= 0 ? Qt::AscendingOrder : Qt::DescendingOrder; |
595 | } |
596 | |
597 | QMatchData QSortedModelEngine::filter(const QString& part, const QModelIndex& parent, int) |
598 | { |
599 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
600 | |
601 | QMatchData hint; |
602 | if (lookupCache(part, parent, m: &hint)) |
603 | return hint; |
604 | |
605 | QIndexMapper indices; |
606 | Qt::SortOrder order = sortOrder(parent); |
607 | |
608 | if (matchHint(part, parent, hint: &hint)) { |
609 | if (!hint.isValid()) |
610 | return QMatchData(); |
611 | indices = hint.indices; |
612 | } else { |
613 | indices = indexHint(part, parent, order); |
614 | } |
615 | |
616 | // binary search the model within 'indices' for 'part' under 'parent' |
617 | int high = indices.to() + 1; |
618 | int low = indices.from() - 1; |
619 | int probe; |
620 | QModelIndex probeIndex; |
621 | QString probeData; |
622 | |
623 | while (high - low > 1) |
624 | { |
625 | probe = (high + low) / 2; |
626 | probeIndex = model->index(row: probe, column: c->column, parent); |
627 | probeData = model->data(index: probeIndex, role: c->role).toString(); |
628 | const int cmp = QString::compare(s1: probeData, s2: part, cs: c->cs); |
629 | if ((order == Qt::AscendingOrder && cmp >= 0) |
630 | || (order == Qt::DescendingOrder && cmp < 0)) { |
631 | high = probe; |
632 | } else { |
633 | low = probe; |
634 | } |
635 | } |
636 | |
637 | if ((order == Qt::AscendingOrder && low == indices.to()) |
638 | || (order == Qt::DescendingOrder && high == indices.from())) { // not found |
639 | saveInCache(part, parent, m: QMatchData()); |
640 | return QMatchData(); |
641 | } |
642 | |
643 | probeIndex = model->index(row: order == Qt::AscendingOrder ? low+1 : high-1, column: c->column, parent); |
644 | probeData = model->data(index: probeIndex, role: c->role).toString(); |
645 | if (!probeData.startsWith(s: part, cs: c->cs)) { |
646 | saveInCache(part, parent, m: QMatchData()); |
647 | return QMatchData(); |
648 | } |
649 | |
650 | const bool exactMatch = QString::compare(s1: probeData, s2: part, cs: c->cs) == 0; |
651 | int emi = exactMatch ? (order == Qt::AscendingOrder ? low+1 : high-1) : -1; |
652 | |
653 | int from = 0; |
654 | int to = 0; |
655 | if (order == Qt::AscendingOrder) { |
656 | from = low + 1; |
657 | high = indices.to() + 1; |
658 | low = from; |
659 | } else { |
660 | to = high - 1; |
661 | low = indices.from() - 1; |
662 | high = to; |
663 | } |
664 | |
665 | while (high - low > 1) |
666 | { |
667 | probe = (high + low) / 2; |
668 | probeIndex = model->index(row: probe, column: c->column, parent); |
669 | probeData = model->data(index: probeIndex, role: c->role).toString(); |
670 | const bool startsWith = probeData.startsWith(s: part, cs: c->cs); |
671 | if ((order == Qt::AscendingOrder && startsWith) |
672 | || (order == Qt::DescendingOrder && !startsWith)) { |
673 | low = probe; |
674 | } else { |
675 | high = probe; |
676 | } |
677 | } |
678 | |
679 | QMatchData m(order == Qt::AscendingOrder ? QIndexMapper(from, high - 1) : QIndexMapper(low+1, to), emi, false); |
680 | saveInCache(part, parent, m); |
681 | return m; |
682 | } |
683 | |
684 | //////////////////////////////////////////////////////////////////////////////////////// |
685 | int QUnsortedModelEngine::buildIndices(const QString& str, const QModelIndex& parent, int n, |
686 | const QIndexMapper& indices, QMatchData* m) |
687 | { |
688 | Q_ASSERT(m->partial); |
689 | Q_ASSERT(n != -1 || m->exactMatchIndex == -1); |
690 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
691 | int i, count = 0; |
692 | |
693 | for (i = 0; i < indices.count() && count != n; ++i) { |
694 | QModelIndex idx = model->index(row: indices[i], column: c->column, parent); |
695 | |
696 | if (!(model->flags(index: idx) & Qt::ItemIsSelectable)) |
697 | continue; |
698 | |
699 | QString data = model->data(index: idx, role: c->role).toString(); |
700 | |
701 | switch (c->filterMode) { |
702 | case Qt::MatchStartsWith: |
703 | if (!data.startsWith(s: str, cs: c->cs)) |
704 | continue; |
705 | break; |
706 | case Qt::MatchContains: |
707 | if (!data.contains(s: str, cs: c->cs)) |
708 | continue; |
709 | break; |
710 | case Qt::MatchEndsWith: |
711 | if (!data.endsWith(s: str, cs: c->cs)) |
712 | continue; |
713 | break; |
714 | case Qt::MatchExactly: |
715 | case Qt::MatchFixedString: |
716 | case Qt::MatchCaseSensitive: |
717 | case Qt::MatchRegularExpression: |
718 | case Qt::MatchWildcard: |
719 | case Qt::MatchWrap: |
720 | case Qt::MatchRecursive: |
721 | Q_UNREACHABLE(); |
722 | break; |
723 | } |
724 | m->indices.append(x: indices[i]); |
725 | ++count; |
726 | if (m->exactMatchIndex == -1 && QString::compare(s1: data, s2: str, cs: c->cs) == 0) { |
727 | m->exactMatchIndex = indices[i]; |
728 | if (n == -1) |
729 | return indices[i]; |
730 | } |
731 | } |
732 | return indices[i-1]; |
733 | } |
734 | |
735 | void QUnsortedModelEngine::filterOnDemand(int n) |
736 | { |
737 | Q_ASSERT(matchCount()); |
738 | if (!curMatch.partial) |
739 | return; |
740 | Q_ASSERT(n >= -1); |
741 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
742 | int lastRow = model->rowCount(parent: curParent) - 1; |
743 | QIndexMapper im(curMatch.indices.last() + 1, lastRow); |
744 | int lastIndex = buildIndices(str: curParts.constLast(), parent: curParent, n, indices: im, m: &curMatch); |
745 | curMatch.partial = (lastRow != lastIndex); |
746 | saveInCache(part: curParts.constLast(), parent: curParent, m: curMatch); |
747 | } |
748 | |
749 | QMatchData QUnsortedModelEngine::filter(const QString& part, const QModelIndex& parent, int n) |
750 | { |
751 | QMatchData hint; |
752 | |
753 | QList<int> v; |
754 | QIndexMapper im(v); |
755 | QMatchData m(im, -1, true); |
756 | |
757 | const QAbstractItemModel *model = c->proxy->sourceModel(); |
758 | bool foundInCache = lookupCache(part, parent, m: &m); |
759 | |
760 | if (!foundInCache) { |
761 | if (matchHint(part, parent, hint: &hint) && !hint.isValid()) |
762 | return QMatchData(); |
763 | } |
764 | |
765 | if (!foundInCache && !hint.isValid()) { |
766 | const int lastRow = model->rowCount(parent) - 1; |
767 | QIndexMapper all(0, lastRow); |
768 | int lastIndex = buildIndices(str: part, parent, n, indices: all, m: &m); |
769 | m.partial = (lastIndex != lastRow); |
770 | } else { |
771 | if (!foundInCache) { // build from hint as much as we can |
772 | buildIndices(str: part, parent, INT_MAX, indices: hint.indices, m: &m); |
773 | m.partial = hint.partial; |
774 | } |
775 | if (m.partial && ((n == -1 && m.exactMatchIndex == -1) || (m.indices.count() < n))) { |
776 | // need more and have more |
777 | const int lastRow = model->rowCount(parent) - 1; |
778 | QIndexMapper rest(hint.indices.last() + 1, lastRow); |
779 | int want = n == -1 ? -1 : n - m.indices.count(); |
780 | int lastIndex = buildIndices(str: part, parent, n: want, indices: rest, m: &m); |
781 | m.partial = (lastRow != lastIndex); |
782 | } |
783 | } |
784 | |
785 | saveInCache(part, parent, m); |
786 | return m; |
787 | } |
788 | |
789 | /////////////////////////////////////////////////////////////////////////////// |
790 | QCompleterPrivate::QCompleterPrivate() |
791 | : widget(nullptr), |
792 | proxy(nullptr), |
793 | popup(nullptr), |
794 | filterMode(Qt::MatchStartsWith), |
795 | cs(Qt::CaseSensitive), |
796 | role(Qt::EditRole), |
797 | column(0), |
798 | maxVisibleItems(7), |
799 | sorting(QCompleter::UnsortedModel), |
800 | wrap(true), |
801 | eatFocusOut(true), |
802 | hiddenBecauseNoMatch(false) |
803 | { |
804 | } |
805 | |
806 | void QCompleterPrivate::init(QAbstractItemModel *m) |
807 | { |
808 | Q_Q(QCompleter); |
809 | proxy = new QCompletionModel(this, q); |
810 | QObject::connect(sender: proxy, SIGNAL(rowsAdded()), receiver: q, SLOT(_q_autoResizePopup())); |
811 | q->setModel(m); |
812 | #if !QT_CONFIG(listview) |
813 | q->setCompletionMode(QCompleter::InlineCompletion); |
814 | #else |
815 | q->setCompletionMode(QCompleter::PopupCompletion); |
816 | #endif // QT_CONFIG(listview) |
817 | } |
818 | |
819 | void QCompleterPrivate::setCurrentIndex(QModelIndex index, bool select) |
820 | { |
821 | Q_Q(QCompleter); |
822 | if (!q->popup()) |
823 | return; |
824 | if (!select) { |
825 | popup->selectionModel()->setCurrentIndex(index, command: QItemSelectionModel::NoUpdate); |
826 | } else { |
827 | if (!index.isValid()) |
828 | popup->selectionModel()->clear(); |
829 | else |
830 | popup->selectionModel()->setCurrentIndex(index, command: QItemSelectionModel::Select |
831 | | QItemSelectionModel::Rows); |
832 | } |
833 | index = popup->selectionModel()->currentIndex(); |
834 | if (!index.isValid()) |
835 | popup->scrollToTop(); |
836 | else |
837 | popup->scrollTo(index, hint: QAbstractItemView::PositionAtTop); |
838 | } |
839 | |
840 | void QCompleterPrivate::_q_completionSelected(const QItemSelection& selection) |
841 | { |
842 | QModelIndex index; |
843 | if (!selection.indexes().isEmpty()) |
844 | index = selection.indexes().first(); |
845 | |
846 | _q_complete(index, true); |
847 | } |
848 | |
849 | void QCompleterPrivate::_q_complete(QModelIndex index, bool highlighted) |
850 | { |
851 | Q_Q(QCompleter); |
852 | QString completion; |
853 | |
854 | if (!index.isValid() || (!proxy->showAll && (index.row() >= proxy->engine->matchCount()))) { |
855 | completion = prefix; |
856 | index = QModelIndex(); |
857 | } else { |
858 | if (!(index.flags() & Qt::ItemIsEnabled)) |
859 | return; |
860 | QModelIndex si = proxy->mapToSource(index); |
861 | si = si.sibling(arow: si.row(), acolumn: column); // for clicked() |
862 | completion = q->pathFromIndex(index: si); |
863 | #if QT_CONFIG(filesystemmodel) |
864 | // add a trailing separator in inline |
865 | if (mode == QCompleter::InlineCompletion) { |
866 | if (qobject_cast<QFileSystemModel *>(object: proxy->sourceModel()) && QFileInfo(completion).isDir()) |
867 | completion += QDir::separator(); |
868 | } |
869 | #endif |
870 | } |
871 | |
872 | if (highlighted) { |
873 | emit q->highlighted(index); |
874 | emit q->highlighted(text: completion); |
875 | } else { |
876 | emit q->activated(index); |
877 | emit q->activated(text: completion); |
878 | } |
879 | } |
880 | |
881 | void QCompleterPrivate::() |
882 | { |
883 | if (!popup || !popup->isVisible()) |
884 | return; |
885 | showPopup(popupRect); |
886 | } |
887 | |
888 | void QCompleterPrivate::(const QRect& rect) |
889 | { |
890 | const QRect screen = widget->screen()->availableGeometry(); |
891 | Qt::LayoutDirection dir = widget->layoutDirection(); |
892 | QPoint pos; |
893 | int rh, w; |
894 | int h = (popup->sizeHintForRow(row: 0) * qMin(a: maxVisibleItems, b: popup->model()->rowCount()) + 3) + 3; |
895 | QScrollBar *hsb = popup->horizontalScrollBar(); |
896 | if (hsb && hsb->isVisible()) |
897 | h += popup->horizontalScrollBar()->sizeHint().height(); |
898 | |
899 | if (rect.isValid()) { |
900 | rh = rect.height(); |
901 | w = rect.width(); |
902 | pos = widget->mapToGlobal(dir == Qt::RightToLeft ? rect.bottomRight() : rect.bottomLeft()); |
903 | } else { |
904 | rh = widget->height(); |
905 | pos = widget->mapToGlobal(QPoint(0, widget->height() - 2)); |
906 | w = widget->width(); |
907 | } |
908 | |
909 | if (w > screen.width()) |
910 | w = screen.width(); |
911 | if ((pos.x() + w) > (screen.x() + screen.width())) |
912 | pos.setX(screen.x() + screen.width() - w); |
913 | if (pos.x() < screen.x()) |
914 | pos.setX(screen.x()); |
915 | |
916 | int top = pos.y() - rh - screen.top() + 2; |
917 | int bottom = screen.bottom() - pos.y(); |
918 | h = qMax(a: h, b: popup->minimumHeight()); |
919 | if (h > bottom) { |
920 | h = qMin(a: qMax(a: top, b: bottom), b: h); |
921 | |
922 | if (top > bottom) |
923 | pos.setY(pos.y() - h - rh + 2); |
924 | } |
925 | |
926 | popup->setGeometry(ax: pos.x(), ay: pos.y(), aw: w, ah: h); |
927 | |
928 | if (!popup->isVisible()) |
929 | popup->show(); |
930 | } |
931 | |
932 | #if QT_CONFIG(filesystemmodel) |
933 | static bool isRoot(const QFileSystemModel *model, const QString &path) |
934 | { |
935 | const auto index = model->index(path); |
936 | return index.isValid() && model->fileInfo(index).isRoot(); |
937 | } |
938 | |
939 | static bool completeOnLoaded(const QFileSystemModel *model, |
940 | const QString &nativePrefix, |
941 | const QString &path, |
942 | Qt::CaseSensitivity caseSensitivity) |
943 | { |
944 | const auto pathSize = path.size(); |
945 | const auto prefixSize = nativePrefix.size(); |
946 | if (prefixSize < pathSize) |
947 | return false; |
948 | const QString prefix = QDir::fromNativeSeparators(pathName: nativePrefix); |
949 | if (prefixSize == pathSize) |
950 | return path.compare(s: prefix, cs: caseSensitivity) == 0 && isRoot(model, path); |
951 | // The user is typing something within that directory and is not in a subdirectory yet. |
952 | const auto separator = u'/'; |
953 | return prefix.startsWith(s: path, cs: caseSensitivity) && prefix.at(i: pathSize) == separator |
954 | && !QStringView{prefix}.right(n: prefixSize - pathSize - 1).contains(c: separator); |
955 | } |
956 | |
957 | void QCompleterPrivate::_q_fileSystemModelDirectoryLoaded(const QString &path) |
958 | { |
959 | Q_Q(QCompleter); |
960 | // Slot called when QFileSystemModel has finished loading. |
961 | // If we hide the popup because there was no match because the model was not loaded yet, |
962 | // we re-start the completion when we get the results (unless triggered by |
963 | // something else, see QTBUG-14292). |
964 | if (hiddenBecauseNoMatch && widget) { |
965 | if (auto model = qobject_cast<const QFileSystemModel *>(object: proxy->sourceModel())) { |
966 | if (completeOnLoaded(model, nativePrefix: prefix, path, caseSensitivity: cs)) |
967 | q->complete(); |
968 | } |
969 | } |
970 | } |
971 | #else // QT_CONFIG(filesystemmodel) |
972 | void QCompleterPrivate::_q_fileSystemModelDirectoryLoaded(const QString &) {} |
973 | #endif |
974 | |
975 | /*! |
976 | Constructs a completer object with the given \a parent. |
977 | */ |
978 | QCompleter::QCompleter(QObject *parent) |
979 | : QObject(*new QCompleterPrivate(), parent) |
980 | { |
981 | Q_D(QCompleter); |
982 | d->init(); |
983 | } |
984 | |
985 | /*! |
986 | Constructs a completer object with the given \a parent that provides completions |
987 | from the specified \a model. |
988 | */ |
989 | QCompleter::QCompleter(QAbstractItemModel *model, QObject *parent) |
990 | : QObject(*new QCompleterPrivate(), parent) |
991 | { |
992 | Q_D(QCompleter); |
993 | d->init(m: model); |
994 | } |
995 | |
996 | #if QT_CONFIG(stringlistmodel) |
997 | /*! |
998 | Constructs a QCompleter object with the given \a parent that uses the specified |
999 | \a list as a source of possible completions. |
1000 | */ |
1001 | QCompleter::QCompleter(const QStringList& list, QObject *parent) |
1002 | : QObject(*new QCompleterPrivate(), parent) |
1003 | { |
1004 | Q_D(QCompleter); |
1005 | d->init(m: new QStringListModel(list, this)); |
1006 | } |
1007 | #endif // QT_CONFIG(stringlistmodel) |
1008 | |
1009 | /*! |
1010 | Destroys the completer object. |
1011 | */ |
1012 | QCompleter::~QCompleter() |
1013 | { |
1014 | } |
1015 | |
1016 | /*! |
1017 | Sets the widget for which completion are provided for to \a widget. This |
1018 | function is automatically called when a QCompleter is set on a QLineEdit |
1019 | using QLineEdit::setCompleter() or on a QComboBox using |
1020 | QComboBox::setCompleter(). The widget needs to be set explicitly when |
1021 | providing completions for custom widgets. |
1022 | |
1023 | \sa widget(), setModel(), setPopup() |
1024 | */ |
1025 | void QCompleter::setWidget(QWidget *widget) |
1026 | { |
1027 | Q_D(QCompleter); |
1028 | if (widget == d->widget) |
1029 | return; |
1030 | |
1031 | if (d->widget) |
1032 | d->widget->removeEventFilter(obj: this); |
1033 | d->widget = widget; |
1034 | if (d->widget) |
1035 | d->widget->installEventFilter(filterObj: this); |
1036 | |
1037 | if (d->popup) { |
1038 | d->popup->hide(); |
1039 | d->popup->setFocusProxy(d->widget); |
1040 | } |
1041 | } |
1042 | |
1043 | /*! |
1044 | Returns the widget for which the completer object is providing completions. |
1045 | |
1046 | \sa setWidget() |
1047 | */ |
1048 | QWidget *QCompleter::widget() const |
1049 | { |
1050 | Q_D(const QCompleter); |
1051 | return d->widget; |
1052 | } |
1053 | |
1054 | /*! |
1055 | Sets the model which provides completions to \a model. The \a model can |
1056 | be list model or a tree model. If a model has been already previously set |
1057 | and it has the QCompleter as its parent, it is deleted. |
1058 | |
1059 | For convenience, if \a model is a QFileSystemModel, QCompleter switches its |
1060 | caseSensitivity to Qt::CaseInsensitive on Windows and Qt::CaseSensitive |
1061 | on other platforms. |
1062 | |
1063 | \sa completionModel(), modelSorting, {Handling Tree Models} |
1064 | */ |
1065 | void QCompleter::setModel(QAbstractItemModel *model) |
1066 | { |
1067 | Q_D(QCompleter); |
1068 | QAbstractItemModel *oldModel = d->proxy->sourceModel(); |
1069 | if (oldModel == model) |
1070 | return; |
1071 | #if QT_CONFIG(filesystemmodel) |
1072 | if (qobject_cast<const QFileSystemModel *>(object: oldModel)) |
1073 | setCompletionRole(Qt::EditRole); // QTBUG-54642, clear FileNameRole set by QFileSystemModel |
1074 | #endif |
1075 | d->proxy->setSourceModel(model); |
1076 | if (d->popup) |
1077 | setPopup(d->popup); // set the model and make new connections |
1078 | if (oldModel && oldModel->QObject::parent() == this) |
1079 | delete oldModel; |
1080 | #if QT_CONFIG(filesystemmodel) |
1081 | QFileSystemModel *fsModel = qobject_cast<QFileSystemModel *>(object: model); |
1082 | if (fsModel) { |
1083 | #if defined(Q_OS_WIN) |
1084 | setCaseSensitivity(Qt::CaseInsensitive); |
1085 | #else |
1086 | setCaseSensitivity(Qt::CaseSensitive); |
1087 | #endif |
1088 | setCompletionRole(QFileSystemModel::FileNameRole); |
1089 | connect(sender: fsModel, SIGNAL(directoryLoaded(QString)), receiver: this, SLOT(_q_fileSystemModelDirectoryLoaded(QString))); |
1090 | } |
1091 | #endif // QT_CONFIG(filesystemmodel) |
1092 | } |
1093 | |
1094 | /*! |
1095 | Returns the model that provides completion strings. |
1096 | |
1097 | \sa completionModel() |
1098 | */ |
1099 | QAbstractItemModel *QCompleter::model() const |
1100 | { |
1101 | Q_D(const QCompleter); |
1102 | return d->proxy->sourceModel(); |
1103 | } |
1104 | |
1105 | /*! |
1106 | \enum QCompleter::CompletionMode |
1107 | |
1108 | This enum specifies how completions are provided to the user. |
1109 | |
1110 | \value PopupCompletion Current completions are displayed in a popup window. |
1111 | \value InlineCompletion Completions appear inline (as selected text). |
1112 | \value UnfilteredPopupCompletion All possible completions are displayed in a popup window with the most likely suggestion indicated as current. |
1113 | |
1114 | \sa setCompletionMode() |
1115 | */ |
1116 | |
1117 | /*! |
1118 | \property QCompleter::completionMode |
1119 | \brief how the completions are provided to the user |
1120 | |
1121 | The default value is QCompleter::PopupCompletion. |
1122 | */ |
1123 | void QCompleter::setCompletionMode(QCompleter::CompletionMode mode) |
1124 | { |
1125 | Q_D(QCompleter); |
1126 | d->mode = mode; |
1127 | d->proxy->setFiltered(mode != QCompleter::UnfilteredPopupCompletion); |
1128 | |
1129 | if (mode == QCompleter::InlineCompletion) { |
1130 | if (d->widget) |
1131 | d->widget->removeEventFilter(obj: this); |
1132 | if (d->popup) { |
1133 | d->popup->deleteLater(); |
1134 | d->popup = nullptr; |
1135 | } |
1136 | } else { |
1137 | if (d->widget) |
1138 | d->widget->installEventFilter(filterObj: this); |
1139 | } |
1140 | } |
1141 | |
1142 | QCompleter::CompletionMode QCompleter::completionMode() const |
1143 | { |
1144 | Q_D(const QCompleter); |
1145 | return d->mode; |
1146 | } |
1147 | |
1148 | /*! |
1149 | \property QCompleter::filterMode |
1150 | \brief This property controls how filtering is performed. |
1151 | \since 5.2 |
1152 | |
1153 | If filterMode is set to Qt::MatchStartsWith, only those entries that start |
1154 | with the typed characters will be displayed. Qt::MatchContains will display |
1155 | the entries that contain the typed characters, and Qt::MatchEndsWith the |
1156 | ones that end with the typed characters. |
1157 | |
1158 | Setting filterMode to any other Qt::MatchFlag will issue a warning, and no |
1159 | action will be performed. Because of this, the \c Qt::MatchCaseSensitive |
1160 | flag has no effect. Use the \l caseSensitivity property to control case |
1161 | sensitivity. |
1162 | |
1163 | The default mode is Qt::MatchStartsWith. |
1164 | |
1165 | \sa caseSensitivity |
1166 | */ |
1167 | |
1168 | void QCompleter::setFilterMode(Qt::MatchFlags filterMode) |
1169 | { |
1170 | Q_D(QCompleter); |
1171 | |
1172 | if (d->filterMode == filterMode) |
1173 | return; |
1174 | |
1175 | if (Q_UNLIKELY(filterMode != Qt::MatchStartsWith && |
1176 | filterMode != Qt::MatchContains && |
1177 | filterMode != Qt::MatchEndsWith)) { |
1178 | qWarning(msg: "Unhandled QCompleter::filterMode flag is used." ); |
1179 | return; |
1180 | } |
1181 | |
1182 | d->filterMode = filterMode; |
1183 | d->proxy->createEngine(); |
1184 | d->proxy->invalidate(); |
1185 | } |
1186 | |
1187 | Qt::MatchFlags QCompleter::filterMode() const |
1188 | { |
1189 | Q_D(const QCompleter); |
1190 | return d->filterMode; |
1191 | } |
1192 | |
1193 | /*! |
1194 | Sets the popup used to display completions to \a popup. QCompleter takes |
1195 | ownership of the view. |
1196 | |
1197 | A QListView is automatically created when the completionMode() is set to |
1198 | QCompleter::PopupCompletion or QCompleter::UnfilteredPopupCompletion. The |
1199 | default popup displays the completionColumn(). |
1200 | |
1201 | Ensure that this function is called before the view settings are modified. |
1202 | This is required since view's properties may require that a model has been |
1203 | set on the view (for example, hiding columns in the view requires a model |
1204 | to be set on the view). |
1205 | |
1206 | \sa popup() |
1207 | */ |
1208 | void QCompleter::(QAbstractItemView *) |
1209 | { |
1210 | Q_ASSERT(popup); |
1211 | Q_D(QCompleter); |
1212 | if (popup == d->popup) |
1213 | return; |
1214 | |
1215 | // Remember existing widget's focus policy, default to NoFocus |
1216 | const Qt::FocusPolicy origPolicy = d->widget ? d->widget->focusPolicy() |
1217 | : Qt::NoFocus; |
1218 | |
1219 | // If popup existed already, disconnect signals and delete object |
1220 | if (d->popup) { |
1221 | QObject::disconnect(sender: d->popup->selectionModel(), signal: nullptr, receiver: this, member: nullptr); |
1222 | QObject::disconnect(sender: d->popup, signal: nullptr, receiver: this, member: nullptr); |
1223 | delete d->popup; |
1224 | } |
1225 | |
1226 | // Assign new object, set model and hide |
1227 | d->popup = popup; |
1228 | if (d->popup->model() != d->proxy) |
1229 | d->popup->setModel(d->proxy); |
1230 | d->popup->hide(); |
1231 | |
1232 | // Mark the widget window as a popup, so that if the last non-popup window is closed by the |
1233 | // user, the application should not be prevented from exiting. It needs to be set explicitly via |
1234 | // setWindowFlag(), because passing the flag via setParent(parent, windowFlags) does not call |
1235 | // QWidgetPrivate::adjustQuitOnCloseAttribute(), and causes an application not to exit if the |
1236 | // popup ends up being the last window. |
1237 | d->popup->setParent(nullptr); |
1238 | d->popup->setWindowFlag(Qt::Popup); |
1239 | d->popup->setFocusPolicy(Qt::NoFocus); |
1240 | if (d->widget) |
1241 | d->widget->setFocusPolicy(origPolicy); |
1242 | |
1243 | d->popup->setFocusProxy(d->widget); |
1244 | d->popup->installEventFilter(filterObj: this); |
1245 | d->popup->setItemDelegate(new QCompleterItemDelegate(d->popup)); |
1246 | #if QT_CONFIG(listview) |
1247 | if (QListView *listView = qobject_cast<QListView *>(object: d->popup)) { |
1248 | listView->setModelColumn(d->column); |
1249 | } |
1250 | #endif |
1251 | |
1252 | QObject::connect(sender: d->popup, SIGNAL(clicked(QModelIndex)), |
1253 | receiver: this, SLOT(_q_complete(QModelIndex))); |
1254 | QObject::connect(sender: this, SIGNAL(activated(QModelIndex)), |
1255 | receiver: d->popup, SLOT(hide())); |
1256 | |
1257 | QObject::connect(sender: d->popup->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), |
1258 | receiver: this, SLOT(_q_completionSelected(QItemSelection))); |
1259 | } |
1260 | |
1261 | /*! |
1262 | Returns the popup used to display completions. |
1263 | |
1264 | \sa setPopup() |
1265 | */ |
1266 | QAbstractItemView *QCompleter::() const |
1267 | { |
1268 | Q_D(const QCompleter); |
1269 | #if QT_CONFIG(listview) |
1270 | if (!d->popup && completionMode() != QCompleter::InlineCompletion) { |
1271 | QListView *listView = new QListView; |
1272 | listView->setEditTriggers(QAbstractItemView::NoEditTriggers); |
1273 | listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
1274 | listView->setSelectionBehavior(QAbstractItemView::SelectRows); |
1275 | listView->setSelectionMode(QAbstractItemView::SingleSelection); |
1276 | listView->setModelColumn(d->column); |
1277 | QCompleter *that = const_cast<QCompleter*>(this); |
1278 | that->setPopup(listView); |
1279 | } |
1280 | #endif // QT_CONFIG(listview) |
1281 | return d->popup; |
1282 | } |
1283 | |
1284 | /*! |
1285 | \reimp |
1286 | */ |
1287 | bool QCompleter::event(QEvent *ev) |
1288 | { |
1289 | return QObject::event(event: ev); |
1290 | } |
1291 | |
1292 | /*! |
1293 | \reimp |
1294 | */ |
1295 | bool QCompleter::eventFilter(QObject *o, QEvent *e) |
1296 | { |
1297 | Q_D(QCompleter); |
1298 | |
1299 | if (d->eatFocusOut && o == d->widget && e->type() == QEvent::FocusOut) { |
1300 | d->hiddenBecauseNoMatch = false; |
1301 | if (d->popup && d->popup->isVisible()) |
1302 | return true; |
1303 | } |
1304 | |
1305 | if (o != d->popup) |
1306 | return QObject::eventFilter(watched: o, event: e); |
1307 | |
1308 | Q_ASSERT(d->popup); |
1309 | switch (e->type()) { |
1310 | case QEvent::KeyPress: { |
1311 | QKeyEvent *ke = static_cast<QKeyEvent *>(e); |
1312 | |
1313 | QModelIndex curIndex = d->popup->currentIndex(); |
1314 | QModelIndexList selList = d->popup->selectionModel()->selectedIndexes(); |
1315 | |
1316 | const int key = ke->key(); |
1317 | // In UnFilteredPopup mode, select the current item |
1318 | if ((key == Qt::Key_Up || key == Qt::Key_Down) && selList.isEmpty() && curIndex.isValid() |
1319 | && d->mode == QCompleter::UnfilteredPopupCompletion) { |
1320 | d->setCurrentIndex(index: curIndex); |
1321 | return true; |
1322 | } |
1323 | |
1324 | // Handle popup navigation keys. These are hardcoded because up/down might make the |
1325 | // widget do something else (lineedit cursor moves to home/end on mac, for instance) |
1326 | switch (key) { |
1327 | case Qt::Key_End: |
1328 | case Qt::Key_Home: |
1329 | if (ke->modifiers() & Qt::ControlModifier) |
1330 | return false; |
1331 | break; |
1332 | |
1333 | case Qt::Key_Up: |
1334 | if (!curIndex.isValid()) { |
1335 | int rowCount = d->proxy->rowCount(); |
1336 | QModelIndex lastIndex = d->proxy->index(row: rowCount - 1, column: d->column); |
1337 | d->setCurrentIndex(index: lastIndex); |
1338 | return true; |
1339 | } else if (curIndex.row() == 0) { |
1340 | if (d->wrap) |
1341 | d->setCurrentIndex(index: QModelIndex()); |
1342 | return true; |
1343 | } |
1344 | return false; |
1345 | |
1346 | case Qt::Key_Down: |
1347 | if (!curIndex.isValid()) { |
1348 | QModelIndex firstIndex = d->proxy->index(row: 0, column: d->column); |
1349 | d->setCurrentIndex(index: firstIndex); |
1350 | return true; |
1351 | } else if (curIndex.row() == d->proxy->rowCount() - 1) { |
1352 | if (d->wrap) |
1353 | d->setCurrentIndex(index: QModelIndex()); |
1354 | return true; |
1355 | } |
1356 | return false; |
1357 | |
1358 | case Qt::Key_PageUp: |
1359 | case Qt::Key_PageDown: |
1360 | return false; |
1361 | } |
1362 | |
1363 | // Send the event to the widget. If the widget accepted the event, do nothing |
1364 | // If the widget did not accept the event, provide a default implementation |
1365 | d->eatFocusOut = false; |
1366 | (static_cast<QObject *>(d->widget))->event(event: ke); |
1367 | d->eatFocusOut = true; |
1368 | if (!d->widget || e->isAccepted() || !d->popup->isVisible()) { |
1369 | // widget lost focus, hide the popup |
1370 | if (d->widget && (!d->widget->hasFocus() |
1371 | #ifdef QT_KEYPAD_NAVIGATION |
1372 | || (QApplicationPrivate::keypadNavigationEnabled() && !d->widget->hasEditFocus()) |
1373 | #endif |
1374 | )) |
1375 | d->popup->hide(); |
1376 | if (e->isAccepted()) |
1377 | return true; |
1378 | } |
1379 | |
1380 | // default implementation for keys not handled by the widget when popup is open |
1381 | #if QT_CONFIG(shortcut) |
1382 | if (ke->matches(key: QKeySequence::Cancel)) { |
1383 | d->popup->hide(); |
1384 | return true; |
1385 | } |
1386 | #endif |
1387 | switch (key) { |
1388 | #ifdef QT_KEYPAD_NAVIGATION |
1389 | case Qt::Key_Select: |
1390 | if (!QApplicationPrivate::keypadNavigationEnabled()) |
1391 | break; |
1392 | #endif |
1393 | case Qt::Key_Return: |
1394 | case Qt::Key_Enter: |
1395 | case Qt::Key_Tab: |
1396 | d->popup->hide(); |
1397 | if (curIndex.isValid()) |
1398 | d->_q_complete(index: curIndex); |
1399 | break; |
1400 | |
1401 | case Qt::Key_F4: |
1402 | if (ke->modifiers() & Qt::AltModifier) |
1403 | d->popup->hide(); |
1404 | break; |
1405 | |
1406 | case Qt::Key_Backtab: |
1407 | d->popup->hide(); |
1408 | break; |
1409 | |
1410 | default: |
1411 | break; |
1412 | } |
1413 | |
1414 | return true; |
1415 | } |
1416 | |
1417 | #ifdef QT_KEYPAD_NAVIGATION |
1418 | case QEvent::KeyRelease: { |
1419 | QKeyEvent *ke = static_cast<QKeyEvent *>(e); |
1420 | if (QApplicationPrivate::keypadNavigationEnabled() && ke->key() == Qt::Key_Back) { |
1421 | // Send the event to the 'widget'. This is what we did for KeyPress, so we need |
1422 | // to do the same for KeyRelease, in case the widget's KeyPress event set |
1423 | // up something (such as a timer) that is relying on also receiving the |
1424 | // key release. I see this as a bug in Qt, and should really set it up for all |
1425 | // the affected keys. However, it is difficult to tell how this will affect |
1426 | // existing code, and I can't test for every combination! |
1427 | d->eatFocusOut = false; |
1428 | static_cast<QObject *>(d->widget)->event(ke); |
1429 | d->eatFocusOut = true; |
1430 | } |
1431 | break; |
1432 | } |
1433 | #endif |
1434 | |
1435 | case QEvent::MouseButtonPress: { |
1436 | #ifdef QT_KEYPAD_NAVIGATION |
1437 | if (QApplicationPrivate::keypadNavigationEnabled()) { |
1438 | // if we've clicked in the widget (or its descendant), let it handle the click |
1439 | QWidget *source = qobject_cast<QWidget *>(o); |
1440 | if (source) { |
1441 | QPoint pos = source->mapToGlobal((static_cast<QMouseEvent *>(e))->pos()); |
1442 | QWidget *target = QApplication::widgetAt(pos); |
1443 | if (target && (d->widget->isAncestorOf(target) || |
1444 | target == d->widget)) { |
1445 | d->eatFocusOut = false; |
1446 | static_cast<QObject *>(target)->event(e); |
1447 | d->eatFocusOut = true; |
1448 | return true; |
1449 | } |
1450 | } |
1451 | } |
1452 | #endif |
1453 | if (!d->popup->underMouse()) { |
1454 | d->popup->hide(); |
1455 | return true; |
1456 | } |
1457 | } |
1458 | return false; |
1459 | |
1460 | case QEvent::InputMethod: |
1461 | case QEvent::ShortcutOverride: |
1462 | QCoreApplication::sendEvent(receiver: d->widget, event: e); |
1463 | break; |
1464 | |
1465 | default: |
1466 | return false; |
1467 | } |
1468 | return false; |
1469 | } |
1470 | |
1471 | /*! |
1472 | For QCompleter::PopupCompletion and QCompletion::UnfilteredPopupCompletion |
1473 | modes, calling this function displays the popup displaying the current |
1474 | completions. By default, if \a rect is not specified, the popup is displayed |
1475 | on the bottom of the widget(). If \a rect is specified the popup is |
1476 | displayed on the left edge of the rectangle. |
1477 | |
1478 | For QCompleter::InlineCompletion mode, the highlighted() signal is fired |
1479 | with the current completion. |
1480 | */ |
1481 | void QCompleter::complete(const QRect& rect) |
1482 | { |
1483 | Q_D(QCompleter); |
1484 | QModelIndex idx = d->proxy->currentIndex(sourceIndex: false); |
1485 | d->hiddenBecauseNoMatch = false; |
1486 | if (d->mode == QCompleter::InlineCompletion) { |
1487 | if (idx.isValid()) |
1488 | d->_q_complete(index: idx, highlighted: true); |
1489 | return; |
1490 | } |
1491 | |
1492 | Q_ASSERT(d->widget); |
1493 | if ((d->mode == QCompleter::PopupCompletion && !idx.isValid()) |
1494 | || (d->mode == QCompleter::UnfilteredPopupCompletion && d->proxy->rowCount() == 0)) { |
1495 | if (d->popup) |
1496 | d->popup->hide(); // no suggestion, hide |
1497 | d->hiddenBecauseNoMatch = true; |
1498 | return; |
1499 | } |
1500 | |
1501 | popup(); |
1502 | if (d->mode == QCompleter::UnfilteredPopupCompletion) |
1503 | d->setCurrentIndex(index: idx, select: false); |
1504 | |
1505 | d->showPopup(rect); |
1506 | d->popupRect = rect; |
1507 | } |
1508 | |
1509 | /*! |
1510 | Sets the current row to the \a row specified. Returns \c true if successful; |
1511 | otherwise returns \c false. |
1512 | |
1513 | This function may be used along with currentCompletion() to iterate |
1514 | through all the possible completions. |
1515 | |
1516 | \sa currentCompletion(), completionCount() |
1517 | */ |
1518 | bool QCompleter::setCurrentRow(int row) |
1519 | { |
1520 | Q_D(QCompleter); |
1521 | return d->proxy->setCurrentRow(row); |
1522 | } |
1523 | |
1524 | /*! |
1525 | Returns the current row. |
1526 | |
1527 | \sa setCurrentRow() |
1528 | */ |
1529 | int QCompleter::currentRow() const |
1530 | { |
1531 | Q_D(const QCompleter); |
1532 | return d->proxy->currentRow(); |
1533 | } |
1534 | |
1535 | /*! |
1536 | Returns the number of completions for the current prefix. For an unsorted |
1537 | model with a large number of items this can be expensive. Use setCurrentRow() |
1538 | and currentCompletion() to iterate through all the completions. |
1539 | */ |
1540 | int QCompleter::completionCount() const |
1541 | { |
1542 | Q_D(const QCompleter); |
1543 | return d->proxy->completionCount(); |
1544 | } |
1545 | |
1546 | /*! |
1547 | \enum QCompleter::ModelSorting |
1548 | |
1549 | This enum specifies how the items in the model are sorted. |
1550 | |
1551 | \value UnsortedModel The model is unsorted. |
1552 | \value CaseSensitivelySortedModel The model is sorted case sensitively. |
1553 | \value CaseInsensitivelySortedModel The model is sorted case insensitively. |
1554 | |
1555 | \sa setModelSorting() |
1556 | */ |
1557 | |
1558 | /*! |
1559 | \property QCompleter::modelSorting |
1560 | \brief the way the model is sorted |
1561 | |
1562 | By default, no assumptions are made about the order of the items |
1563 | in the model that provides the completions. |
1564 | |
1565 | If the model's data for the completionColumn() and completionRole() is sorted in |
1566 | ascending order, you can set this property to \l CaseSensitivelySortedModel |
1567 | or \l CaseInsensitivelySortedModel. On large models, this can lead to |
1568 | significant performance improvements because the completer object can |
1569 | then use a binary search algorithm instead of linear search algorithm. |
1570 | |
1571 | The sort order (i.e ascending or descending order) of the model is determined |
1572 | dynamically by inspecting the contents of the model. |
1573 | |
1574 | \b{Note:} The performance improvements described above cannot take place |
1575 | when the completer's \l caseSensitivity is different to the case sensitivity |
1576 | used by the model's when sorting. |
1577 | |
1578 | \sa setCaseSensitivity(), QCompleter::ModelSorting |
1579 | */ |
1580 | void QCompleter::setModelSorting(QCompleter::ModelSorting sorting) |
1581 | { |
1582 | Q_D(QCompleter); |
1583 | if (d->sorting == sorting) |
1584 | return; |
1585 | d->sorting = sorting; |
1586 | d->proxy->createEngine(); |
1587 | d->proxy->invalidate(); |
1588 | } |
1589 | |
1590 | QCompleter::ModelSorting QCompleter::modelSorting() const |
1591 | { |
1592 | Q_D(const QCompleter); |
1593 | return d->sorting; |
1594 | } |
1595 | |
1596 | /*! |
1597 | \property QCompleter::completionColumn |
1598 | \brief the column in the model in which completions are searched for. |
1599 | |
1600 | If the popup() is a QListView, it is automatically setup to display |
1601 | this column. |
1602 | |
1603 | By default, the match column is 0. |
1604 | |
1605 | \sa completionRole, caseSensitivity |
1606 | */ |
1607 | void QCompleter::setCompletionColumn(int column) |
1608 | { |
1609 | Q_D(QCompleter); |
1610 | if (d->column == column) |
1611 | return; |
1612 | #if QT_CONFIG(listview) |
1613 | if (QListView *listView = qobject_cast<QListView *>(object: d->popup)) |
1614 | listView->setModelColumn(column); |
1615 | #endif |
1616 | d->column = column; |
1617 | d->proxy->invalidate(); |
1618 | } |
1619 | |
1620 | int QCompleter::completionColumn() const |
1621 | { |
1622 | Q_D(const QCompleter); |
1623 | return d->column; |
1624 | } |
1625 | |
1626 | /*! |
1627 | \property QCompleter::completionRole |
1628 | \brief the item role to be used to query the contents of items for matching. |
1629 | |
1630 | The default role is Qt::EditRole. |
1631 | |
1632 | \sa completionColumn, caseSensitivity |
1633 | */ |
1634 | void QCompleter::setCompletionRole(int role) |
1635 | { |
1636 | Q_D(QCompleter); |
1637 | if (d->role == role) |
1638 | return; |
1639 | d->role = role; |
1640 | d->proxy->invalidate(); |
1641 | } |
1642 | |
1643 | int QCompleter::completionRole() const |
1644 | { |
1645 | Q_D(const QCompleter); |
1646 | return d->role; |
1647 | } |
1648 | |
1649 | /*! |
1650 | \property QCompleter::wrapAround |
1651 | \brief the completions wrap around when navigating through items |
1652 | \since 4.3 |
1653 | |
1654 | The default is true. |
1655 | */ |
1656 | void QCompleter::setWrapAround(bool wrap) |
1657 | { |
1658 | Q_D(QCompleter); |
1659 | if (d->wrap == wrap) |
1660 | return; |
1661 | d->wrap = wrap; |
1662 | } |
1663 | |
1664 | bool QCompleter::wrapAround() const |
1665 | { |
1666 | Q_D(const QCompleter); |
1667 | return d->wrap; |
1668 | } |
1669 | |
1670 | /*! |
1671 | \property QCompleter::maxVisibleItems |
1672 | \brief the maximum allowed size on screen of the completer, measured in items |
1673 | \since 4.6 |
1674 | |
1675 | By default, this property has a value of 7. |
1676 | */ |
1677 | int QCompleter::maxVisibleItems() const |
1678 | { |
1679 | Q_D(const QCompleter); |
1680 | return d->maxVisibleItems; |
1681 | } |
1682 | |
1683 | void QCompleter::setMaxVisibleItems(int maxItems) |
1684 | { |
1685 | Q_D(QCompleter); |
1686 | if (Q_UNLIKELY(maxItems < 0)) { |
1687 | qWarning(msg: "QCompleter::setMaxVisibleItems: " |
1688 | "Invalid max visible items (%d) must be >= 0" , maxItems); |
1689 | return; |
1690 | } |
1691 | d->maxVisibleItems = maxItems; |
1692 | } |
1693 | |
1694 | /*! |
1695 | \property QCompleter::caseSensitivity |
1696 | \brief the case sensitivity of the matching |
1697 | |
1698 | The default value is \c Qt::CaseSensitive. |
1699 | |
1700 | \sa completionColumn, completionRole, modelSorting, filterMode |
1701 | */ |
1702 | void QCompleter::setCaseSensitivity(Qt::CaseSensitivity cs) |
1703 | { |
1704 | Q_D(QCompleter); |
1705 | if (d->cs == cs) |
1706 | return; |
1707 | d->cs = cs; |
1708 | d->proxy->createEngine(); |
1709 | d->proxy->invalidate(); |
1710 | } |
1711 | |
1712 | Qt::CaseSensitivity QCompleter::caseSensitivity() const |
1713 | { |
1714 | Q_D(const QCompleter); |
1715 | return d->cs; |
1716 | } |
1717 | |
1718 | /*! |
1719 | \property QCompleter::completionPrefix |
1720 | \brief the completion prefix used to provide completions. |
1721 | |
1722 | The completionModel() is updated to reflect the list of possible |
1723 | matches for \a prefix. |
1724 | */ |
1725 | void QCompleter::setCompletionPrefix(const QString &prefix) |
1726 | { |
1727 | Q_D(QCompleter); |
1728 | d->prefix = prefix; |
1729 | d->proxy->filter(parts: splitPath(path: prefix)); |
1730 | } |
1731 | |
1732 | QString QCompleter::completionPrefix() const |
1733 | { |
1734 | Q_D(const QCompleter); |
1735 | return d->prefix; |
1736 | } |
1737 | |
1738 | /*! |
1739 | Returns the model index of the current completion in the completionModel(). |
1740 | |
1741 | \sa setCurrentRow(), currentCompletion(), model() |
1742 | */ |
1743 | QModelIndex QCompleter::currentIndex() const |
1744 | { |
1745 | Q_D(const QCompleter); |
1746 | return d->proxy->currentIndex(sourceIndex: false); |
1747 | } |
1748 | |
1749 | /*! |
1750 | Returns the current completion string. This includes the \l completionPrefix. |
1751 | When used alongside setCurrentRow(), it can be used to iterate through |
1752 | all the matches. |
1753 | |
1754 | \sa setCurrentRow(), currentIndex() |
1755 | */ |
1756 | QString QCompleter::currentCompletion() const |
1757 | { |
1758 | Q_D(const QCompleter); |
1759 | return pathFromIndex(index: d->proxy->currentIndex(sourceIndex: true)); |
1760 | } |
1761 | |
1762 | /*! |
1763 | Returns the completion model. The completion model is a read-only list model |
1764 | that contains all the possible matches for the current completion prefix. |
1765 | The completion model is auto-updated to reflect the current completions. |
1766 | |
1767 | \note The return value of this function is defined to be an QAbstractItemModel |
1768 | purely for generality. This actual kind of model returned is an instance of an |
1769 | QAbstractProxyModel subclass. |
1770 | |
1771 | \sa completionPrefix, model() |
1772 | */ |
1773 | QAbstractItemModel *QCompleter::completionModel() const |
1774 | { |
1775 | Q_D(const QCompleter); |
1776 | return d->proxy; |
1777 | } |
1778 | |
1779 | /*! |
1780 | Returns the path for the given \a index. The completer object uses this to |
1781 | obtain the completion text from the underlying model. |
1782 | |
1783 | The default implementation returns the \l{Qt::EditRole}{edit role} of the |
1784 | item for list models. It returns the absolute file path if the model is a |
1785 | QFileSystemModel. |
1786 | |
1787 | \sa splitPath() |
1788 | */ |
1789 | |
1790 | QString QCompleter::pathFromIndex(const QModelIndex& index) const |
1791 | { |
1792 | Q_D(const QCompleter); |
1793 | if (!index.isValid()) |
1794 | return QString(); |
1795 | |
1796 | QAbstractItemModel *sourceModel = d->proxy->sourceModel(); |
1797 | if (!sourceModel) |
1798 | return QString(); |
1799 | bool isFsModel = false; |
1800 | #if QT_CONFIG(filesystemmodel) |
1801 | isFsModel = qobject_cast<QFileSystemModel *>(object: d->proxy->sourceModel()) != nullptr; |
1802 | #endif |
1803 | if (!isFsModel) |
1804 | return sourceModel->data(index, role: d->role).toString(); |
1805 | |
1806 | QModelIndex idx = index; |
1807 | QStringList list; |
1808 | do { |
1809 | QString t; |
1810 | #if QT_CONFIG(filesystemmodel) |
1811 | t = sourceModel->data(index: idx, role: QFileSystemModel::FileNameRole).toString(); |
1812 | #endif |
1813 | list.prepend(t); |
1814 | QModelIndex parent = idx.parent(); |
1815 | idx = parent.sibling(arow: parent.row(), acolumn: index.column()); |
1816 | } while (idx.isValid()); |
1817 | |
1818 | #if !defined(Q_OS_WIN) |
1819 | if (list.size() == 1) // only the separator or some other text |
1820 | return list[0]; |
1821 | list[0].clear() ; // the join below will provide the separator |
1822 | #endif |
1823 | |
1824 | return list.join(sep: QDir::separator()); |
1825 | } |
1826 | |
1827 | /*! |
1828 | Splits the given \a path into strings that are used to match at each level |
1829 | in the model(). |
1830 | |
1831 | The default implementation of splitPath() splits a file system path based on |
1832 | QDir::separator() when the sourceModel() is a QFileSystemModel. |
1833 | |
1834 | When used with list models, the first item in the returned list is used for |
1835 | matching. |
1836 | |
1837 | \sa pathFromIndex(), {Handling Tree Models} |
1838 | */ |
1839 | QStringList QCompleter::splitPath(const QString& path) const |
1840 | { |
1841 | bool isFsModel = false; |
1842 | #if QT_CONFIG(filesystemmodel) |
1843 | Q_D(const QCompleter); |
1844 | isFsModel = qobject_cast<QFileSystemModel *>(object: d->proxy->sourceModel()) != nullptr; |
1845 | #endif |
1846 | |
1847 | if (!isFsModel || path.isEmpty()) |
1848 | return QStringList(completionPrefix()); |
1849 | |
1850 | QString pathCopy = QDir::toNativeSeparators(pathName: path); |
1851 | #if defined(Q_OS_WIN) |
1852 | if (pathCopy == "\\"_L1 || pathCopy == "\\\\"_L1 ) |
1853 | return QStringList(pathCopy); |
1854 | const bool startsWithDoubleSlash = pathCopy.startsWith("\\\\"_L1 ); |
1855 | if (startsWithDoubleSlash) |
1856 | pathCopy = pathCopy.mid(2); |
1857 | #endif |
1858 | |
1859 | const QChar sep = QDir::separator(); |
1860 | QStringList parts = pathCopy.split(sep); |
1861 | |
1862 | #if defined(Q_OS_WIN) |
1863 | if (startsWithDoubleSlash) |
1864 | parts[0].prepend("\\\\"_L1 ); |
1865 | #else |
1866 | if (pathCopy[0] == sep) // readd the "/" at the beginning as the split removed it |
1867 | parts[0] = u'/'; |
1868 | #endif |
1869 | |
1870 | return parts; |
1871 | } |
1872 | |
1873 | /*! |
1874 | \fn void QCompleter::activated(const QModelIndex& index) |
1875 | |
1876 | This signal is sent when an item in the popup() is activated by the user. |
1877 | (by clicking or pressing return). The item's \a index in the completionModel() |
1878 | is given. |
1879 | |
1880 | */ |
1881 | |
1882 | /*! |
1883 | \fn void QCompleter::activated(const QString &text) |
1884 | |
1885 | This signal is sent when an item in the popup() is activated by the user (by |
1886 | clicking or pressing return). The item's \a text is given. |
1887 | |
1888 | */ |
1889 | |
1890 | /*! |
1891 | \fn void QCompleter::highlighted(const QModelIndex& index) |
1892 | |
1893 | This signal is sent when an item in the popup() is highlighted by |
1894 | the user. It is also sent if complete() is called with the completionMode() |
1895 | set to QCompleter::InlineCompletion. The item's \a index in the completionModel() |
1896 | is given. |
1897 | */ |
1898 | |
1899 | /*! |
1900 | \fn void QCompleter::highlighted(const QString &text) |
1901 | |
1902 | This signal is sent when an item in the popup() is highlighted by |
1903 | the user. It is also sent if complete() is called with the completionMode() |
1904 | set to QCompleter::InlineCompletion. The item's \a text is given. |
1905 | */ |
1906 | |
1907 | QT_END_NAMESPACE |
1908 | |
1909 | #include "moc_qcompleter.cpp" |
1910 | |
1911 | #include "moc_qcompleter_p.cpp" |
1912 | |