1 /*
2     SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "tocmodel.h"
8 
9 #include <QApplication>
10 #include <QList>
11 #include <QTreeView>
12 #include <qdom.h>
13 
14 #include <QFont>
15 
16 #include "core/document.h"
17 #include "core/page.h"
18 
19 Q_DECLARE_METATYPE(QModelIndex)
20 
21 struct TOCItem {
22     TOCItem();
23     TOCItem(TOCItem *parent, const QDomElement &e);
24     ~TOCItem();
25 
26     TOCItem(const TOCItem &) = delete;
27     TOCItem &operator=(const TOCItem &) = delete;
28 
29     QString text;
30     Okular::DocumentViewport viewport;
31     QString extFileName;
32     QString url;
33     bool highlight : 1;
34     TOCItem *parent;
35     QList<TOCItem *> children;
36     TOCModelPrivate *model;
37 };
38 
39 class TOCModelPrivate
40 {
41 public:
42     explicit TOCModelPrivate(TOCModel *qq);
43     ~TOCModelPrivate();
44 
45     void addChildren(const QDomNode &parentNode, TOCItem *parentItem);
46     QModelIndex indexForItem(TOCItem *item) const;
47     void findViewport(const Okular::DocumentViewport &viewport, TOCItem *item, QList<TOCItem *> &list) const;
48 
49     TOCModel *q;
50     TOCItem *root;
51     bool dirty : 1;
52     Okular::Document *document;
53     QList<TOCItem *> itemsToOpen;
54     QList<TOCItem *> currentPage;
55     TOCModel *m_oldModel;
56     QVector<QModelIndex> m_oldTocExpandedIndexes;
57 };
58 
TOCItem()59 TOCItem::TOCItem()
60     : highlight(false)
61     , parent(nullptr)
62     , model(nullptr)
63 {
64 }
65 
TOCItem(TOCItem * _parent,const QDomElement & e)66 TOCItem::TOCItem(TOCItem *_parent, const QDomElement &e)
67     : highlight(false)
68     , parent(_parent)
69 {
70     parent->children.append(this);
71     model = parent->model;
72     text = e.tagName();
73 
74     // viewport loading
75     if (e.hasAttribute(QStringLiteral("Viewport"))) {
76         // if the node has a viewport, set it
77         viewport = Okular::DocumentViewport(e.attribute(QStringLiteral("Viewport")));
78     } else if (e.hasAttribute(QStringLiteral("ViewportName"))) {
79         // if the node references a viewport, get the reference and set it
80         const QString &page = e.attribute(QStringLiteral("ViewportName"));
81         QString viewport_string = model->document->metaData(QStringLiteral("NamedViewport"), page).toString();
82         if (!viewport_string.isEmpty())
83             viewport = Okular::DocumentViewport(viewport_string);
84     }
85 
86     extFileName = e.attribute(QStringLiteral("ExternalFileName"));
87     url = e.attribute(QStringLiteral("URL"));
88 }
89 
~TOCItem()90 TOCItem::~TOCItem()
91 {
92     qDeleteAll(children);
93 }
94 
TOCModelPrivate(TOCModel * qq)95 TOCModelPrivate::TOCModelPrivate(TOCModel *qq)
96     : q(qq)
97     , root(new TOCItem)
98     , dirty(false)
99     , m_oldModel(nullptr)
100 {
101     root->model = this;
102 }
103 
~TOCModelPrivate()104 TOCModelPrivate::~TOCModelPrivate()
105 {
106     delete root;
107     delete m_oldModel;
108 }
109 
addChildren(const QDomNode & parentNode,TOCItem * parentItem)110 void TOCModelPrivate::addChildren(const QDomNode &parentNode, TOCItem *parentItem)
111 {
112     TOCItem *currentItem = nullptr;
113     QDomNode n = parentNode.firstChild();
114     while (!n.isNull()) {
115         // convert the node to an element (sure it is)
116         QDomElement e = n.toElement();
117 
118         // insert the entry as top level (listview parented) or 2nd+ level
119         currentItem = new TOCItem(parentItem, e);
120 
121         // descend recursively and advance to the next node
122         if (e.hasChildNodes())
123             addChildren(n, currentItem);
124 
125         // open/keep close the item
126         bool isOpen = false;
127         if (e.hasAttribute(QStringLiteral("Open")))
128             isOpen = QVariant(e.attribute(QStringLiteral("Open"))).toBool();
129         if (isOpen)
130             itemsToOpen.append(currentItem);
131 
132         n = n.nextSibling();
133         emit q->countChanged();
134     }
135 }
136 
indexForItem(TOCItem * item) const137 QModelIndex TOCModelPrivate::indexForItem(TOCItem *item) const
138 {
139     if (item->parent) {
140         int id = item->parent->children.indexOf(item);
141         if (id >= 0 && id < item->parent->children.count())
142             return q->createIndex(id, 0, item);
143     }
144     return QModelIndex();
145 }
146 
findViewport(const Okular::DocumentViewport & viewport,TOCItem * item,QList<TOCItem * > & list) const147 void TOCModelPrivate::findViewport(const Okular::DocumentViewport &viewport, TOCItem *item, QList<TOCItem *> &list) const
148 {
149     TOCItem *todo = item;
150 
151     while (todo) {
152         const TOCItem *current = todo;
153         todo = nullptr;
154         TOCItem *pos = nullptr;
155 
156         for (TOCItem *child : current->children) {
157             if (child->viewport.isValid()) {
158                 if (child->viewport.pageNumber <= viewport.pageNumber) {
159                     pos = child;
160                     if (child->viewport.pageNumber == viewport.pageNumber) {
161                         break;
162                     }
163                 } else {
164                     break;
165                 }
166             }
167         }
168         if (pos) {
169             list.append(pos);
170             todo = pos;
171         }
172     }
173 }
174 
TOCModel(Okular::Document * document,QObject * parent)175 TOCModel::TOCModel(Okular::Document *document, QObject *parent)
176     : QAbstractItemModel(parent)
177     , d(new TOCModelPrivate(this))
178 {
179     d->document = document;
180 
181     qRegisterMetaType<QModelIndex>();
182 }
183 
~TOCModel()184 TOCModel::~TOCModel()
185 {
186     delete d;
187 }
188 
roleNames() const189 QHash<int, QByteArray> TOCModel::roleNames() const
190 {
191     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
192     roles[(int)PageItemDelegate::PageRole] = "page";
193     roles[(int)PageItemDelegate::PageLabelRole] = "pageLabel";
194     roles[HighlightRole] = "highlight";
195     roles[HighlightedParentRole] = "highlightedParent";
196     return roles;
197 }
198 
columnCount(const QModelIndex & parent) const199 int TOCModel::columnCount(const QModelIndex &parent) const
200 {
201     Q_UNUSED(parent)
202     return 1;
203 }
204 
data(const QModelIndex & index,int role) const205 QVariant TOCModel::data(const QModelIndex &index, int role) const
206 {
207     if (!index.isValid())
208         return QVariant();
209 
210     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
211     switch (role) {
212     case Qt::DisplayRole:
213     case Qt::ToolTipRole:
214         return item->text;
215         break;
216     case Qt::FontRole:
217         if (item->highlight) {
218             QFont font;
219             font.setBold(true);
220 
221             TOCItem *lastHighlighted = d->currentPage.last();
222 
223             // in the mobile version our parent is not a QTreeView; embolden the last highlighted item
224             // TODO misusing parent() here, fix
225             QTreeView *view = dynamic_cast<QTreeView *>(QObject::parent());
226             if (!view) {
227                 if (item == lastHighlighted)
228                     return font;
229                 return QVariant();
230             }
231 
232             if (view->isExpanded(index)) {
233                 // if this is the last highlighted node, its child is on a page below, thus it gets emboldened
234                 if (item == lastHighlighted)
235                     return font;
236             } else {
237                 return font;
238             }
239         }
240         break;
241     case HighlightRole:
242         return item->highlight;
243     case PageItemDelegate::PageRole:
244         if (item->viewport.isValid())
245             return item->viewport.pageNumber + 1;
246         break;
247     case PageItemDelegate::PageLabelRole:
248         if (item->viewport.isValid() && item->viewport.pageNumber < int(d->document->pages()))
249             return d->document->page(item->viewport.pageNumber)->label();
250         break;
251     }
252     return QVariant();
253 }
254 
hasChildren(const QModelIndex & parent) const255 bool TOCModel::hasChildren(const QModelIndex &parent) const
256 {
257     if (!parent.isValid())
258         return true;
259 
260     TOCItem *item = static_cast<TOCItem *>(parent.internalPointer());
261     return !item->children.isEmpty();
262 }
263 
headerData(int section,Qt::Orientation orientation,int role) const264 QVariant TOCModel::headerData(int section, Qt::Orientation orientation, int role) const
265 {
266     if (orientation != Qt::Horizontal)
267         return QVariant();
268 
269     if (section == 0 && role == Qt::DisplayRole)
270         return QStringLiteral("Topics");
271 
272     return QVariant();
273 }
274 
index(int row,int column,const QModelIndex & parent) const275 QModelIndex TOCModel::index(int row, int column, const QModelIndex &parent) const
276 {
277     if (row < 0 || column != 0)
278         return QModelIndex();
279 
280     TOCItem *item = parent.isValid() ? static_cast<TOCItem *>(parent.internalPointer()) : d->root;
281     if (row < item->children.count())
282         return createIndex(row, column, item->children.at(row));
283 
284     return QModelIndex();
285 }
286 
parent(const QModelIndex & index) const287 QModelIndex TOCModel::parent(const QModelIndex &index) const
288 {
289     if (!index.isValid())
290         return QModelIndex();
291 
292     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
293     return d->indexForItem(item->parent);
294 }
295 
rowCount(const QModelIndex & parent) const296 int TOCModel::rowCount(const QModelIndex &parent) const
297 {
298     TOCItem *item = parent.isValid() ? static_cast<TOCItem *>(parent.internalPointer()) : d->root;
299     return item->children.count();
300 }
301 
indexForIndex(const QModelIndex & oldModelIndex,QAbstractItemModel * newModel)302 static QModelIndex indexForIndex(const QModelIndex &oldModelIndex, QAbstractItemModel *newModel)
303 {
304     QModelIndex newModelIndex;
305     if (oldModelIndex.parent().isValid()) {
306         newModelIndex = newModel->index(oldModelIndex.row(), oldModelIndex.column(), indexForIndex(oldModelIndex.parent(), newModel));
307     } else {
308         newModelIndex = newModel->index(oldModelIndex.row(), oldModelIndex.column());
309     }
310     return newModelIndex;
311 }
312 
fill(const Okular::DocumentSynopsis * toc)313 void TOCModel::fill(const Okular::DocumentSynopsis *toc)
314 {
315     if (!toc)
316         return;
317 
318     clear();
319     emit layoutAboutToBeChanged();
320     d->addChildren(*toc, d->root);
321     d->dirty = true;
322     emit layoutChanged();
323     if (equals(d->m_oldModel)) {
324         for (const QModelIndex &oldIndex : qAsConst(d->m_oldTocExpandedIndexes)) {
325             const QModelIndex index = indexForIndex(oldIndex, this);
326             if (!index.isValid())
327                 continue;
328 
329             // TODO misusing parent() here, fix
330             QMetaObject::invokeMethod(QObject::parent(), "expand", Qt::QueuedConnection, Q_ARG(QModelIndex, index));
331         }
332     } else {
333         for (TOCItem *item : qAsConst(d->itemsToOpen)) {
334             const QModelIndex index = d->indexForItem(item);
335             if (!index.isValid())
336                 continue;
337 
338             // TODO misusing parent() here, fix
339             QMetaObject::invokeMethod(QObject::parent(), "expand", Qt::QueuedConnection, Q_ARG(QModelIndex, index));
340         }
341     }
342     d->itemsToOpen.clear();
343     delete d->m_oldModel;
344     d->m_oldModel = nullptr;
345     d->m_oldTocExpandedIndexes.clear();
346 }
347 
clear()348 void TOCModel::clear()
349 {
350     if (!d->dirty)
351         return;
352 
353     beginResetModel();
354     qDeleteAll(d->root->children);
355     d->root->children.clear();
356     d->currentPage.clear();
357     endResetModel();
358     d->dirty = false;
359 }
360 
setCurrentViewport(const Okular::DocumentViewport & viewport)361 void TOCModel::setCurrentViewport(const Okular::DocumentViewport &viewport)
362 {
363     for (TOCItem *item : qAsConst(d->currentPage)) {
364         QModelIndex index = d->indexForItem(item);
365         if (!index.isValid())
366             continue;
367 
368         item->highlight = false;
369         emit dataChanged(index, index);
370     }
371     d->currentPage.clear();
372 
373     QList<TOCItem *> newCurrentPage;
374     d->findViewport(viewport, d->root, newCurrentPage);
375 
376     d->currentPage = newCurrentPage;
377 
378     for (TOCItem *item : qAsConst(d->currentPage)) {
379         QModelIndex index = d->indexForItem(item);
380         if (!index.isValid())
381             continue;
382 
383         item->highlight = true;
384         emit dataChanged(index, index);
385     }
386 }
387 
isEmpty() const388 bool TOCModel::isEmpty() const
389 {
390     return d->root->children.isEmpty();
391 }
392 
equals(const TOCModel * model) const393 bool TOCModel::equals(const TOCModel *model) const
394 {
395     if (model)
396         return checkequality(model);
397     else
398         return false;
399 }
400 
setOldModelData(TOCModel * model,const QVector<QModelIndex> & list)401 void TOCModel::setOldModelData(TOCModel *model, const QVector<QModelIndex> &list)
402 {
403     delete d->m_oldModel;
404     d->m_oldModel = model;
405     d->m_oldTocExpandedIndexes = list;
406 }
407 
hasOldModelData() const408 bool TOCModel::hasOldModelData() const
409 {
410     return (d->m_oldModel != nullptr);
411 }
412 
clearOldModelData() const413 TOCModel *TOCModel::clearOldModelData() const
414 {
415     TOCModel *oldModel = d->m_oldModel;
416     d->m_oldModel = nullptr;
417     d->m_oldTocExpandedIndexes.clear();
418     return oldModel;
419 }
420 
externalFileNameForIndex(const QModelIndex & index) const421 QString TOCModel::externalFileNameForIndex(const QModelIndex &index) const
422 {
423     if (!index.isValid())
424         return QString();
425 
426     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
427     return item->extFileName;
428 }
429 
viewportForIndex(const QModelIndex & index) const430 Okular::DocumentViewport TOCModel::viewportForIndex(const QModelIndex &index) const
431 {
432     if (!index.isValid())
433         return Okular::DocumentViewport();
434 
435     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
436     return item->viewport;
437 }
438 
urlForIndex(const QModelIndex & index) const439 QString TOCModel::urlForIndex(const QModelIndex &index) const
440 {
441     if (!index.isValid())
442         return QString();
443 
444     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
445     return item->url;
446 }
447 
checkequality(const TOCModel * model,const QModelIndex & parentA,const QModelIndex & parentB) const448 bool TOCModel::checkequality(const TOCModel *model, const QModelIndex &parentA, const QModelIndex &parentB) const
449 {
450     if (rowCount(parentA) != model->rowCount(parentB))
451         return false;
452     for (int i = 0; i < rowCount(parentA); i++) {
453         QModelIndex indxA = index(i, 0, parentA);
454         QModelIndex indxB = model->index(i, 0, parentB);
455         if (indxA.data() != indxB.data()) {
456             return false;
457         }
458         if (hasChildren(indxA) != model->hasChildren(indxB)) {
459             return false;
460         }
461         if (!checkequality(model, indxA, indxB)) {
462             return false;
463         }
464     }
465     return true;
466 }
467 #include "moc_tocmodel.cpp"
468