1 /*
2     SPDX-FileCopyrightText: 2006 Pino Toscano <pino@kde.org>
3 
4     Work sponsored by the LiMux project of the city of Munich:
5     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "annotationmodel.h"
11 
12 #include <QLinkedList>
13 #include <QList>
14 #include <QPointer>
15 
16 #include <KLocalizedString>
17 #include <QIcon>
18 
19 #include "core/annotations.h"
20 #include "core/document.h"
21 #include "core/observer.h"
22 #include "core/page.h"
23 #include "guiutils.h"
24 
25 struct AnnItem {
26     AnnItem();
27     AnnItem(AnnItem *parent, Okular::Annotation *ann);
28     AnnItem(AnnItem *parent, int page);
29     ~AnnItem();
30 
31     AnnItem(const AnnItem &) = delete;
32     AnnItem &operator=(const AnnItem &) = delete;
33 
34     AnnItem *parent;
35     QList<AnnItem *> children;
36 
37     Okular::Annotation *annotation;
38     int page;
39 };
40 
filterOutWidgetAnnotations(const QLinkedList<Okular::Annotation * > & annotations)41 static QLinkedList<Okular::Annotation *> filterOutWidgetAnnotations(const QLinkedList<Okular::Annotation *> &annotations)
42 {
43     QLinkedList<Okular::Annotation *> result;
44 
45     for (Okular::Annotation *annotation : annotations) {
46         if (annotation->subType() == Okular::Annotation::AWidget)
47             continue;
48 
49         result.append(annotation);
50     }
51 
52     return result;
53 }
54 
55 class AnnotationModelPrivate : public Okular::DocumentObserver
56 {
57 public:
58     explicit AnnotationModelPrivate(AnnotationModel *qq);
59     ~AnnotationModelPrivate() override;
60 
61     void notifySetup(const QVector<Okular::Page *> &pages, int setupFlags) override;
62     void notifyPageChanged(int page, int flags) override;
63 
64     QModelIndex indexForItem(AnnItem *item) const;
65     void rebuildTree(const QVector<Okular::Page *> &pages);
66     AnnItem *findItem(int page, int *index) const;
67 
68     AnnotationModel *q;
69     AnnItem *root;
70     QPointer<Okular::Document> document;
71 };
72 
AnnItem()73 AnnItem::AnnItem()
74     : parent(nullptr)
75     , annotation(nullptr)
76     , page(-1)
77 {
78 }
79 
AnnItem(AnnItem * _parent,Okular::Annotation * ann)80 AnnItem::AnnItem(AnnItem *_parent, Okular::Annotation *ann)
81     : parent(_parent)
82     , annotation(ann)
83     , page(_parent->page)
84 {
85     Q_ASSERT(!parent->annotation);
86     parent->children.append(this);
87 }
88 
AnnItem(AnnItem * _parent,int _page)89 AnnItem::AnnItem(AnnItem *_parent, int _page)
90     : parent(_parent)
91     , annotation(nullptr)
92     , page(_page)
93 {
94     Q_ASSERT(!parent->parent);
95     parent->children.append(this);
96 }
97 
~AnnItem()98 AnnItem::~AnnItem()
99 {
100     qDeleteAll(children);
101 }
102 
AnnotationModelPrivate(AnnotationModel * qq)103 AnnotationModelPrivate::AnnotationModelPrivate(AnnotationModel *qq)
104     : q(qq)
105     , root(new AnnItem)
106 {
107 }
108 
~AnnotationModelPrivate()109 AnnotationModelPrivate::~AnnotationModelPrivate()
110 {
111     delete root;
112 }
113 
updateAnnotationPointer(AnnItem * item,const QVector<Okular::Page * > & pages)114 static void updateAnnotationPointer(AnnItem *item, const QVector<Okular::Page *> &pages)
115 {
116     if (item->annotation) {
117         item->annotation = pages[item->page]->annotation(item->annotation->uniqueName());
118         if (!item->annotation)
119             qWarning() << "Lost annotation on document save, something went wrong";
120     }
121 
122     for (AnnItem *child : qAsConst(item->children)) {
123         updateAnnotationPointer(child, pages);
124     }
125 }
126 
notifySetup(const QVector<Okular::Page * > & pages,int setupFlags)127 void AnnotationModelPrivate::notifySetup(const QVector<Okular::Page *> &pages, int setupFlags)
128 {
129     if (!(setupFlags & Okular::DocumentObserver::DocumentChanged)) {
130         if (setupFlags & Okular::DocumentObserver::UrlChanged) {
131             // Here with UrlChanged and no document changed it means we
132             // need to update all the Annotation* otherwise
133             // they still point to the old document ones, luckily the old ones are still
134             // around so we can look for the new ones using unique ids, etc
135             updateAnnotationPointer(root, pages);
136         }
137         return;
138     }
139 
140     q->beginResetModel();
141     qDeleteAll(root->children);
142     root->children.clear();
143 
144     rebuildTree(pages);
145     q->endResetModel();
146 }
147 
notifyPageChanged(int page,int flags)148 void AnnotationModelPrivate::notifyPageChanged(int page, int flags)
149 {
150     // we are strictly interested in annotations
151     if (!(flags & Okular::DocumentObserver::Annotations))
152         return;
153 
154     const QLinkedList<Okular::Annotation *> annots = filterOutWidgetAnnotations(document->page(page)->annotations());
155     int annItemIndex = -1;
156     AnnItem *annItem = findItem(page, &annItemIndex);
157     // case 1: the page has no more annotations
158     //         => remove the branch, if any
159     if (annots.isEmpty()) {
160         if (annItem) {
161             q->beginRemoveRows(indexForItem(root), annItemIndex, annItemIndex);
162             delete root->children.at(annItemIndex);
163             root->children.removeAt(annItemIndex);
164             q->endRemoveRows();
165         }
166         return;
167     }
168     // case 2: no existing branch
169     //         => add a new branch, and add the annotations for the page
170     if (!annItem) {
171         int i = 0;
172         while (i < root->children.count() && root->children.at(i)->page < page)
173             ++i;
174 
175         AnnItem *annItem = new AnnItem();
176         annItem->page = page;
177         annItem->parent = root;
178         q->beginInsertRows(indexForItem(root), i, i);
179         annItem->parent->children.insert(i, annItem);
180         q->endInsertRows();
181         QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
182         int newid = 0;
183         for (; it != itEnd; ++it, ++newid) {
184             q->beginInsertRows(indexForItem(annItem), newid, newid);
185             new AnnItem(annItem, *it);
186             q->endInsertRows();
187         }
188         return;
189     }
190     // case 3: existing branch, less annotations than items
191     //         => lookup and remove the annotations
192     if (annItem->children.count() > annots.count()) {
193         for (int i = annItem->children.count(); i > 0; --i) {
194             Okular::Annotation *ref = annItem->children.at(i - 1)->annotation;
195             bool found = false;
196             QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
197             for (; !found && it != itEnd; ++it) {
198                 if ((*it) == ref)
199                     found = true;
200             }
201             if (!found) {
202                 q->beginRemoveRows(indexForItem(annItem), i - 1, i - 1);
203                 delete annItem->children.at(i - 1);
204                 annItem->children.removeAt(i - 1);
205                 q->endRemoveRows();
206             }
207         }
208         return;
209     }
210     // case 4: existing branch, less items than annotations
211     //         => lookup and add annotations if not in the branch
212     if (annots.count() > annItem->children.count()) {
213         QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
214         for (; it != itEnd; ++it) {
215             Okular::Annotation *ref = *it;
216             bool found = false;
217             int count = annItem->children.count();
218             for (int i = 0; !found && i < count; ++i) {
219                 if (ref == annItem->children.at(i)->annotation)
220                     found = true;
221             }
222             if (!found) {
223                 q->beginInsertRows(indexForItem(annItem), count, count);
224                 new AnnItem(annItem, ref);
225                 q->endInsertRows();
226             }
227         }
228         return;
229     }
230     // case 5: the data of some annotation changed
231     // TODO: what do we do in this case?
232     // FIXME: for now, update ALL the annotations for that page
233     for (int i = 0; i < annItem->children.count(); ++i) {
234         QModelIndex index = indexForItem(annItem->children.at(i));
235         emit q->dataChanged(index, index);
236     }
237 }
238 
indexForItem(AnnItem * item) const239 QModelIndex AnnotationModelPrivate::indexForItem(AnnItem *item) const
240 {
241     if (item->parent) {
242         int id = item->parent->children.indexOf(item);
243         if (id >= 0 && id < item->parent->children.count())
244             return q->createIndex(id, 0, item);
245     }
246     return QModelIndex();
247 }
248 
rebuildTree(const QVector<Okular::Page * > & pages)249 void AnnotationModelPrivate::rebuildTree(const QVector<Okular::Page *> &pages)
250 {
251     if (pages.isEmpty())
252         return;
253 
254     emit q->layoutAboutToBeChanged();
255     for (int i = 0; i < pages.count(); ++i) {
256         const QLinkedList<Okular::Annotation *> annots = filterOutWidgetAnnotations(pages.at(i)->annotations());
257         if (annots.isEmpty())
258             continue;
259 
260         AnnItem *annItem = new AnnItem(root, i);
261         QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
262         for (; it != itEnd; ++it) {
263             new AnnItem(annItem, *it);
264         }
265     }
266     emit q->layoutChanged();
267 }
268 
findItem(int page,int * index) const269 AnnItem *AnnotationModelPrivate::findItem(int page, int *index) const
270 {
271     for (int i = 0; i < root->children.count(); ++i) {
272         AnnItem *tmp = root->children.at(i);
273         if (tmp->page == page) {
274             if (index)
275                 *index = i;
276             return tmp;
277         }
278     }
279     if (index)
280         *index = -1;
281     return nullptr;
282 }
283 
AnnotationModel(Okular::Document * document,QObject * parent)284 AnnotationModel::AnnotationModel(Okular::Document *document, QObject *parent)
285     : QAbstractItemModel(parent)
286     , d(new AnnotationModelPrivate(this))
287 {
288     d->document = document;
289 
290     d->document->addObserver(d);
291 }
292 
~AnnotationModel()293 AnnotationModel::~AnnotationModel()
294 {
295     if (d->document)
296         d->document->removeObserver(d);
297 
298     delete d;
299 }
300 
columnCount(const QModelIndex & parent) const301 int AnnotationModel::columnCount(const QModelIndex &parent) const
302 {
303     Q_UNUSED(parent)
304     return 1;
305 }
306 
data(const QModelIndex & index,int role) const307 QVariant AnnotationModel::data(const QModelIndex &index, int role) const
308 {
309     if (!index.isValid())
310         return QVariant();
311 
312     AnnItem *item = static_cast<AnnItem *>(index.internalPointer());
313     if (!item->annotation) {
314         if (role == Qt::DisplayRole)
315             return i18n("Page %1", item->page + 1);
316         else if (role == Qt::DecorationRole)
317             return QIcon::fromTheme(QStringLiteral("text-plain"));
318         else if (role == PageRole)
319             return item->page;
320 
321         return QVariant();
322     }
323     switch (role) {
324     case Qt::DisplayRole:
325         return GuiUtils::captionForAnnotation(item->annotation);
326         break;
327     case Qt::DecorationRole:
328         return QIcon::fromTheme(QStringLiteral("okular"));
329         break;
330     case Qt::ToolTipRole:
331         return GuiUtils::prettyToolTip(item->annotation);
332         break;
333     case AuthorRole:
334         return item->annotation->author();
335         break;
336     case PageRole:
337         return item->page;
338         break;
339     }
340     return QVariant();
341 }
342 
hasChildren(const QModelIndex & parent) const343 bool AnnotationModel::hasChildren(const QModelIndex &parent) const
344 {
345     if (!parent.isValid())
346         return true;
347 
348     AnnItem *item = static_cast<AnnItem *>(parent.internalPointer());
349     return !item->children.isEmpty();
350 }
351 
headerData(int section,Qt::Orientation orientation,int role) const352 QVariant AnnotationModel::headerData(int section, Qt::Orientation orientation, int role) const
353 {
354     if (orientation != Qt::Horizontal)
355         return QVariant();
356 
357     if (section == 0 && role == Qt::DisplayRole)
358         return QString::fromLocal8Bit("Annotations");
359 
360     return QVariant();
361 }
362 
index(int row,int column,const QModelIndex & parent) const363 QModelIndex AnnotationModel::index(int row, int column, const QModelIndex &parent) const
364 {
365     if (row < 0 || column != 0)
366         return QModelIndex();
367 
368     AnnItem *item = parent.isValid() ? static_cast<AnnItem *>(parent.internalPointer()) : d->root;
369     if (row < item->children.count())
370         return createIndex(row, column, item->children.at(row));
371 
372     return QModelIndex();
373 }
374 
parent(const QModelIndex & index) const375 QModelIndex AnnotationModel::parent(const QModelIndex &index) const
376 {
377     if (!index.isValid())
378         return QModelIndex();
379 
380     AnnItem *item = static_cast<AnnItem *>(index.internalPointer());
381     return d->indexForItem(item->parent);
382 }
383 
rowCount(const QModelIndex & parent) const384 int AnnotationModel::rowCount(const QModelIndex &parent) const
385 {
386     AnnItem *item = parent.isValid() ? static_cast<AnnItem *>(parent.internalPointer()) : d->root;
387     return item->children.count();
388 }
389 
isAnnotation(const QModelIndex & index) const390 bool AnnotationModel::isAnnotation(const QModelIndex &index) const
391 {
392     return annotationForIndex(index);
393 }
394 
annotationForIndex(const QModelIndex & index) const395 Okular::Annotation *AnnotationModel::annotationForIndex(const QModelIndex &index) const
396 {
397     if (!index.isValid())
398         return nullptr;
399 
400     AnnItem *item = static_cast<AnnItem *>(index.internalPointer());
401     return item->annotation;
402 }
403 
404 #include "moc_annotationmodel.cpp"
405