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"