okular/gui/tocmodel.cpp
2024-04-15 19:59:42 +02:00

497 lines
13 KiB
C++

/*
SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "tocmodel.h"
#include <QApplication>
#include <QList>
#include <QTreeView>
#include <qdom.h>
#include <QFont>
#include "core/document.h"
#include "core/page.h"
Q_DECLARE_METATYPE(QModelIndex)
struct TOCItem {
TOCItem();
TOCItem(TOCItem *parent, const QDomElement &e);
~TOCItem();
TOCItem(const TOCItem &) = delete;
TOCItem &operator=(const TOCItem &) = delete;
QString text;
Okular::DocumentViewport viewport;
QString extFileName;
QString url;
bool highlight : 1;
TOCItem *parent;
QList<TOCItem *> children;
TOCModelPrivate *model;
};
class TOCModelPrivate
{
public:
explicit TOCModelPrivate(TOCModel *qq);
~TOCModelPrivate();
void addChildren(const QDomNode &parentNode, TOCItem *parentItem);
QModelIndex indexForItem(TOCItem *item) const;
void findViewport(const Okular::DocumentViewport &viewport, TOCItem *item, QList<TOCItem *> &list) const;
TOCModel *q;
TOCItem *root;
bool dirty : 1;
Okular::Document *document;
QList<TOCItem *> itemsToOpen;
QList<TOCItem *> currentPage;
TOCModel *m_oldModel;
QVector<QModelIndex> m_oldTocExpandedIndexes;
Q_DISABLE_COPY(TOCModelPrivate)
};
TOCItem::TOCItem()
: highlight(false)
, parent(nullptr)
, model(nullptr)
{
}
TOCItem::TOCItem(TOCItem *_parent, const QDomElement &e)
: highlight(false)
, parent(_parent)
{
parent->children.append(this);
model = parent->model;
text = e.tagName();
// viewport loading
if (e.hasAttribute(QStringLiteral("Viewport"))) {
// if the node has a viewport, set it
viewport = Okular::DocumentViewport(e.attribute(QStringLiteral("Viewport")));
} else if (e.hasAttribute(QStringLiteral("ViewportName"))) {
// if the node references a viewport, get the reference and set it
const QString &page = e.attribute(QStringLiteral("ViewportName"));
QString viewport_string = model->document->metaData(QStringLiteral("NamedViewport"), page).toString();
if (!viewport_string.isEmpty()) {
viewport = Okular::DocumentViewport(viewport_string);
}
}
extFileName = e.attribute(QStringLiteral("ExternalFileName"));
url = e.attribute(QStringLiteral("URL"));
}
TOCItem::~TOCItem()
{
qDeleteAll(children);
}
TOCModelPrivate::TOCModelPrivate(TOCModel *qq)
: q(qq)
, root(new TOCItem)
, dirty(false)
, document(nullptr)
, m_oldModel(nullptr)
{
root->model = this;
}
TOCModelPrivate::~TOCModelPrivate()
{
delete root;
delete m_oldModel;
}
void TOCModelPrivate::addChildren(const QDomNode &parentNode, TOCItem *parentItem)
{
TOCItem *currentItem = nullptr;
QDomNode n = parentNode.firstChild();
while (!n.isNull()) {
// convert the node to an element (sure it is)
QDomElement e = n.toElement();
// insert the entry as top level (listview parented) or 2nd+ level
currentItem = new TOCItem(parentItem, e);
// descend recursively and advance to the next node
if (e.hasChildNodes()) {
addChildren(n, currentItem);
}
// open/keep close the item
bool isOpen = false;
if (e.hasAttribute(QStringLiteral("Open"))) {
isOpen = QVariant(e.attribute(QStringLiteral("Open"))).toBool();
}
if (isOpen) {
itemsToOpen.append(currentItem);
}
n = n.nextSibling();
Q_EMIT q->countChanged();
}
}
QModelIndex TOCModelPrivate::indexForItem(TOCItem *item) const
{
if (item->parent) {
int id = item->parent->children.indexOf(item);
if (id >= 0 && id < item->parent->children.count()) {
return q->createIndex(id, 0, item);
}
}
return QModelIndex();
}
void TOCModelPrivate::findViewport(const Okular::DocumentViewport &viewport, TOCItem *item, QList<TOCItem *> &list) const
{
TOCItem *todo = item;
while (todo) {
const TOCItem *current = todo;
todo = nullptr;
TOCItem *pos = nullptr;
for (TOCItem *child : current->children) {
if (child->viewport.isValid()) {
if (child->viewport.pageNumber <= viewport.pageNumber) {
pos = child;
if (child->viewport.pageNumber == viewport.pageNumber) {
break;
}
} else {
break;
}
}
}
if (pos) {
list.append(pos);
todo = pos;
}
}
}
TOCModel::TOCModel(Okular::Document *document, QObject *parent)
: QAbstractItemModel(parent)
, d(new TOCModelPrivate(this))
{
d->document = document;
qRegisterMetaType<QModelIndex>();
}
TOCModel::~TOCModel()
{
delete d;
}
QHash<int, QByteArray> TOCModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[PageRole] = "page";
roles[PageLabelRole] = "pageLabel";
roles[HighlightRole] = "highlight";
roles[HighlightedParentRole] = "highlightedParent";
return roles;
}
int TOCModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
QVariant TOCModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
switch (role) {
case Qt::DisplayRole:
case Qt::ToolTipRole:
return item->text;
break;
case Qt::FontRole:
if (item->highlight) {
QFont font;
font.setBold(true);
const TOCItem *lastHighlighted = d->currentPage.last();
// in the mobile version our parent is not a QTreeView; embolden the last highlighted item
// TODO misusing parent() here, fix
QTreeView *view = dynamic_cast<QTreeView *>(QObject::parent());
if (!view) {
if (item == lastHighlighted) {
return font;
}
return QVariant();
}
if (view->isExpanded(index)) {
// if this is the last highlighted node, its child is on a page below, thus it gets emboldened
if (item == lastHighlighted) {
return font;
}
} else {
return font;
}
}
break;
case HighlightRole:
return item->highlight;
case PageRole:
if (item->viewport.isValid()) {
return item->viewport.pageNumber + 1;
}
break;
case PageLabelRole:
if (item->viewport.isValid() && item->viewport.pageNumber < int(d->document->pages())) {
return d->document->page(item->viewport.pageNumber)->label();
}
break;
}
return QVariant();
}
bool TOCModel::hasChildren(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return true;
}
TOCItem *item = static_cast<TOCItem *>(parent.internalPointer());
return !item->children.isEmpty();
}
QVariant TOCModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal) {
return QVariant();
}
if (section == 0 && role == Qt::DisplayRole) {
return QStringLiteral("Topics");
}
return QVariant();
}
QModelIndex TOCModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column != 0) {
return QModelIndex();
}
TOCItem *item = parent.isValid() ? static_cast<TOCItem *>(parent.internalPointer()) : d->root;
if (row < item->children.count()) {
return createIndex(row, column, item->children.at(row));
}
return QModelIndex();
}
QModelIndex TOCModel::parent(const QModelIndex &index) const
{
if (!index.isValid()) {
return QModelIndex();
}
TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
return d->indexForItem(item->parent);
}
int TOCModel::rowCount(const QModelIndex &parent) const
{
TOCItem *item = parent.isValid() ? static_cast<TOCItem *>(parent.internalPointer()) : d->root;
return item->children.count();
}
static QModelIndex indexForIndex(const QModelIndex &oldModelIndex, QAbstractItemModel *newModel)
{
QModelIndex newModelIndex;
if (oldModelIndex.parent().isValid()) {
newModelIndex = newModel->index(oldModelIndex.row(), oldModelIndex.column(), indexForIndex(oldModelIndex.parent(), newModel));
} else {
newModelIndex = newModel->index(oldModelIndex.row(), oldModelIndex.column());
}
return newModelIndex;
}
void TOCModel::fill(const Okular::DocumentSynopsis *toc)
{
if (!toc) {
return;
}
clear();
Q_EMIT layoutAboutToBeChanged();
d->addChildren(*toc, d->root);
d->dirty = true;
Q_EMIT layoutChanged();
if (equals(d->m_oldModel)) {
for (const QModelIndex &oldIndex : std::as_const(d->m_oldTocExpandedIndexes)) {
const QModelIndex index = indexForIndex(oldIndex, this);
if (!index.isValid()) {
continue;
}
// TODO misusing parent() here, fix
QMetaObject::invokeMethod(QObject::parent(), "expand", Qt::QueuedConnection, Q_ARG(QModelIndex, index));
}
} else {
for (TOCItem *item : std::as_const(d->itemsToOpen)) {
const QModelIndex index = d->indexForItem(item);
if (!index.isValid()) {
continue;
}
// TODO misusing parent() here, fix
QMetaObject::invokeMethod(QObject::parent(), "expand", Qt::QueuedConnection, Q_ARG(QModelIndex, index));
}
}
d->itemsToOpen.clear();
delete d->m_oldModel;
d->m_oldModel = nullptr;
d->m_oldTocExpandedIndexes.clear();
}
void TOCModel::clear()
{
if (!d->dirty) {
return;
}
beginResetModel();
qDeleteAll(d->root->children);
d->root->children.clear();
d->currentPage.clear();
endResetModel();
d->dirty = false;
}
void TOCModel::setCurrentViewport(const Okular::DocumentViewport &viewport)
{
for (TOCItem *item : std::as_const(d->currentPage)) {
QModelIndex index = d->indexForItem(item);
if (!index.isValid()) {
continue;
}
item->highlight = false;
Q_EMIT dataChanged(index, index);
}
d->currentPage.clear();
QList<TOCItem *> newCurrentPage;
d->findViewport(viewport, d->root, newCurrentPage);
d->currentPage = newCurrentPage;
for (TOCItem *item : std::as_const(d->currentPage)) {
QModelIndex index = d->indexForItem(item);
if (!index.isValid()) {
continue;
}
item->highlight = true;
Q_EMIT dataChanged(index, index);
}
}
bool TOCModel::isEmpty() const
{
return d->root->children.isEmpty();
}
bool TOCModel::equals(const TOCModel *model) const
{
if (model) {
return checkequality(model);
} else {
return false;
}
}
void TOCModel::setOldModelData(TOCModel *model, const QVector<QModelIndex> &list)
{
delete d->m_oldModel;
d->m_oldModel = model;
d->m_oldTocExpandedIndexes = list;
}
bool TOCModel::hasOldModelData() const
{
return (d->m_oldModel != nullptr);
}
TOCModel *TOCModel::clearOldModelData() const
{
TOCModel *oldModel = d->m_oldModel;
d->m_oldModel = nullptr;
d->m_oldTocExpandedIndexes.clear();
return oldModel;
}
QString TOCModel::externalFileNameForIndex(const QModelIndex &index) const
{
if (!index.isValid()) {
return QString();
}
const TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
return item->extFileName;
}
Okular::DocumentViewport TOCModel::viewportForIndex(const QModelIndex &index) const
{
if (!index.isValid()) {
return Okular::DocumentViewport();
}
const TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
return item->viewport;
}
QString TOCModel::urlForIndex(const QModelIndex &index) const
{
if (!index.isValid()) {
return QString();
}
const TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
return item->url;
}
bool TOCModel::checkequality(const TOCModel *model, const QModelIndex &parentA, const QModelIndex &parentB) const
{
if (rowCount(parentA) != model->rowCount(parentB)) {
return false;
}
for (int i = 0; i < rowCount(parentA); i++) {
QModelIndex indxA = index(i, 0, parentA);
QModelIndex indxB = model->index(i, 0, parentB);
if (indxA.data() != indxB.data()) {
return false;
}
if (hasChildren(indxA) != model->hasChildren(indxB)) {
return false;
}
if (!checkequality(model, indxA, indxB)) {
return false;
}
}
return true;
}
#include "moc_tocmodel.cpp"