okular/part/side_reviews.cpp
2024-05-24 10:52:39 +00:00

368 lines
12 KiB
C++

/*
SPDX-FileCopyrightText: 2005 Enrico Ros <eros.kde@email.it>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "side_reviews.h"
// qt/kde includes
#include <QAction>
#include <QHeaderView>
#include <QIcon>
#include <QLayout>
#include <QPaintEvent>
#include <QPainter>
#include <QSizePolicy>
#include <QStringList>
#include <QTextDocument>
#include <QToolBar>
#include <QTreeView>
#include <KLocalizedString>
#include <KTitleWidget>
#include <KViewStateSerializer>
#include <kwidgetsaddons_version.h>
// local includes
#include "annotationmodel.h"
#include "annotationpopup.h"
#include "annotationproxymodels.h"
#include "core/annotations.h"
#include "core/document.h"
#include "core/page.h"
#include "ktreeviewsearchline.h"
#include "settings.h"
class TreeView : public QTreeView
{
Q_OBJECT
public:
explicit TreeView(Okular::Document *document, QWidget *parent = Q_NULLPTR)
: QTreeView(parent)
, m_document(document)
{
}
protected:
void paintEvent(QPaintEvent *event) override
{
bool hasAnnotations = false;
for (uint i = 0; i < m_document->pages(); ++i) {
if (m_document->page(i)->hasAnnotations()) {
hasAnnotations = true;
break;
}
}
if (!hasAnnotations) {
QPainter p(viewport());
p.setRenderHint(QPainter::Antialiasing, true);
p.setClipRect(event->rect());
QTextDocument document;
document.setHtml(
i18n("<div align=center><h3>No annotations</h3>"
"To create new annotations press F6 or select <i>Tools -&gt; Annotations</i>"
" from the menu.</div>"));
document.setTextWidth(width() - 50);
const uint w = document.size().width() + 20;
const uint h = document.size().height() + 20;
p.setBrush(palette().window());
p.translate(0.5, 0.5);
p.drawRoundedRect(15, 15, w, h, 3, 3);
p.translate(20, 20);
document.drawContents(&p);
} else {
QTreeView::paintEvent(event);
}
}
private:
Okular::Document *m_document;
};
Reviews::Reviews(QWidget *parent, Okular::Document *document)
: QWidget(parent)
, m_document(document)
{
// create widgets and layout them vertically
QVBoxLayout *vLayout = new QVBoxLayout(this);
vLayout->setSpacing(6);
KTitleWidget *titleWidget = new KTitleWidget(this);
titleWidget->setLevel(4);
titleWidget->setText(i18n("Annotations"));
m_view = new TreeView(m_document, this);
m_view->setAlternatingRowColors(true);
m_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_view->header()->hide();
QToolBar *toolBar = new QToolBar(this);
toolBar->setObjectName(QStringLiteral("reviewOptsBar"));
QSizePolicy sp = toolBar->sizePolicy();
sp.setVerticalPolicy(QSizePolicy::Minimum);
toolBar->setSizePolicy(sp);
m_model = new AnnotationModel(m_document, m_view);
m_filterProxy = new PageFilterProxyModel(m_view);
m_groupProxy = new PageGroupProxyModel(m_view);
m_authorProxy = new AuthorGroupProxyModel(m_view);
m_filterProxy->setSourceModel(m_model);
m_groupProxy->setSourceModel(m_filterProxy);
m_authorProxy->setSourceModel(m_groupProxy);
m_view->setModel(m_authorProxy);
connect(m_authorProxy, &QAbstractItemModel::modelAboutToBeReset, this, &Reviews::saveTreeState);
// Must be after setModel() so that we restore after QTreeView::reset cleared expanded items, not the other way around
connect(m_authorProxy, &QAbstractItemModel::modelReset, this, &Reviews::restoreTreeState);
m_searchLine = new KTreeViewSearchLine(this, m_view);
m_searchLine->setPlaceholderText(i18n("Search..."));
m_searchLine->setCaseSensitivity(Okular::Settings::self()->reviewsSearchCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive);
m_searchLine->setRegularExpression(Okular::Settings::self()->reviewsSearchRegularExpression());
connect(m_searchLine, &KTreeViewSearchLine::searchOptionsChanged, this, &Reviews::saveSearchOptions);
vLayout->addWidget(titleWidget);
vLayout->setAlignment(titleWidget, Qt::AlignHCenter);
vLayout->addWidget(m_searchLine);
vLayout->addWidget(m_view);
vLayout->addWidget(toolBar);
toolBar->setIconSize(QSize(16, 16));
toolBar->setMovable(false);
// - add Page button
QAction *groupByPageAction = toolBar->addAction(QIcon::fromTheme(QStringLiteral("text-x-generic")), i18n("Group by Page"));
groupByPageAction->setCheckable(true);
connect(groupByPageAction, &QAction::toggled, this, &Reviews::slotPageEnabled);
groupByPageAction->setChecked(Okular::Settings::groupByPage());
// - add Author button
QAction *groupByAuthorAction = toolBar->addAction(QIcon::fromTheme(QStringLiteral("user-identity")), i18n("Group by Author"));
groupByAuthorAction->setCheckable(true);
connect(groupByAuthorAction, &QAction::toggled, this, &Reviews::slotAuthorEnabled);
groupByAuthorAction->setChecked(Okular::Settings::groupByAuthor());
// - add separator
toolBar->addSeparator();
// - add Current Page Only button
QAction *curPageOnlyAction = toolBar->addAction(QIcon::fromTheme(QStringLiteral("arrow-down")), i18n("Show annotations for current page only"));
curPageOnlyAction->setCheckable(true);
connect(curPageOnlyAction, &QAction::toggled, this, &Reviews::slotCurrentPageOnly);
curPageOnlyAction->setChecked(Okular::Settings::currentPageOnly());
// Adds space between left actions, so that the next two buttons are aligned to the right
QWidget *spacer = new QWidget();
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
toolBar->addWidget(spacer);
QAction *expandAll = toolBar->addAction(QIcon::fromTheme(QStringLiteral("expand-all")), i18n("Expand all elements"));
connect(expandAll, &QAction::triggered, this, &Reviews::slotExpandAll);
QAction *collapseAll = toolBar->addAction(QIcon::fromTheme(QStringLiteral("collapse-all")), i18n("Collapse all elements"));
connect(collapseAll, &QAction::triggered, this, &Reviews::slotCollapseAll);
connect(m_view, &TreeView::activated, this, &Reviews::activated);
m_view->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_view, &TreeView::customContextMenuRequested, this, &Reviews::contextMenuRequested);
}
Reviews::~Reviews()
{
m_document->removeObserver(this);
}
// BEGIN DocumentObserver Notifies
void Reviews::notifyCurrentPageChanged(int previousPage, int currentPage)
{
Q_UNUSED(previousPage)
m_filterProxy->setCurrentPage(currentPage);
}
// END DocumentObserver Notifies
void Reviews::reparseConfig()
{
m_searchLine->setCaseSensitivity(Okular::Settings::reviewsSearchCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive);
m_searchLine->setRegularExpression(Okular::Settings::reviewsSearchRegularExpression());
m_view->update();
}
// BEGIN GUI Slots -> requestListViewUpdate
void Reviews::slotPageEnabled(bool on)
{
// store toggle state in Settings and update the listview
Okular::Settings::setGroupByPage(on);
m_groupProxy->groupByPage(on);
m_view->expandAll();
}
void Reviews::slotAuthorEnabled(bool on)
{
// store toggle state in Settings and update the listview
Okular::Settings::setGroupByAuthor(on);
m_authorProxy->groupByAuthor(on);
m_view->expandAll();
}
void Reviews::slotCurrentPageOnly(bool on)
{
// store toggle state in Settings and update the listview
Okular::Settings::setCurrentPageOnly(on);
m_filterProxy->groupByCurrentPage(on);
m_view->expandAll();
}
void Reviews::slotExpandAll()
{
m_view->expandAll();
}
void Reviews::slotCollapseAll()
{
m_view->collapseAll();
}
// END GUI Slots
void Reviews::activated(const QModelIndex &index)
{
const QModelIndex authorIndex = m_authorProxy->mapToSource(index);
const QModelIndex filterIndex = m_groupProxy->mapToSource(authorIndex);
const QModelIndex annotIndex = m_filterProxy->mapToSource(filterIndex);
Okular::Annotation *annotation = m_model->annotationForIndex(annotIndex);
if (!annotation) {
return;
}
int pageNumber = m_model->data(annotIndex, AnnotationModel::PageRole).toInt();
const Okular::Page *page = m_document->page(pageNumber);
// calculating the right coordinates to center the view on the annotation
QRect rect = Okular::AnnotationUtils::annotationGeometry(annotation, page->width(), page->height());
Okular::NormalizedRect nr(rect, (int)page->width(), (int)page->height());
// set the viewport parameters
Okular::DocumentViewport vp;
vp.pageNumber = pageNumber;
vp.rePos.enabled = true;
vp.rePos.pos = Okular::DocumentViewport::Center;
vp.rePos.normalizedX = (nr.right + nr.left) / 2.0;
vp.rePos.normalizedY = (nr.bottom + nr.top) / 2.0;
// setting the viewport
m_document->setViewport(vp, nullptr, true);
}
QModelIndexList Reviews::retrieveAnnotations(const QModelIndex &idx) const
{
QModelIndexList ret;
if (idx.isValid()) {
const QAbstractItemModel *model = idx.model();
if (model->hasChildren(idx)) {
int rowCount = model->rowCount(idx);
for (int i = 0; i < rowCount; i++) {
ret += retrieveAnnotations(model->index(i, idx.column(), idx));
}
} else {
ret += idx;
}
}
return ret;
}
void Reviews::contextMenuRequested(const QPoint pos)
{
AnnotationPopup popup(m_document, AnnotationPopup::SingleAnnotationMode, this);
connect(&popup, &AnnotationPopup::openAnnotationWindow, this, &Reviews::openAnnotationWindow);
const QModelIndexList indexes = m_view->selectionModel()->selectedIndexes();
for (const QModelIndex &index : indexes) {
const QModelIndexList annotations = retrieveAnnotations(index);
for (const QModelIndex &idx : annotations) {
const QModelIndex authorIndex = m_authorProxy->mapToSource(idx);
const QModelIndex filterIndex = m_groupProxy->mapToSource(authorIndex);
const QModelIndex annotIndex = m_filterProxy->mapToSource(filterIndex);
Okular::Annotation *annotation = m_model->annotationForIndex(annotIndex);
if (annotation) {
const int pageNumber = m_model->data(annotIndex, AnnotationModel::PageRole).toInt();
popup.addAnnotation(annotation, pageNumber);
}
}
}
popup.exec(m_view->viewport()->mapToGlobal(pos));
}
void Reviews::saveSearchOptions()
{
Okular::Settings::setReviewsSearchRegularExpression(m_searchLine->regularExpression());
Okular::Settings::setReviewsSearchCaseSensitive(m_searchLine->caseSensitivity() == Qt::CaseSensitive ? true : false);
Okular::Settings::self()->save();
}
class AnnotationStateSaver : public KViewStateSerializer
{
Q_OBJECT
public:
explicit AnnotationStateSaver(QTreeView *view)
: KViewStateSerializer()
{
setView(view);
}
QString indexToConfigString(const QModelIndex &index) const override
{
// Store the display text, with parent if any, e.g. "Page 4 / David Faure"
QString key = index.data().toString();
if (auto parent = index.parent(); parent.isValid())
key.prepend(parent.data().toString() + QLatin1String(" / "));
return key;
}
QModelIndex indexFromConfigString(const QAbstractItemModel *model, const QString &key) const override
{
const auto matchFirst = [model](const QModelIndex &startIndex, const QString &text) {
const QModelIndexList list = model->match(startIndex, Qt::DisplayRole, text, 1, Qt::MatchRecursive);
if (list.isEmpty()) {
return QModelIndex();
}
return list.first();
};
const QStringList subKeys = key.split(QStringLiteral(" / "));
QModelIndex index = model->index(0, 0);
for (const QString &subKey : subKeys) {
index = matchFirst(index, subKey);
if (!index.isValid())
break;
}
return index;
}
};
void Reviews::saveTreeState()
{
AnnotationStateSaver saver(m_view);
m_expansionKeys = saver.expansionKeys();
}
void Reviews::restoreTreeState()
{
AnnotationStateSaver saver(m_view);
saver.restoreExpanded(m_expansionKeys);
}
QAbstractItemModel *Reviews::annotationsModel() const
{
return m_model;
}
#include "side_reviews.moc"