1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "documentmodel.h"
27 #include "documentmodel_p.h"
28 
29 #include "ieditor.h"
30 #include <coreplugin/documentmanager.h>
31 #include <coreplugin/idocument.h>
32 
33 #include <utils/algorithm.h>
34 #include <utils/dropsupport.h>
35 #include <utils/hostosinfo.h>
36 #include <utils/qtcassert.h>
37 #include <utils/utilsicons.h>
38 
39 #include <QAbstractItemModel>
40 #include <QDir>
41 #include <QIcon>
42 #include <QMimeData>
43 #include <QSet>
44 #include <QUrl>
45 
46 using namespace Utils;
47 
48 static Core::Internal::DocumentModelPrivate *d;
49 
50 namespace Core {
51 namespace Internal {
52 
53 namespace {
compare(const DocumentModel::Entry * e1,const DocumentModel::Entry * e2)54 bool compare(const DocumentModel::Entry *e1, const DocumentModel::Entry *e2)
55 {
56     // Pinned files should go at the top.
57     if (e1->pinned != e2->pinned)
58         return e1->pinned;
59 
60     const int cmp = e1->plainDisplayName().localeAwareCompare(e2->plainDisplayName());
61     return (cmp < 0) || (cmp == 0 && e1->fileName() < e2->fileName());
62 }
63 
64 // Return a pair of indices. The first is the index that needs to be removed or -1 if no removal
65 // is necessary. The second is the index to add the entry into, or -1 if no addition is necessary.
66 // If the entry does not need to be moved, then (-1, -1) will be returned as no action is needed.
positionEntry(const QList<DocumentModel::Entry * > & list,DocumentModel::Entry * entry)67 std::pair<int, int> positionEntry(const QList<DocumentModel::Entry *> &list,
68                                   DocumentModel::Entry *entry)
69 {
70     const int to_remove = list.indexOf(entry);
71 
72     const QList<DocumentModel::Entry *> toSort
73             = Utils::filtered(list, [entry](DocumentModel::Entry *e) { return e != entry; });
74 
75     const auto begin = std::begin(toSort);
76     const auto end = std::end(toSort);
77     const auto to_insert
78             = static_cast<int>(std::distance(begin, std::lower_bound(begin, end, entry, &compare)));
79     if (to_remove == to_insert)
80         return std::make_pair(-1, -1);
81     return std::make_pair(to_remove, to_insert);
82 }
83 } // namespace
84 
~DocumentModelPrivate()85 DocumentModelPrivate::~DocumentModelPrivate()
86 {
87     qDeleteAll(m_entries);
88 }
89 
columnCount(const QModelIndex & parent) const90 int DocumentModelPrivate::columnCount(const QModelIndex &parent) const
91 {
92     if (!parent.isValid())
93         return 2;
94     return 0;
95 }
96 
rowCount(const QModelIndex & parent) const97 int DocumentModelPrivate::rowCount(const QModelIndex &parent) const
98 {
99     if (!parent.isValid())
100         return m_entries.count() + 1/*<no document>*/;
101     return 0;
102 }
103 
addEntry(DocumentModel::Entry * entry)104 void DocumentModelPrivate::addEntry(DocumentModel::Entry *entry)
105 {
106     const Utils::FilePath filePath = entry->fileName();
107 
108     // replace a non-loaded entry (aka 'suspended') if possible
109     DocumentModel::Entry *previousEntry = DocumentModel::entryForFilePath(filePath);
110     if (previousEntry) {
111         const bool replace = !entry->isSuspended && previousEntry->isSuspended;
112         if (replace) {
113             previousEntry->isSuspended = false;
114             delete previousEntry->document;
115             previousEntry->document = entry->document;
116             connect(previousEntry->document, &IDocument::changed,
117                     this, [this, document = previousEntry->document] { itemChanged(document); });
118         }
119         delete entry;
120         entry = nullptr;
121         disambiguateDisplayNames(previousEntry);
122         return;
123     }
124 
125     auto positions = positionEntry(m_entries, entry);
126     // Do not remove anything (new entry), insert somewhere:
127     QTC_CHECK(positions.first == -1 && positions.second >= 0);
128 
129     int row = positions.second + 1/*<no document>*/;
130     beginInsertRows(QModelIndex(), row, row);
131     m_entries.insert(positions.second, entry);
132     disambiguateDisplayNames(entry);
133     FilePath fixedPath = DocumentManager::filePathKey(filePath, DocumentManager::ResolveLinks);
134     if (!fixedPath.isEmpty())
135         m_entryByFixedPath[fixedPath] = entry;
136     connect(entry->document, &IDocument::changed, this, [this, document = entry->document] {
137         itemChanged(document);
138     });
139     endInsertRows();
140 }
141 
disambiguateDisplayNames(DocumentModel::Entry * entry)142 bool DocumentModelPrivate::disambiguateDisplayNames(DocumentModel::Entry *entry)
143 {
144     const QString displayName = entry->plainDisplayName();
145     int minIdx = -1, maxIdx = -1;
146 
147     QList<DynamicEntry> dups;
148 
149     for (int i = 0, total = m_entries.count(); i < total; ++i) {
150         DocumentModel::Entry *e = m_entries.at(i);
151         if (e == entry || e->plainDisplayName() == displayName) {
152             e->document->setUniqueDisplayName(QString());
153             dups += DynamicEntry(e);
154             maxIdx = i;
155             if (minIdx < 0)
156                 minIdx = i;
157         }
158     }
159 
160     const int dupsCount = dups.count();
161     if (dupsCount == 0)
162         return false;
163 
164     if (dupsCount > 1) {
165         int serial = 0;
166         int count = 0;
167         // increase uniqueness unless no dups are left
168         forever {
169             bool seenDups = false;
170             for (int i = 0; i < dupsCount - 1; ++i) {
171                 DynamicEntry &e = dups[i];
172                 const Utils::FilePath myFileName = e->document->filePath();
173                 if (e->document->isTemporary() || myFileName.isEmpty() || count > 10) {
174                     // path-less entry, append number
175                     e.setNumberedName(++serial);
176                     continue;
177                 }
178                 for (int j = i + 1; j < dupsCount; ++j) {
179                     DynamicEntry &e2 = dups[j];
180                     if (e->displayName().compare(e2->displayName(), Utils::HostOsInfo::fileNameCaseSensitivity()) == 0) {
181                         const Utils::FilePath otherFileName = e2->document->filePath();
182                         if (otherFileName.isEmpty())
183                             continue;
184                         seenDups = true;
185                         e2.disambiguate();
186                         if (j > maxIdx)
187                             maxIdx = j;
188                     }
189                 }
190                 if (seenDups) {
191                     e.disambiguate();
192                     ++count;
193                     break;
194                 }
195             }
196             if (!seenDups)
197                 break;
198         }
199     }
200 
201     emit dataChanged(index(minIdx + 1, 0), index(maxIdx + 1, 0));
202     return true;
203 }
204 
setPinned(DocumentModel::Entry * entry,bool pinned)205 void DocumentModelPrivate::setPinned(DocumentModel::Entry *entry, bool pinned)
206 {
207     if (entry->pinned == pinned)
208         return;
209 
210     entry->pinned = pinned;
211     // Ensure that this entry is re-sorted in the list of open documents
212     // now that its pinned state has changed.
213     d->itemChanged(entry->document);
214 }
215 
lockedIcon()216 QIcon DocumentModelPrivate::lockedIcon()
217 {
218     const static QIcon icon = Utils::Icons::LOCKED.icon();
219     return icon;
220 }
221 
pinnedIcon()222 QIcon DocumentModelPrivate::pinnedIcon()
223 {
224     const static QIcon icon = Utils::Icons::PINNED.icon();
225     return icon;
226 }
227 
indexOfFilePath(const Utils::FilePath & filePath) const228 Utils::optional<int> DocumentModelPrivate::indexOfFilePath(const Utils::FilePath &filePath) const
229 {
230     if (filePath.isEmpty())
231         return Utils::nullopt;
232     const FilePath fixedPath = DocumentManager::filePathKey(filePath, DocumentManager::ResolveLinks);
233     const int index = m_entries.indexOf(m_entryByFixedPath.value(fixedPath));
234     if (index < 0)
235         return Utils::nullopt;
236     return index;
237 }
238 
removeDocument(int idx)239 void DocumentModelPrivate::removeDocument(int idx)
240 {
241     if (idx < 0)
242         return;
243     QTC_ASSERT(idx < m_entries.size(), return);
244     int row = idx + 1/*<no document>*/;
245     beginRemoveRows(QModelIndex(), row, row);
246     DocumentModel::Entry *entry = m_entries.takeAt(idx);
247     endRemoveRows();
248 
249     const FilePath fixedPath = DocumentManager::filePathKey(entry->fileName(),
250                                                             DocumentManager::ResolveLinks);
251     if (!fixedPath.isEmpty())
252         m_entryByFixedPath.remove(fixedPath);
253     disconnect(entry->document, &IDocument::changed, this, nullptr);
254     disambiguateDisplayNames(entry);
255     delete entry;
256 }
257 
indexOfDocument(IDocument * document) const258 Utils::optional<int> DocumentModelPrivate::indexOfDocument(IDocument *document) const
259 {
260     const int index = Utils::indexOf(m_entries, [&document](DocumentModel::Entry *entry) {
261         return entry->document == document;
262     });
263     if (index < 0)
264         return Utils::nullopt;
265     return index;
266 }
267 
flags(const QModelIndex & index) const268 Qt::ItemFlags DocumentModelPrivate::flags(const QModelIndex &index) const
269 {
270     const DocumentModel::Entry *e = DocumentModel::entryAtRow(index.row());
271     if (!e || e->fileName().isEmpty())
272         return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
273     return Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
274 }
275 
mimeData(const QModelIndexList & indexes) const276 QMimeData *DocumentModelPrivate::mimeData(const QModelIndexList &indexes) const
277 {
278     auto data = new Utils::DropMimeData;
279     foreach (const QModelIndex &index, indexes) {
280         const DocumentModel::Entry *e = DocumentModel::entryAtRow(index.row());
281         if (!e || e->fileName().isEmpty())
282             continue;
283         data->addFile(e->fileName().toString());
284     }
285     return data;
286 }
287 
index(int row,int column,const QModelIndex & parent) const288 QModelIndex DocumentModelPrivate::index(int row, int column, const QModelIndex &parent) const
289 {
290     Q_UNUSED(parent)
291     if (column < 0 || column > 1 || row < 0 || row >= m_entries.count() + 1/*<no document>*/)
292         return QModelIndex();
293     return createIndex(row, column);
294 }
295 
supportedDragActions() const296 Qt::DropActions DocumentModelPrivate::supportedDragActions() const
297 {
298     return Qt::MoveAction;
299 }
300 
mimeTypes() const301 QStringList DocumentModelPrivate::mimeTypes() const
302 {
303     return Utils::DropSupport::mimeTypesForFilePaths();
304 }
305 
data(const QModelIndex & index,int role) const306 QVariant DocumentModelPrivate::data(const QModelIndex &index, int role) const
307 {
308     if (!index.isValid() || (index.column() != 0 && role < Qt::UserRole))
309         return QVariant();
310     const DocumentModel::Entry *entry = DocumentModel::entryAtRow(index.row());
311     if (!entry) {
312         // <no document> entry
313         switch (role) {
314         case Qt::DisplayRole:
315             return tr("<no document>");
316         case Qt::ToolTipRole:
317             return tr("No document is selected.");
318         default:
319             return QVariant();
320         }
321     }
322     switch (role) {
323     case Qt::DisplayRole: {
324         QString name = entry->displayName();
325         if (entry->document->isModified())
326             name += QLatin1Char('*');
327         return name;
328     }
329     case Qt::DecorationRole:
330         if (entry->document->isFileReadOnly())
331             return lockedIcon();
332         if (entry->pinned)
333             return pinnedIcon();
334         return QVariant();
335     case Qt::ToolTipRole:
336         return entry->fileName().isEmpty() ? entry->displayName() : entry->fileName().toUserOutput();
337     default:
338         break;
339     }
340     return QVariant();
341 }
342 
itemChanged(IDocument * document)343 void DocumentModelPrivate::itemChanged(IDocument *document)
344 {
345     const Utils::optional<int> idx = indexOfDocument(document);
346     if (!idx)
347         return;
348     const FilePath fixedPath = DocumentManager::filePathKey(document->filePath(),
349                                                             DocumentManager::ResolveLinks);
350     DocumentModel::Entry *entry = m_entries.at(idx.value());
351     bool found = false;
352     // The entry's fileName might have changed, so find the previous fileName that was associated
353     // with it and remove it, then add the new fileName.
354     for (auto it = m_entryByFixedPath.begin(), end = m_entryByFixedPath.end(); it != end; ++it) {
355         if (it.value() == entry) {
356             found = true;
357             if (it.key() != fixedPath) {
358                 m_entryByFixedPath.remove(it.key());
359                 if (!fixedPath.isEmpty())
360                     m_entryByFixedPath[fixedPath] = entry;
361             }
362             break;
363         }
364     }
365     if (!found && !fixedPath.isEmpty())
366         m_entryByFixedPath[fixedPath] = entry;
367 
368     if (!disambiguateDisplayNames(m_entries.at(idx.value()))) {
369         QModelIndex mindex = index(idx.value() + 1/*<no document>*/, 0);
370         emit dataChanged(mindex, mindex);
371     }
372 
373     // Make sure the entries stay sorted:
374     auto positions = positionEntry(m_entries, entry);
375     if (positions.first >= 0 && positions.second >= 0) {
376         // Entry did move: update its position.
377 
378         // Account for the <no document> entry.
379         static const int noDocumentEntryOffset = 1;
380         const int fromIndex = positions.first + noDocumentEntryOffset;
381         const int toIndex = positions.second + noDocumentEntryOffset;
382         // Account for the weird requirements of beginMoveRows().
383         const int effectiveToIndex = toIndex > fromIndex ? toIndex + 1 : toIndex;
384         beginMoveRows(QModelIndex(), fromIndex, fromIndex, QModelIndex(), effectiveToIndex);
385 
386         m_entries.move(fromIndex - 1, toIndex - 1);
387 
388         endMoveRows();
389     } else {
390         // Nothing to remove or add: The entry did not move.
391         QTC_CHECK(positions.first == -1 && positions.second == -1);
392     }
393 }
394 
addEditor(IEditor * editor,bool * isNewDocument)395 void DocumentModelPrivate::addEditor(IEditor *editor, bool *isNewDocument)
396 {
397     if (!editor)
398         return;
399 
400     QList<IEditor *> &editorList = d->m_editors[editor->document()];
401     bool isNew = editorList.isEmpty();
402     if (isNewDocument)
403         *isNewDocument = isNew;
404     editorList << editor;
405     if (isNew) {
406         auto entry = new DocumentModel::Entry;
407         entry->document = editor->document();
408         d->addEntry(entry);
409     }
410 }
411 
412 /*!
413     \class Core::DocumentModel
414     \inmodule QtCreator
415     \internal
416 */
417 
418 /*!
419     \class Core::DocumentModel::Entry
420     \inmodule QtCreator
421     \internal
422 */
423 
addSuspendedDocument(const QString & fileName,const QString & displayName,Id id)424 DocumentModel::Entry *DocumentModelPrivate::addSuspendedDocument(const QString &fileName,
425                                                                  const QString &displayName,
426                                                                  Id id)
427 {
428     QTC_CHECK(id.isValid());
429     auto entry = new DocumentModel::Entry;
430     entry->document = new IDocument;
431     entry->document->setFilePath(Utils::FilePath::fromString(fileName));
432     if (!displayName.isEmpty())
433         entry->document->setPreferredDisplayName(displayName);
434     entry->document->setId(id);
435     entry->isSuspended = true;
436     d->addEntry(entry);
437     return entry;
438 }
439 
firstSuspendedEntry()440 DocumentModel::Entry *DocumentModelPrivate::firstSuspendedEntry()
441 {
442     return Utils::findOrDefault(d->m_entries, [](DocumentModel::Entry *entry) { return entry->isSuspended; });
443 }
444 
445 /*!
446     Removes an editor from the list of open editors for its entry. If the editor is the last
447     one, the entry is put into suspended state.
448     Returns the affected entry.
449 */
removeEditor(IEditor * editor)450 DocumentModel::Entry *DocumentModelPrivate::removeEditor(IEditor *editor)
451 {
452     QTC_ASSERT(editor, return nullptr);
453     IDocument *document = editor->document();
454     QTC_ASSERT(d->m_editors.contains(document), return nullptr);
455     d->m_editors[document].removeAll(editor);
456     DocumentModel::Entry *entry = DocumentModel::entryForDocument(document);
457     QTC_ASSERT(entry, return nullptr);
458     if (d->m_editors.value(document).isEmpty()) {
459         d->m_editors.remove(document);
460         entry->document = new IDocument;
461         entry->document->setFilePath(document->filePath());
462         entry->document->setPreferredDisplayName(document->preferredDisplayName());
463         entry->document->setUniqueDisplayName(document->uniqueDisplayName());
464         entry->document->setId(document->id());
465         entry->isSuspended = true;
466     }
467     return entry;
468 }
469 
removeEntry(DocumentModel::Entry * entry)470 void DocumentModelPrivate::removeEntry(DocumentModel::Entry *entry)
471 {
472     // For non suspended entries, we wouldn't know what to do with the associated editors
473     QTC_ASSERT(entry->isSuspended, return);
474     int index = d->m_entries.indexOf(entry);
475     d->removeDocument(index);
476 }
477 
removeAllSuspendedEntries(PinnedFileRemovalPolicy pinnedFileRemovalPolicy)478 void DocumentModelPrivate::removeAllSuspendedEntries(PinnedFileRemovalPolicy pinnedFileRemovalPolicy)
479 {
480     for (int i = d->m_entries.count()-1; i >= 0; --i) {
481         const DocumentModel::Entry *entry = d->m_entries.at(i);
482         if (!entry->isSuspended)
483             continue;
484 
485         if (pinnedFileRemovalPolicy == DoNotRemovePinnedFiles && entry->pinned)
486             continue;
487 
488         int row = i + 1/*<no document>*/;
489         d->beginRemoveRows(QModelIndex(), row, row);
490         delete d->m_entries.takeAt(i);
491         d->endRemoveRows();
492     }
493     QSet<QString> displayNames;
494     foreach (DocumentModel::Entry *entry, d->m_entries) {
495         const QString displayName = entry->plainDisplayName();
496         if (displayNames.contains(displayName))
497             continue;
498         displayNames.insert(displayName);
499         d->disambiguateDisplayNames(entry);
500     }
501 }
502 
DynamicEntry(DocumentModel::Entry * e)503 DocumentModelPrivate::DynamicEntry::DynamicEntry(DocumentModel::Entry *e) :
504     entry(e),
505     pathComponents(0)
506 {
507 }
508 
operator ->() const509 DocumentModel::Entry *DocumentModelPrivate::DynamicEntry::operator->() const
510 {
511     return entry;
512 }
513 
disambiguate()514 void DocumentModelPrivate::DynamicEntry::disambiguate()
515 {
516     const QString display = entry->fileName().fileNameWithPathComponents(++pathComponents);
517     entry->document->setUniqueDisplayName(display);
518 }
519 
setNumberedName(int number)520 void DocumentModelPrivate::DynamicEntry::setNumberedName(int number)
521 {
522     entry->document->setUniqueDisplayName(QStringLiteral("%1 (%2)")
523                                           .arg(entry->document->displayName())
524                                           .arg(number));
525 }
526 
527 } // Internal
528 
Entry()529 DocumentModel::Entry::Entry() :
530     document(nullptr),
531     isSuspended(false),
532     pinned(false)
533 {
534 }
535 
~Entry()536 DocumentModel::Entry::~Entry()
537 {
538     if (isSuspended)
539         delete document;
540 }
541 
542 DocumentModel::DocumentModel() = default;
543 
init()544 void DocumentModel::init()
545 {
546     d = new Internal::DocumentModelPrivate;
547 }
548 
destroy()549 void DocumentModel::destroy()
550 {
551     delete d;
552 }
553 
lockedIcon()554 QIcon DocumentModel::lockedIcon()
555 {
556     return Internal::DocumentModelPrivate::lockedIcon();
557 }
558 
model()559 QAbstractItemModel *DocumentModel::model()
560 {
561     return d;
562 }
563 
fileName() const564 Utils::FilePath DocumentModel::Entry::fileName() const
565 {
566     return document->filePath();
567 }
568 
displayName() const569 QString DocumentModel::Entry::displayName() const
570 {
571     return document->displayName();
572 }
573 
plainDisplayName() const574 QString DocumentModel::Entry::plainDisplayName() const
575 {
576     return document->plainDisplayName();
577 }
578 
id() const579 Id DocumentModel::Entry::id() const
580 {
581     return document->id();
582 }
583 
editorsForDocument(IDocument * document)584 QList<IEditor *> DocumentModel::editorsForDocument(IDocument *document)
585 {
586     return d->m_editors.value(document);
587 }
588 
editorsForOpenedDocuments()589 QList<IEditor *> DocumentModel::editorsForOpenedDocuments()
590 {
591     return editorsForDocuments(openedDocuments());
592 }
593 
editorsForDocuments(const QList<IDocument * > & documents)594 QList<IEditor *> DocumentModel::editorsForDocuments(const QList<IDocument *> &documents)
595 {
596     QList<IEditor *> result;
597     foreach (IDocument *document, documents)
598         result += d->m_editors.value(document);
599     return result;
600 }
601 
indexOfDocument(IDocument * document)602 Utils::optional<int> DocumentModel::indexOfDocument(IDocument *document)
603 {
604     return d->indexOfDocument(document);
605 }
606 
indexOfFilePath(const Utils::FilePath & filePath)607 Utils::optional<int> DocumentModel::indexOfFilePath(const Utils::FilePath &filePath)
608 {
609     return d->indexOfFilePath(filePath);
610 }
611 
entryForDocument(IDocument * document)612 DocumentModel::Entry *DocumentModel::entryForDocument(IDocument *document)
613 {
614     return Utils::findOrDefault(d->m_entries,
615                                 [&document](Entry *entry) { return entry->document == document; });
616 }
617 
entryForFilePath(const Utils::FilePath & filePath)618 DocumentModel::Entry *DocumentModel::entryForFilePath(const Utils::FilePath &filePath)
619 {
620     const Utils::optional<int> index = d->indexOfFilePath(filePath);
621     if (!index)
622         return nullptr;
623     return d->m_entries.at(*index);
624 }
625 
openedDocuments()626 QList<IDocument *> DocumentModel::openedDocuments()
627 {
628     return d->m_editors.keys();
629 }
630 
documentForFilePath(const Utils::FilePath & filePath)631 IDocument *DocumentModel::documentForFilePath(const Utils::FilePath &filePath)
632 {
633     const Utils::optional<int> index = d->indexOfFilePath(filePath);
634     if (!index)
635         return nullptr;
636     return d->m_entries.at(*index)->document;
637 }
638 
editorsForFilePath(const Utils::FilePath & filePath)639 QList<IEditor *> DocumentModel::editorsForFilePath(const Utils::FilePath &filePath)
640 {
641     IDocument *document = documentForFilePath(filePath);
642     if (document)
643         return editorsForDocument(document);
644     return QList<IEditor *>();
645 }
646 
entryAtRow(int row)647 DocumentModel::Entry *DocumentModel::entryAtRow(int row)
648 {
649     int entryIndex = row - 1/*<no document>*/;
650     if (entryIndex < 0)
651         return nullptr;
652     return d->m_entries[entryIndex];
653 }
654 
entryCount()655 int DocumentModel::entryCount()
656 {
657     return d->m_entries.count();
658 }
659 
rowOfDocument(IDocument * document)660 Utils::optional<int> DocumentModel::rowOfDocument(IDocument *document)
661 {
662     if (!document)
663         return 0 /*<no document>*/;
664     const Utils::optional<int> index = indexOfDocument(document);
665     if (index)
666         return *index + 1/*correction for <no document>*/;
667     return Utils::nullopt;
668 }
669 
entries()670 QList<DocumentModel::Entry *> DocumentModel::entries()
671 {
672     return d->m_entries;
673 }
674 
675 } // namespace Core
676