File indexing completed on 2024-04-28 04:33:07

0001 /*
0002     SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "tocmodel.h"
0008 
0009 #include <QApplication>
0010 #include <QList>
0011 #include <QTreeView>
0012 #include <qdom.h>
0013 
0014 #include <QFont>
0015 
0016 #include "core/document.h"
0017 #include "core/page.h"
0018 
0019 Q_DECLARE_METATYPE(QModelIndex)
0020 
0021 struct TOCItem {
0022     TOCItem();
0023     TOCItem(TOCItem *parent, const QDomElement &e);
0024     ~TOCItem();
0025 
0026     TOCItem(const TOCItem &) = delete;
0027     TOCItem &operator=(const TOCItem &) = delete;
0028 
0029     QString text;
0030     Okular::DocumentViewport viewport;
0031     QString extFileName;
0032     QString url;
0033     bool highlight : 1;
0034     TOCItem *parent;
0035     QList<TOCItem *> children;
0036     TOCModelPrivate *model;
0037 };
0038 
0039 class TOCModelPrivate
0040 {
0041 public:
0042     explicit TOCModelPrivate(TOCModel *qq);
0043     ~TOCModelPrivate();
0044 
0045     void addChildren(const QDomNode &parentNode, TOCItem *parentItem);
0046     QModelIndex indexForItem(TOCItem *item) const;
0047     void findViewport(const Okular::DocumentViewport &viewport, TOCItem *item, QList<TOCItem *> &list) const;
0048 
0049     TOCModel *q;
0050     TOCItem *root;
0051     bool dirty : 1;
0052     Okular::Document *document;
0053     QList<TOCItem *> itemsToOpen;
0054     QList<TOCItem *> currentPage;
0055     TOCModel *m_oldModel;
0056     QVector<QModelIndex> m_oldTocExpandedIndexes;
0057     Q_DISABLE_COPY(TOCModelPrivate)
0058 };
0059 
0060 TOCItem::TOCItem()
0061     : highlight(false)
0062     , parent(nullptr)
0063     , model(nullptr)
0064 {
0065 }
0066 
0067 TOCItem::TOCItem(TOCItem *_parent, const QDomElement &e)
0068     : highlight(false)
0069     , parent(_parent)
0070 {
0071     parent->children.append(this);
0072     model = parent->model;
0073     text = e.tagName();
0074 
0075     // viewport loading
0076     if (e.hasAttribute(QStringLiteral("Viewport"))) {
0077         // if the node has a viewport, set it
0078         viewport = Okular::DocumentViewport(e.attribute(QStringLiteral("Viewport")));
0079     } else if (e.hasAttribute(QStringLiteral("ViewportName"))) {
0080         // if the node references a viewport, get the reference and set it
0081         const QString &page = e.attribute(QStringLiteral("ViewportName"));
0082         QString viewport_string = model->document->metaData(QStringLiteral("NamedViewport"), page).toString();
0083         if (!viewport_string.isEmpty()) {
0084             viewport = Okular::DocumentViewport(viewport_string);
0085         }
0086     }
0087 
0088     extFileName = e.attribute(QStringLiteral("ExternalFileName"));
0089     url = e.attribute(QStringLiteral("URL"));
0090 }
0091 
0092 TOCItem::~TOCItem()
0093 {
0094     qDeleteAll(children);
0095 }
0096 
0097 TOCModelPrivate::TOCModelPrivate(TOCModel *qq)
0098     : q(qq)
0099     , root(new TOCItem)
0100     , dirty(false)
0101     , document(nullptr)
0102     , m_oldModel(nullptr)
0103 {
0104     root->model = this;
0105 }
0106 
0107 TOCModelPrivate::~TOCModelPrivate()
0108 {
0109     delete root;
0110     delete m_oldModel;
0111 }
0112 
0113 void TOCModelPrivate::addChildren(const QDomNode &parentNode, TOCItem *parentItem)
0114 {
0115     TOCItem *currentItem = nullptr;
0116     QDomNode n = parentNode.firstChild();
0117     while (!n.isNull()) {
0118         // convert the node to an element (sure it is)
0119         QDomElement e = n.toElement();
0120 
0121         // insert the entry as top level (listview parented) or 2nd+ level
0122         currentItem = new TOCItem(parentItem, e);
0123 
0124         // descend recursively and advance to the next node
0125         if (e.hasChildNodes()) {
0126             addChildren(n, currentItem);
0127         }
0128 
0129         // open/keep close the item
0130         bool isOpen = false;
0131         if (e.hasAttribute(QStringLiteral("Open"))) {
0132             isOpen = QVariant(e.attribute(QStringLiteral("Open"))).toBool();
0133         }
0134         if (isOpen) {
0135             itemsToOpen.append(currentItem);
0136         }
0137 
0138         n = n.nextSibling();
0139         Q_EMIT q->countChanged();
0140     }
0141 }
0142 
0143 QModelIndex TOCModelPrivate::indexForItem(TOCItem *item) const
0144 {
0145     if (item->parent) {
0146         int id = item->parent->children.indexOf(item);
0147         if (id >= 0 && id < item->parent->children.count()) {
0148             return q->createIndex(id, 0, item);
0149         }
0150     }
0151     return QModelIndex();
0152 }
0153 
0154 void TOCModelPrivate::findViewport(const Okular::DocumentViewport &viewport, TOCItem *item, QList<TOCItem *> &list) const
0155 {
0156     TOCItem *todo = item;
0157 
0158     while (todo) {
0159         const TOCItem *current = todo;
0160         todo = nullptr;
0161         TOCItem *pos = nullptr;
0162 
0163         for (TOCItem *child : current->children) {
0164             if (child->viewport.isValid()) {
0165                 if (child->viewport.pageNumber <= viewport.pageNumber) {
0166                     pos = child;
0167                     if (child->viewport.pageNumber == viewport.pageNumber) {
0168                         break;
0169                     }
0170                 } else {
0171                     break;
0172                 }
0173             }
0174         }
0175         if (pos) {
0176             list.append(pos);
0177             todo = pos;
0178         }
0179     }
0180 }
0181 
0182 TOCModel::TOCModel(Okular::Document *document, QObject *parent)
0183     : QAbstractItemModel(parent)
0184     , d(new TOCModelPrivate(this))
0185 {
0186     d->document = document;
0187 
0188     qRegisterMetaType<QModelIndex>();
0189 }
0190 
0191 TOCModel::~TOCModel()
0192 {
0193     delete d;
0194 }
0195 
0196 QHash<int, QByteArray> TOCModel::roleNames() const
0197 {
0198     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
0199     roles[PageRole] = "page";
0200     roles[PageLabelRole] = "pageLabel";
0201     roles[HighlightRole] = "highlight";
0202     roles[HighlightedParentRole] = "highlightedParent";
0203     return roles;
0204 }
0205 
0206 int TOCModel::columnCount(const QModelIndex &parent) const
0207 {
0208     Q_UNUSED(parent)
0209     return 1;
0210 }
0211 
0212 QVariant TOCModel::data(const QModelIndex &index, int role) const
0213 {
0214     if (!index.isValid()) {
0215         return QVariant();
0216     }
0217 
0218     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
0219     switch (role) {
0220     case Qt::DisplayRole:
0221     case Qt::ToolTipRole:
0222         return item->text;
0223         break;
0224     case Qt::FontRole:
0225         if (item->highlight) {
0226             QFont font;
0227             font.setBold(true);
0228 
0229             TOCItem *lastHighlighted = d->currentPage.last();
0230 
0231             // in the mobile version our parent is not a QTreeView; embolden the last highlighted item
0232             // TODO misusing parent() here, fix
0233             QTreeView *view = dynamic_cast<QTreeView *>(QObject::parent());
0234             if (!view) {
0235                 if (item == lastHighlighted) {
0236                     return font;
0237                 }
0238                 return QVariant();
0239             }
0240 
0241             if (view->isExpanded(index)) {
0242                 // if this is the last highlighted node, its child is on a page below, thus it gets emboldened
0243                 if (item == lastHighlighted) {
0244                     return font;
0245                 }
0246             } else {
0247                 return font;
0248             }
0249         }
0250         break;
0251     case HighlightRole:
0252         return item->highlight;
0253     case PageRole:
0254         if (item->viewport.isValid()) {
0255             return item->viewport.pageNumber + 1;
0256         }
0257         break;
0258     case PageLabelRole:
0259         if (item->viewport.isValid() && item->viewport.pageNumber < int(d->document->pages())) {
0260             return d->document->page(item->viewport.pageNumber)->label();
0261         }
0262         break;
0263     }
0264     return QVariant();
0265 }
0266 
0267 bool TOCModel::hasChildren(const QModelIndex &parent) const
0268 {
0269     if (!parent.isValid()) {
0270         return true;
0271     }
0272 
0273     TOCItem *item = static_cast<TOCItem *>(parent.internalPointer());
0274     return !item->children.isEmpty();
0275 }
0276 
0277 QVariant TOCModel::headerData(int section, Qt::Orientation orientation, int role) const
0278 {
0279     if (orientation != Qt::Horizontal) {
0280         return QVariant();
0281     }
0282 
0283     if (section == 0 && role == Qt::DisplayRole) {
0284         return QStringLiteral("Topics");
0285     }
0286 
0287     return QVariant();
0288 }
0289 
0290 QModelIndex TOCModel::index(int row, int column, const QModelIndex &parent) const
0291 {
0292     if (row < 0 || column != 0) {
0293         return QModelIndex();
0294     }
0295 
0296     TOCItem *item = parent.isValid() ? static_cast<TOCItem *>(parent.internalPointer()) : d->root;
0297     if (row < item->children.count()) {
0298         return createIndex(row, column, item->children.at(row));
0299     }
0300 
0301     return QModelIndex();
0302 }
0303 
0304 QModelIndex TOCModel::parent(const QModelIndex &index) const
0305 {
0306     if (!index.isValid()) {
0307         return QModelIndex();
0308     }
0309 
0310     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
0311     return d->indexForItem(item->parent);
0312 }
0313 
0314 int TOCModel::rowCount(const QModelIndex &parent) const
0315 {
0316     TOCItem *item = parent.isValid() ? static_cast<TOCItem *>(parent.internalPointer()) : d->root;
0317     return item->children.count();
0318 }
0319 
0320 static QModelIndex indexForIndex(const QModelIndex &oldModelIndex, QAbstractItemModel *newModel)
0321 {
0322     QModelIndex newModelIndex;
0323     if (oldModelIndex.parent().isValid()) {
0324         newModelIndex = newModel->index(oldModelIndex.row(), oldModelIndex.column(), indexForIndex(oldModelIndex.parent(), newModel));
0325     } else {
0326         newModelIndex = newModel->index(oldModelIndex.row(), oldModelIndex.column());
0327     }
0328     return newModelIndex;
0329 }
0330 
0331 void TOCModel::fill(const Okular::DocumentSynopsis *toc)
0332 {
0333     if (!toc) {
0334         return;
0335     }
0336 
0337     clear();
0338     Q_EMIT layoutAboutToBeChanged();
0339     d->addChildren(*toc, d->root);
0340     d->dirty = true;
0341     Q_EMIT layoutChanged();
0342     if (equals(d->m_oldModel)) {
0343         for (const QModelIndex &oldIndex : std::as_const(d->m_oldTocExpandedIndexes)) {
0344             const QModelIndex index = indexForIndex(oldIndex, this);
0345             if (!index.isValid()) {
0346                 continue;
0347             }
0348 
0349             // TODO misusing parent() here, fix
0350             QMetaObject::invokeMethod(QObject::parent(), "expand", Qt::QueuedConnection, Q_ARG(QModelIndex, index));
0351         }
0352     } else {
0353         for (TOCItem *item : std::as_const(d->itemsToOpen)) {
0354             const QModelIndex index = d->indexForItem(item);
0355             if (!index.isValid()) {
0356                 continue;
0357             }
0358 
0359             // TODO misusing parent() here, fix
0360             QMetaObject::invokeMethod(QObject::parent(), "expand", Qt::QueuedConnection, Q_ARG(QModelIndex, index));
0361         }
0362     }
0363     d->itemsToOpen.clear();
0364     delete d->m_oldModel;
0365     d->m_oldModel = nullptr;
0366     d->m_oldTocExpandedIndexes.clear();
0367 }
0368 
0369 void TOCModel::clear()
0370 {
0371     if (!d->dirty) {
0372         return;
0373     }
0374 
0375     beginResetModel();
0376     qDeleteAll(d->root->children);
0377     d->root->children.clear();
0378     d->currentPage.clear();
0379     endResetModel();
0380     d->dirty = false;
0381 }
0382 
0383 void TOCModel::setCurrentViewport(const Okular::DocumentViewport &viewport)
0384 {
0385     for (TOCItem *item : std::as_const(d->currentPage)) {
0386         QModelIndex index = d->indexForItem(item);
0387         if (!index.isValid()) {
0388             continue;
0389         }
0390 
0391         item->highlight = false;
0392         Q_EMIT dataChanged(index, index);
0393     }
0394     d->currentPage.clear();
0395 
0396     QList<TOCItem *> newCurrentPage;
0397     d->findViewport(viewport, d->root, newCurrentPage);
0398 
0399     d->currentPage = newCurrentPage;
0400 
0401     for (TOCItem *item : std::as_const(d->currentPage)) {
0402         QModelIndex index = d->indexForItem(item);
0403         if (!index.isValid()) {
0404             continue;
0405         }
0406 
0407         item->highlight = true;
0408         Q_EMIT dataChanged(index, index);
0409     }
0410 }
0411 
0412 bool TOCModel::isEmpty() const
0413 {
0414     return d->root->children.isEmpty();
0415 }
0416 
0417 bool TOCModel::equals(const TOCModel *model) const
0418 {
0419     if (model) {
0420         return checkequality(model);
0421     } else {
0422         return false;
0423     }
0424 }
0425 
0426 void TOCModel::setOldModelData(TOCModel *model, const QVector<QModelIndex> &list)
0427 {
0428     delete d->m_oldModel;
0429     d->m_oldModel = model;
0430     d->m_oldTocExpandedIndexes = list;
0431 }
0432 
0433 bool TOCModel::hasOldModelData() const
0434 {
0435     return (d->m_oldModel != nullptr);
0436 }
0437 
0438 TOCModel *TOCModel::clearOldModelData() const
0439 {
0440     TOCModel *oldModel = d->m_oldModel;
0441     d->m_oldModel = nullptr;
0442     d->m_oldTocExpandedIndexes.clear();
0443     return oldModel;
0444 }
0445 
0446 QString TOCModel::externalFileNameForIndex(const QModelIndex &index) const
0447 {
0448     if (!index.isValid()) {
0449         return QString();
0450     }
0451 
0452     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
0453     return item->extFileName;
0454 }
0455 
0456 Okular::DocumentViewport TOCModel::viewportForIndex(const QModelIndex &index) const
0457 {
0458     if (!index.isValid()) {
0459         return Okular::DocumentViewport();
0460     }
0461 
0462     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
0463     return item->viewport;
0464 }
0465 
0466 QString TOCModel::urlForIndex(const QModelIndex &index) const
0467 {
0468     if (!index.isValid()) {
0469         return QString();
0470     }
0471 
0472     TOCItem *item = static_cast<TOCItem *>(index.internalPointer());
0473     return item->url;
0474 }
0475 
0476 bool TOCModel::checkequality(const TOCModel *model, const QModelIndex &parentA, const QModelIndex &parentB) const
0477 {
0478     if (rowCount(parentA) != model->rowCount(parentB)) {
0479         return false;
0480     }
0481     for (int i = 0; i < rowCount(parentA); i++) {
0482         QModelIndex indxA = index(i, 0, parentA);
0483         QModelIndex indxB = model->index(i, 0, parentB);
0484         if (indxA.data() != indxB.data()) {
0485             return false;
0486         }
0487         if (hasChildren(indxA) != model->hasChildren(indxB)) {
0488             return false;
0489         }
0490         if (!checkequality(model, indxA, indxB)) {
0491             return false;
0492         }
0493     }
0494     return true;
0495 }
0496 #include "moc_tocmodel.cpp"