File indexing completed on 2024-04-28 15:51:41

0001 /*
0002     SPDX-FileCopyrightText: 2006 Pino Toscano <pino@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "bookmarklist.h"
0008 
0009 // qt/kde includes
0010 #include <QAction>
0011 #include <QCheckBox>
0012 #include <QCursor>
0013 #include <QDebug>
0014 #include <QHeaderView>
0015 #include <QIcon>
0016 #include <QLayout>
0017 #include <QMenu>
0018 #include <QToolButton>
0019 #include <QTreeWidget>
0020 
0021 #include <KLocalizedString>
0022 #include <KTitleWidget>
0023 #include <KTreeWidgetSearchLine>
0024 
0025 #include <kwidgetsaddons_version.h>
0026 
0027 #include "core/action.h"
0028 #include "core/bookmarkmanager.h"
0029 #include "core/document.h"
0030 #include "gui/tocmodel.h"
0031 #include "pageitemdelegate.h"
0032 
0033 static const int BookmarkItemType = QTreeWidgetItem::UserType + 1;
0034 static const int FileItemType = QTreeWidgetItem::UserType + 2;
0035 static const int UrlRole = Qt::UserRole + 1;
0036 
0037 class BookmarkItem : public QTreeWidgetItem
0038 {
0039 public:
0040     explicit BookmarkItem(const KBookmark &bm)
0041         : QTreeWidgetItem(BookmarkItemType)
0042         , m_bookmark(bm)
0043     {
0044         setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
0045         m_url = m_bookmark.url();
0046         m_viewport = Okular::DocumentViewport(m_url.fragment(QUrl::FullyDecoded));
0047         m_url.setFragment(QString());
0048         setText(0, m_bookmark.fullText());
0049         if (m_viewport.isValid()) {
0050             setData(0, TOCModel::PageRole, QString::number(m_viewport.pageNumber + 1));
0051         }
0052     }
0053 
0054     BookmarkItem(const BookmarkItem &) = delete;
0055     BookmarkItem &operator=(const BookmarkItem &) = delete;
0056 
0057     QVariant data(int column, int role) const override
0058     {
0059         switch (role) {
0060         case Qt::ToolTipRole:
0061             return m_bookmark.fullText();
0062         }
0063         return QTreeWidgetItem::data(column, role);
0064     }
0065 
0066     bool operator<(const QTreeWidgetItem &other) const override
0067     {
0068         if (other.type() == BookmarkItemType) {
0069             const BookmarkItem *cmp = static_cast<const BookmarkItem *>(&other);
0070             return m_viewport < cmp->m_viewport;
0071         }
0072         return QTreeWidgetItem::operator<(other);
0073     }
0074 
0075     KBookmark &bookmark()
0076     {
0077         return m_bookmark;
0078     }
0079 
0080     const Okular::DocumentViewport &viewport() const
0081     {
0082         return m_viewport;
0083     }
0084 
0085     QUrl url() const
0086     {
0087         return m_url;
0088     }
0089 
0090 private:
0091     KBookmark m_bookmark;
0092     QUrl m_url;
0093     Okular::DocumentViewport m_viewport;
0094 };
0095 
0096 class FileItem : public QTreeWidgetItem
0097 {
0098 public:
0099     FileItem(const QUrl &url, QTreeWidget *tree, Okular::Document *document)
0100         : QTreeWidgetItem(tree, FileItemType)
0101     {
0102         setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
0103         const QString fileString = document->bookmarkManager()->titleForUrl(url);
0104         setText(0, fileString);
0105         setData(0, UrlRole, QVariant::fromValue(url));
0106     }
0107 
0108     FileItem(const FileItem &) = delete;
0109     FileItem &operator=(const FileItem &) = delete;
0110 
0111     QVariant data(int column, int role) const override
0112     {
0113         switch (role) {
0114         case Qt::ToolTipRole:
0115             return i18ncp("%1 is the file name", "%1\n\nOne bookmark", "%1\n\n%2 bookmarks", text(0), childCount());
0116         }
0117         return QTreeWidgetItem::data(column, role);
0118     }
0119 };
0120 
0121 BookmarkList::BookmarkList(Okular::Document *document, QWidget *parent)
0122     : QWidget(parent)
0123     , m_document(document)
0124     , m_currentDocumentItem(nullptr)
0125 {
0126     QVBoxLayout *mainlay = new QVBoxLayout(this);
0127     mainlay->setSpacing(6);
0128 
0129     KTitleWidget *titleWidget = new KTitleWidget(this);
0130     titleWidget->setLevel(4);
0131     titleWidget->setText(i18n("Bookmarks"));
0132     mainlay->addWidget(titleWidget);
0133     mainlay->setAlignment(titleWidget, Qt::AlignHCenter);
0134 
0135     m_showForAllDocumentsCheckbox = new QCheckBox(i18n("Show for all documents"), this);
0136     m_showForAllDocumentsCheckbox->setChecked(true); // this setting isn't saved
0137     connect(m_showForAllDocumentsCheckbox, &QCheckBox::toggled, this, &BookmarkList::slotShowAllBookmarks);
0138     mainlay->addWidget(m_showForAllDocumentsCheckbox);
0139 
0140     m_searchLine = new KTreeWidgetSearchLine(this);
0141     mainlay->addWidget(m_searchLine);
0142     m_searchLine->setPlaceholderText(i18n("Search..."));
0143 
0144     m_tree = new QTreeWidget(this);
0145     mainlay->addWidget(m_tree);
0146     QStringList cols;
0147     cols.append(QStringLiteral("Bookmarks"));
0148     m_tree->setContextMenuPolicy(Qt::CustomContextMenu);
0149     m_tree->setHeaderLabels(cols);
0150     m_tree->setSortingEnabled(false);
0151     m_tree->setRootIsDecorated(true);
0152     m_tree->setAlternatingRowColors(true);
0153     m_tree->setItemDelegate(new PageItemDelegate(m_tree));
0154     m_tree->header()->hide();
0155     m_tree->setSelectionBehavior(QAbstractItemView::SelectRows);
0156     m_tree->setEditTriggers(QAbstractItemView::EditKeyPressed);
0157     connect(m_tree, &QTreeWidget::itemActivated, this, &BookmarkList::slotExecuted);
0158     connect(m_tree, &QTreeWidget::customContextMenuRequested, this, &BookmarkList::slotContextMenu);
0159     m_searchLine->addTreeWidget(m_tree);
0160 
0161     connect(m_document->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &BookmarkList::slotBookmarksChanged);
0162 
0163     rebuildTree(m_showForAllDocumentsCheckbox->isChecked());
0164 
0165     m_showAllToolButton = new QToolButton(this);
0166     m_showAllToolButton->setAutoRaise(true);
0167     m_showAllToolButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0168     mainlay->addWidget(m_showAllToolButton);
0169 }
0170 
0171 BookmarkList::~BookmarkList()
0172 {
0173     m_document->removeObserver(this);
0174 }
0175 
0176 void BookmarkList::notifySetup(const QVector<Okular::Page *> &pages, int setupFlags)
0177 {
0178     Q_UNUSED(pages);
0179     if (!(setupFlags & Okular::DocumentObserver::UrlChanged)) {
0180         return;
0181     }
0182 
0183     // clear contents
0184     m_searchLine->clear();
0185 
0186     if (!m_showForAllDocumentsCheckbox->isChecked()) {
0187         rebuildTree(m_showForAllDocumentsCheckbox->isChecked());
0188     } else {
0189         disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
0190         if (m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem()) {
0191             m_currentDocumentItem->setIcon(0, QIcon());
0192         }
0193         m_currentDocumentItem = itemForUrl(m_document->currentDocument());
0194         if (m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem()) {
0195             m_currentDocumentItem->setIcon(0, QIcon::fromTheme(QStringLiteral("bookmarks")));
0196             m_currentDocumentItem->setExpanded(true);
0197         }
0198         connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
0199     }
0200 }
0201 
0202 void BookmarkList::setAddBookmarkAction(QAction *addBookmarkAction)
0203 {
0204     m_showAllToolButton->setDefaultAction(addBookmarkAction);
0205 }
0206 
0207 void BookmarkList::slotShowAllBookmarks(bool showAll)
0208 {
0209     rebuildTree(showAll);
0210 }
0211 
0212 void BookmarkList::slotExecuted(QTreeWidgetItem *item)
0213 {
0214     BookmarkItem *bmItem = dynamic_cast<BookmarkItem *>(item);
0215     if (!bmItem || !bmItem->viewport().isValid()) {
0216         return;
0217     }
0218 
0219     goTo(bmItem);
0220 }
0221 
0222 void BookmarkList::slotChanged(QTreeWidgetItem *item)
0223 {
0224     BookmarkItem *bmItem = dynamic_cast<BookmarkItem *>(item);
0225     if (bmItem && bmItem->viewport().isValid()) {
0226         bmItem->bookmark().setFullText(bmItem->text(0));
0227         m_document->bookmarkManager()->save();
0228     }
0229 
0230     FileItem *fItem = dynamic_cast<FileItem *>(item);
0231     if (fItem) {
0232         const QUrl url = fItem->data(0, UrlRole).value<QUrl>();
0233         m_document->bookmarkManager()->renameBookmark(url, fItem->text(0));
0234         m_document->bookmarkManager()->save();
0235     }
0236 }
0237 
0238 void BookmarkList::slotContextMenu(const QPoint p)
0239 {
0240     QTreeWidgetItem *item = m_tree->itemAt(p);
0241     BookmarkItem *bmItem = item ? dynamic_cast<BookmarkItem *>(item) : nullptr;
0242     if (bmItem) {
0243         contextMenuForBookmarkItem(p, bmItem);
0244     } else if (FileItem *fItem = dynamic_cast<FileItem *>(item)) {
0245         contextMenuForFileItem(p, fItem);
0246     }
0247 }
0248 
0249 void BookmarkList::contextMenuForBookmarkItem(const QPoint p, BookmarkItem *bmItem)
0250 {
0251     Q_UNUSED(p);
0252     if (!bmItem || !bmItem->viewport().isValid()) {
0253         return;
0254     }
0255 
0256     QMenu menu(this);
0257     const QAction *gotobm = menu.addAction(i18n("Go to This Bookmark"));
0258     const QAction *editbm = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Bookmark"));
0259     const QAction *removebm = menu.addAction(QIcon::fromTheme(QStringLiteral("bookmark-remove"), QIcon::fromTheme(QStringLiteral("edit-delete-bookmark"))), i18n("Remove Bookmark"));
0260     const QAction *res = menu.exec(QCursor::pos());
0261     if (!res) {
0262         return;
0263     }
0264 
0265     if (res == gotobm) {
0266         goTo(bmItem);
0267     } else if (res == editbm) {
0268         m_tree->editItem(bmItem, 0);
0269     } else if (res == removebm) {
0270         m_document->bookmarkManager()->removeBookmark(bmItem->url(), bmItem->bookmark());
0271     }
0272 }
0273 
0274 void BookmarkList::contextMenuForFileItem(const QPoint p, FileItem *fItem)
0275 {
0276     Q_UNUSED(p);
0277     if (!fItem) {
0278         return;
0279     }
0280 
0281     const QUrl itemurl = fItem->data(0, UrlRole).value<QUrl>();
0282     const bool thisdoc = itemurl == m_document->currentDocument();
0283 
0284     QMenu menu(this);
0285     QAction *open = nullptr;
0286     if (!thisdoc) {
0287         open = menu.addAction(i18nc("Opens the selected document", "Open Document"));
0288     }
0289     const QAction *editbm = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Bookmark"));
0290     const QAction *removebm = menu.addAction(QIcon::fromTheme(QStringLiteral("bookmark-remove"), QIcon::fromTheme(QStringLiteral("edit-delete-bookmark"))), i18n("Remove all Bookmarks for this Document"));
0291     const QAction *res = menu.exec(QCursor::pos());
0292     if (!res) {
0293         return;
0294     }
0295 
0296     if (res == open) {
0297         Okular::GotoAction action(itemurl.toDisplayString(QUrl::PreferLocalFile), Okular::DocumentViewport());
0298         m_document->processAction(&action);
0299     } else if (res == editbm) {
0300         m_tree->editItem(fItem, 0);
0301     } else if (res == removebm) {
0302         KBookmark::List list;
0303         for (int i = 0; i < fItem->childCount(); ++i) {
0304             list.append(static_cast<BookmarkItem *>(fItem->child(i))->bookmark());
0305         }
0306         m_document->bookmarkManager()->removeBookmarks(itemurl, list);
0307     }
0308 }
0309 
0310 void BookmarkList::slotBookmarksChanged(const QUrl &url)
0311 {
0312     // special case here, as m_currentDocumentItem could represent
0313     // the invisible root item
0314     if (url == m_document->currentDocument()) {
0315         selectiveUrlUpdate(m_document->currentDocument(), m_currentDocumentItem);
0316         return;
0317     }
0318 
0319     // we are showing the bookmarks for the current document only
0320     if (!m_showForAllDocumentsCheckbox->isChecked()) {
0321         return;
0322     }
0323 
0324     QTreeWidgetItem *item = itemForUrl(url);
0325     selectiveUrlUpdate(url, item);
0326 }
0327 
0328 QList<QTreeWidgetItem *> createItems(const QUrl &baseurl, const KBookmark::List &bmlist)
0329 {
0330     Q_UNUSED(baseurl)
0331     QList<QTreeWidgetItem *> ret;
0332     for (const KBookmark &bm : bmlist) {
0333         //        qCDebug(OkularUiDebug).nospace() << "checking '" << tmp << "'";
0334         //        qCDebug(OkularUiDebug).nospace() << "      vs '" << baseurl << "'";
0335         // TODO check that bm and baseurl are the same (#ref excluded)
0336         QTreeWidgetItem *item = new BookmarkItem(bm);
0337         ret.append(item);
0338     }
0339     return ret;
0340 }
0341 
0342 void BookmarkList::rebuildTree(bool showAll)
0343 {
0344     // disconnect and reconnect later, otherwise we'll get many itemChanged()
0345     // signals for all the current items
0346     disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
0347 
0348     m_currentDocumentItem = nullptr;
0349     m_tree->clear();
0350 
0351     const QList<QUrl> urls = m_document->bookmarkManager()->files();
0352     if (!showAll) {
0353         if (m_document->isOpened()) {
0354             for (const QUrl &url : urls) {
0355                 if (url == m_document->currentDocument()) {
0356                     m_tree->addTopLevelItems(createItems(url, m_document->bookmarkManager()->bookmarks(url)));
0357                     m_currentDocumentItem = m_tree->invisibleRootItem();
0358                     break;
0359                 }
0360             }
0361         }
0362     } else {
0363         QTreeWidgetItem *currenturlitem = nullptr;
0364         for (const QUrl &url : urls) {
0365             QList<QTreeWidgetItem *> subitems = createItems(url, m_document->bookmarkManager()->bookmarks(url));
0366             if (!subitems.isEmpty()) {
0367                 FileItem *item = new FileItem(url, m_tree, m_document);
0368                 item->addChildren(subitems);
0369                 if (!currenturlitem && url == m_document->currentDocument()) {
0370                     currenturlitem = item;
0371                 }
0372             }
0373         }
0374         if (currenturlitem) {
0375             currenturlitem->setExpanded(true);
0376             currenturlitem->setIcon(0, QIcon::fromTheme(QStringLiteral("bookmarks")));
0377             m_tree->scrollToItem(currenturlitem, QAbstractItemView::PositionAtTop);
0378             m_currentDocumentItem = currenturlitem;
0379         }
0380     }
0381 
0382     m_tree->sortItems(0, Qt::AscendingOrder);
0383 
0384     connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
0385 }
0386 
0387 void BookmarkList::goTo(BookmarkItem *item)
0388 {
0389     if (item->url() == m_document->currentDocument()) {
0390         m_document->setViewport(item->viewport(), nullptr, true);
0391     } else {
0392         Okular::GotoAction action(item->url().toDisplayString(QUrl::PreferLocalFile), item->viewport());
0393         m_document->processAction(&action);
0394     }
0395 }
0396 
0397 void BookmarkList::selectiveUrlUpdate(const QUrl &url, QTreeWidgetItem *&item)
0398 {
0399     disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
0400 
0401     const KBookmark::List urlbookmarks = m_document->bookmarkManager()->bookmarks(url);
0402     if (urlbookmarks.isEmpty()) {
0403         if (item != m_tree->invisibleRootItem()) {
0404             m_tree->invisibleRootItem()->removeChild(item);
0405             item = nullptr;
0406         } else if (item) {
0407             for (int i = item->childCount(); i >= 0; --i) {
0408                 item->removeChild(item->child(i));
0409             }
0410         }
0411     } else {
0412         bool fileitem_created = false;
0413 
0414         if (item) {
0415             for (int i = item->childCount() - 1; i >= 0; --i) {
0416                 item->removeChild(item->child(i));
0417             }
0418         } else {
0419             item = new FileItem(url, m_tree, m_document);
0420             fileitem_created = true;
0421         }
0422         if (m_document->isOpened() && url == m_document->currentDocument()) {
0423             item->setIcon(0, QIcon::fromTheme(QStringLiteral("bookmarks")));
0424             item->setExpanded(true);
0425         }
0426         item->addChildren(createItems(url, urlbookmarks));
0427 
0428         if (fileitem_created) {
0429             // we need to sort also the parent of the new file item,
0430             // so it can be properly shown in the correct place
0431             m_tree->invisibleRootItem()->sortChildren(0, Qt::AscendingOrder);
0432         }
0433         item->sortChildren(0, Qt::AscendingOrder);
0434     }
0435 
0436     connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged);
0437 }
0438 
0439 QTreeWidgetItem *BookmarkList::itemForUrl(const QUrl &url) const
0440 {
0441     const int count = m_tree->topLevelItemCount();
0442     for (int i = 0; i < count; ++i) {
0443         QTreeWidgetItem *item = m_tree->topLevelItem(i);
0444         const QUrl itemurl = item->data(0, UrlRole).value<QUrl>();
0445         if (itemurl.isValid() && itemurl == url) {
0446             return item;
0447         }
0448     }
0449     return nullptr;
0450 }
0451 
0452 #include "moc_bookmarklist.cpp"