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