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