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