1 /*
2     SPDX-FileCopyrightText: 2006 Pino Toscano <pino@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "bookmarklist.h"
8 
9 // qt/kde includes
10 #include <QAction>
11 #include <QCursor>
12 #include <QDebug>
13 #include <QHeaderView>
14 #include <QIcon>
15 #include <QLayout>
16 #include <QMenu>
17 #include <QToolBar>
18 #include <QTreeWidget>
19 
20 #include <KLocalizedString>
21 #include <KTitleWidget>
22 #include <KTreeWidgetSearchLine>
23 
24 #include <kwidgetsaddons_version.h>
25 
26 #include "core/action.h"
27 #include "core/bookmarkmanager.h"
28 #include "core/document.h"
29 #include "pageitemdelegate.h"
30 
31 static const int BookmarkItemType = QTreeWidgetItem::UserType + 1;
32 static const int FileItemType = QTreeWidgetItem::UserType + 2;
33 static const int UrlRole = Qt::UserRole + 1;
34 
35 class BookmarkItem : public QTreeWidgetItem
36 {
37 public:
BookmarkItem(const KBookmark & bm)38     explicit BookmarkItem(const KBookmark &bm)
39         : QTreeWidgetItem(BookmarkItemType)
40         , m_bookmark(bm)
41     {
42         setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
43         m_url = m_bookmark.url();
44         m_viewport = Okular::DocumentViewport(m_url.fragment(QUrl::FullyDecoded));
45         m_url.setFragment(QString());
46         setText(0, m_bookmark.fullText());
47         if (m_viewport.isValid())
48             setData(0, PageItemDelegate::PageRole, QString::number(m_viewport.pageNumber + 1));
49     }
50 
51     BookmarkItem(const BookmarkItem &) = delete;
52     BookmarkItem &operator=(const BookmarkItem &) = delete;
53 
data(int column,int role) const54     QVariant data(int column, int role) const override
55     {
56         switch (role) {
57         case Qt::ToolTipRole:
58             return m_bookmark.fullText();
59         }
60         return QTreeWidgetItem::data(column, role);
61     }
62 
operator <(const QTreeWidgetItem & other) const63     bool operator<(const QTreeWidgetItem &other) const override
64     {
65         if (other.type() == BookmarkItemType) {
66             const BookmarkItem *cmp = static_cast<const BookmarkItem *>(&other);
67             return m_viewport < cmp->m_viewport;
68         }
69         return QTreeWidgetItem::operator<(other);
70     }
71 
bookmark()72     KBookmark &bookmark()
73     {
74         return m_bookmark;
75     }
76 
viewport() const77     const Okular::DocumentViewport &viewport() const
78     {
79         return m_viewport;
80     }
81 
url() const82     QUrl url() const
83     {
84         return m_url;
85     }
86 
87 private:
88     KBookmark m_bookmark;
89     QUrl m_url;
90     Okular::DocumentViewport m_viewport;
91 };
92 
93 class FileItem : public QTreeWidgetItem
94 {
95 public:
FileItem(const QUrl & url,QTreeWidget * tree,Okular::Document * document)96     FileItem(const QUrl &url, QTreeWidget *tree, Okular::Document *document)
97         : QTreeWidgetItem(tree, FileItemType)
98     {
99         setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
100         const QString fileString = document->bookmarkManager()->titleForUrl(url);
101         setText(0, fileString);
102         setData(0, UrlRole, QVariant::fromValue(url));
103     }
104 
105     FileItem(const FileItem &) = delete;
106     FileItem &operator=(const FileItem &) = delete;
107 
data(int column,int role) const108     QVariant data(int column, int role) const override
109     {
110         switch (role) {
111         case Qt::ToolTipRole:
112             return i18ncp("%1 is the file name", "%1\n\nOne bookmark", "%1\n\n%2 bookmarks", text(0), childCount());
113         }
114         return QTreeWidgetItem::data(column, role);
115     }
116 };
117 
BookmarkList(Okular::Document * document,QWidget * parent)118 BookmarkList::BookmarkList(Okular::Document *document, QWidget *parent)
119     : QWidget(parent)
120     , m_document(document)
121     , m_currentDocumentItem(nullptr)
122 {
123     QVBoxLayout *mainlay = new QVBoxLayout(this);
124     mainlay->setSpacing(6);
125 
126     KTitleWidget *titleWidget = new KTitleWidget(this);
127     titleWidget->setLevel(2);
128     titleWidget->setText(i18n("Bookmarks"));
129     mainlay->addWidget(titleWidget);
130     mainlay->setAlignment(titleWidget, Qt::AlignHCenter);
131     m_searchLine = new KTreeWidgetSearchLine(this);
132     mainlay->addWidget(m_searchLine);
133     m_searchLine->setPlaceholderText(i18n("Search..."));
134 
135     m_tree = new QTreeWidget(this);
136     mainlay->addWidget(m_tree);
137     QStringList cols;
138     cols.append(QStringLiteral("Bookmarks"));
139     m_tree->setContextMenuPolicy(Qt::CustomContextMenu);
140     m_tree->setHeaderLabels(cols);
141     m_tree->setSortingEnabled(false);
142     m_tree->setRootIsDecorated(true);
143     m_tree->setAlternatingRowColors(true);
144     m_tree->setItemDelegate(new PageItemDelegate(m_tree));
145     m_tree->header()->hide();
146     m_tree->setSelectionBehavior(QAbstractItemView::SelectRows);
147     m_tree->setEditTriggers(QAbstractItemView::EditKeyPressed);
148     connect(m_tree, &QTreeWidget::itemActivated, this, &BookmarkList::slotExecuted);
149     connect(m_tree, &QTreeWidget::customContextMenuRequested, this, &BookmarkList::slotContextMenu);
150     m_searchLine->addTreeWidget(m_tree);
151 
152     QToolBar *bookmarkController = new QToolBar(this);
153     mainlay->addWidget(bookmarkController);
154     bookmarkController->setObjectName(QStringLiteral("BookmarkControlBar"));
155     // change toolbar appearance
156     bookmarkController->setIconSize(QSize(16, 16));
157     bookmarkController->setMovable(false);
158     QSizePolicy sp = bookmarkController->sizePolicy();
159     sp.setVerticalPolicy(QSizePolicy::Minimum);
160     bookmarkController->setSizePolicy(sp);
161     // insert a togglebutton [show only bookmarks in the current document]
162     m_showBoomarkOnlyAction = bookmarkController->addAction(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Current document only"));
163     m_showBoomarkOnlyAction->setCheckable(true);
164     connect(m_showBoomarkOnlyAction, &QAction::toggled, this, &BookmarkList::slotFilterBookmarks);
165 
166     connect(m_document->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &BookmarkList::slotBookmarksChanged);
167 
168     rebuildTree(m_showBoomarkOnlyAction->isChecked());
169 }
170 
~BookmarkList()171 BookmarkList::~BookmarkList()
172 {
173     m_document->removeObserver(this);
174 }
175 
notifySetup(const QVector<Okular::Page * > & pages,int setupFlags)176 void BookmarkList::notifySetup(const QVector<Okular::Page *> &pages, int setupFlags)
177 {
178     Q_UNUSED(pages);
179     if (!(setupFlags & Okular::DocumentObserver::UrlChanged))
180         return;
181 
182     // clear contents
183     m_searchLine->clear();
184 
185     if (m_showBoomarkOnlyAction->isChecked()) {
186         rebuildTree(m_showBoomarkOnlyAction->isChecked());
187     } else {
188         disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
189         if (m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem()) {
190             m_currentDocumentItem->setIcon(0, QIcon());
191         }
192         m_currentDocumentItem = itemForUrl(m_document->currentDocument());
193         if (m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem()) {
194             m_currentDocumentItem->setIcon(0, QIcon::fromTheme(QStringLiteral("bookmarks")));
195             m_currentDocumentItem->setExpanded(true);
196         }
197         connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
198     }
199 }
200 
slotFilterBookmarks(bool on)201 void BookmarkList::slotFilterBookmarks(bool on)
202 {
203     rebuildTree(on);
204 }
205 
slotExecuted(QTreeWidgetItem * item)206 void BookmarkList::slotExecuted(QTreeWidgetItem *item)
207 {
208     BookmarkItem *bmItem = dynamic_cast<BookmarkItem *>(item);
209     if (!bmItem || !bmItem->viewport().isValid())
210         return;
211 
212     goTo(bmItem);
213 }
214 
slotChanged(QTreeWidgetItem * item)215 void BookmarkList::slotChanged(QTreeWidgetItem *item)
216 {
217     BookmarkItem *bmItem = dynamic_cast<BookmarkItem *>(item);
218     if (bmItem && bmItem->viewport().isValid()) {
219         bmItem->bookmark().setFullText(bmItem->text(0));
220         m_document->bookmarkManager()->save();
221     }
222 
223     FileItem *fItem = dynamic_cast<FileItem *>(item);
224     if (fItem) {
225         const QUrl url = fItem->data(0, UrlRole).value<QUrl>();
226         m_document->bookmarkManager()->renameBookmark(url, fItem->text(0));
227         m_document->bookmarkManager()->save();
228     }
229 }
230 
slotContextMenu(const QPoint p)231 void BookmarkList::slotContextMenu(const QPoint p)
232 {
233     QTreeWidgetItem *item = m_tree->itemAt(p);
234     BookmarkItem *bmItem = item ? dynamic_cast<BookmarkItem *>(item) : nullptr;
235     if (bmItem)
236         contextMenuForBookmarkItem(p, bmItem);
237     else if (FileItem *fItem = dynamic_cast<FileItem *>(item))
238         contextMenuForFileItem(p, fItem);
239 }
240 
contextMenuForBookmarkItem(const QPoint p,BookmarkItem * bmItem)241 void BookmarkList::contextMenuForBookmarkItem(const QPoint p, BookmarkItem *bmItem)
242 {
243     Q_UNUSED(p);
244     if (!bmItem || !bmItem->viewport().isValid())
245         return;
246 
247     QMenu menu(this);
248     QAction *gotobm = menu.addAction(i18n("Go to This Bookmark"));
249     QAction *editbm = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Bookmark"));
250     QAction *removebm = menu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove Bookmark"));
251     QAction *res = menu.exec(QCursor::pos());
252     if (!res)
253         return;
254 
255     if (res == gotobm)
256         goTo(bmItem);
257     else if (res == editbm)
258         m_tree->editItem(bmItem, 0);
259     else if (res == removebm)
260         m_document->bookmarkManager()->removeBookmark(bmItem->url(), bmItem->bookmark());
261 }
262 
contextMenuForFileItem(const QPoint p,FileItem * fItem)263 void BookmarkList::contextMenuForFileItem(const QPoint p, FileItem *fItem)
264 {
265     Q_UNUSED(p);
266     if (!fItem)
267         return;
268 
269     const QUrl itemurl = fItem->data(0, UrlRole).value<QUrl>();
270     const bool thisdoc = itemurl == m_document->currentDocument();
271 
272     QMenu menu(this);
273     QAction *open = nullptr;
274     if (!thisdoc)
275         open = menu.addAction(i18nc("Opens the selected document", "Open Document"));
276     QAction *editbm = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Bookmark"));
277     QAction *removebm = menu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove Bookmarks"));
278     QAction *res = menu.exec(QCursor::pos());
279     if (!res)
280         return;
281 
282     if (res == open) {
283         Okular::GotoAction action(itemurl.toDisplayString(QUrl::PreferLocalFile), Okular::DocumentViewport());
284         m_document->processAction(&action);
285     } else if (res == editbm)
286         m_tree->editItem(fItem, 0);
287     else if (res == removebm) {
288         KBookmark::List list;
289         for (int i = 0; i < fItem->childCount(); ++i) {
290             list.append(static_cast<BookmarkItem *>(fItem->child(i))->bookmark());
291         }
292         m_document->bookmarkManager()->removeBookmarks(itemurl, list);
293     }
294 }
295 
slotBookmarksChanged(const QUrl & url)296 void BookmarkList::slotBookmarksChanged(const QUrl &url)
297 {
298     // special case here, as m_currentDocumentItem could represent
299     // the invisible root item
300     if (url == m_document->currentDocument()) {
301         selectiveUrlUpdate(m_document->currentDocument(), m_currentDocumentItem);
302         return;
303     }
304 
305     // we are showing the bookmarks for the current document only
306     if (m_showBoomarkOnlyAction->isChecked())
307         return;
308 
309     QTreeWidgetItem *item = itemForUrl(url);
310     selectiveUrlUpdate(url, item);
311 }
312 
createItems(const QUrl & baseurl,const KBookmark::List & bmlist)313 QList<QTreeWidgetItem *> createItems(const QUrl &baseurl, const KBookmark::List &bmlist)
314 {
315     Q_UNUSED(baseurl)
316     QList<QTreeWidgetItem *> ret;
317     for (const KBookmark &bm : bmlist) {
318         //        qCDebug(OkularUiDebug).nospace() << "checking '" << tmp << "'";
319         //        qCDebug(OkularUiDebug).nospace() << "      vs '" << baseurl << "'";
320         // TODO check that bm and baseurl are the same (#ref excluded)
321         QTreeWidgetItem *item = new BookmarkItem(bm);
322         ret.append(item);
323     }
324     return ret;
325 }
326 
rebuildTree(bool filter)327 void BookmarkList::rebuildTree(bool filter)
328 {
329     // disconnect and reconnect later, otherwise we'll get many itemChanged()
330     // signals for all the current items
331     disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
332 
333     m_currentDocumentItem = nullptr;
334     m_tree->clear();
335 
336     const QList<QUrl> urls = m_document->bookmarkManager()->files();
337     if (filter) {
338         if (m_document->isOpened()) {
339             for (const QUrl &url : urls) {
340                 if (url == m_document->currentDocument()) {
341                     m_tree->addTopLevelItems(createItems(url, m_document->bookmarkManager()->bookmarks(url)));
342                     m_currentDocumentItem = m_tree->invisibleRootItem();
343                     break;
344                 }
345             }
346         }
347     } else {
348         QTreeWidgetItem *currenturlitem = nullptr;
349         for (const QUrl &url : urls) {
350             QList<QTreeWidgetItem *> subitems = createItems(url, m_document->bookmarkManager()->bookmarks(url));
351             if (!subitems.isEmpty()) {
352                 FileItem *item = new FileItem(url, m_tree, m_document);
353                 item->addChildren(subitems);
354                 if (!currenturlitem && url == m_document->currentDocument()) {
355                     currenturlitem = item;
356                 }
357             }
358         }
359         if (currenturlitem) {
360             currenturlitem->setExpanded(true);
361             currenturlitem->setIcon(0, QIcon::fromTheme(QStringLiteral("bookmarks")));
362             m_tree->scrollToItem(currenturlitem, QAbstractItemView::PositionAtTop);
363             m_currentDocumentItem = currenturlitem;
364         }
365     }
366 
367     m_tree->sortItems(0, Qt::AscendingOrder);
368 
369     connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
370 }
371 
goTo(BookmarkItem * item)372 void BookmarkList::goTo(BookmarkItem *item)
373 {
374     if (item->url() == m_document->currentDocument()) {
375         m_document->setViewport(item->viewport(), nullptr, true);
376     } else {
377         Okular::GotoAction action(item->url().toDisplayString(QUrl::PreferLocalFile), item->viewport());
378         m_document->processAction(&action);
379     }
380 }
381 
selectiveUrlUpdate(const QUrl & url,QTreeWidgetItem * & item)382 void BookmarkList::selectiveUrlUpdate(const QUrl &url, QTreeWidgetItem *&item)
383 {
384     disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
385 
386     const KBookmark::List urlbookmarks = m_document->bookmarkManager()->bookmarks(url);
387     if (urlbookmarks.isEmpty()) {
388         if (item != m_tree->invisibleRootItem()) {
389             m_tree->invisibleRootItem()->removeChild(item);
390             item = nullptr;
391         } else if (item) {
392             for (int i = item->childCount(); i >= 0; --i) {
393                 item->removeChild(item->child(i));
394             }
395         }
396     } else {
397         bool fileitem_created = false;
398 
399         if (item) {
400             for (int i = item->childCount() - 1; i >= 0; --i) {
401                 item->removeChild(item->child(i));
402             }
403         } else {
404             item = new FileItem(url, m_tree, m_document);
405             fileitem_created = true;
406         }
407         if (m_document->isOpened() && url == m_document->currentDocument()) {
408             item->setIcon(0, QIcon::fromTheme(QStringLiteral("bookmarks")));
409             item->setExpanded(true);
410         }
411         item->addChildren(createItems(url, urlbookmarks));
412 
413         if (fileitem_created) {
414             // we need to sort also the parent of the new file item,
415             // so it can be properly shown in the correct place
416             m_tree->invisibleRootItem()->sortChildren(0, Qt::AscendingOrder);
417         }
418         item->sortChildren(0, Qt::AscendingOrder);
419     }
420 
421     connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
422 }
423 
itemForUrl(const QUrl & url) const424 QTreeWidgetItem *BookmarkList::itemForUrl(const QUrl &url) const
425 {
426     const int count = m_tree->topLevelItemCount();
427     for (int i = 0; i < count; ++i) {
428         QTreeWidgetItem *item = m_tree->topLevelItem(i);
429         const QUrl itemurl = item->data(0, UrlRole).value<QUrl>();
430         if (itemurl.isValid() && itemurl == url) {
431             return item;
432         }
433     }
434     return nullptr;
435 }
436 
437 #include "moc_bookmarklist.cpp"
438