File indexing completed on 2024-05-12 13:04:47

0001 /*
0002  * This file is part of the KDE project
0003  *
0004  * SPDX-FileCopyrightText: 2013 Dan Leinir Turthra Jensen <admin@leinir.dk>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.0-or-later
0007  *
0008  */
0009 
0010 #include "CQTextToCModel.h"
0011 #include "CQTextDocumentCanvas.h"
0012 
0013 #include <KoTextDocumentLayout.h>
0014 #include <KoTextLayoutRootArea.h>
0015 #include <KoTextPage.h>
0016 #include <styles/KoParagraphStyle.h>
0017 #include <KWDocument.h>
0018 #include <frames/KWTextFrameSet.h>
0019 
0020 #include <QTextObject>
0021 #include <QTextDocument>
0022 #include <QTextCursor>
0023 #include <QTimer>
0024 
0025 Q_DECLARE_METATYPE(QTextDocument*)
0026 
0027 struct TextToCModelEntry {
0028     TextToCModelEntry()
0029         : level(0)
0030         , pageNumber(0)
0031     {}
0032     QString title;
0033     int level;
0034     int pageNumber;
0035 };
0036 
0037 class CQTextToCModel::Private {
0038 public:
0039     Private()
0040         : canvas(0)
0041         , document(0)
0042         , documentLayout(0)
0043     {}
0044 
0045     QList<TextToCModelEntry*> entries;
0046 
0047     CQTextDocumentCanvas* canvas;
0048     QTextDocument* document;
0049     KoTextDocumentLayout* documentLayout;
0050 
0051     QTimer updateTimer;
0052     QTimer doneTimer;
0053 
0054     int resolvePageNumber(const QTextBlock &headingBlock) {
0055         KoTextDocumentLayout *layout = qobject_cast<KoTextDocumentLayout*>(document->documentLayout());
0056         KoTextLayoutRootArea *rootArea = layout->rootAreaForPosition(headingBlock.position());
0057         if (rootArea) {
0058             if (rootArea->page()) {
0059                 return rootArea->page()->visiblePageNumber();
0060             } else {
0061                 // had root but no page;
0062             }
0063         }
0064         return 0;
0065     }
0066 };
0067 
0068 CQTextToCModel::CQTextToCModel(QObject* parent)
0069     : QAbstractListModel(parent)
0070     , d(new Private)
0071 {
0072     QHash<int, QByteArray> roleNames;
0073     roleNames[Title] = "title";
0074     roleNames[Level] = "level";
0075     roleNames[PageNumber] = "pageNumber";
0076     setRoleNames(roleNames);
0077 
0078     connect(&d->updateTimer, SIGNAL(timeout()), this, SLOT(timeout()));
0079     d->updateTimer.setInterval(5000); // after 5 seconds of pause we update
0080     d->updateTimer.setSingleShot(true);
0081 
0082     connect(&d->doneTimer, SIGNAL(timeout()), this, SLOT(updateToC()));
0083     d->doneTimer.setInterval(1000); // after 1 seconds of silence we assume layout is done
0084     d->doneTimer.setSingleShot(true);
0085 }
0086 
0087 CQTextToCModel::~CQTextToCModel()
0088 {
0089     delete d;
0090 }
0091 
0092 QVariant CQTextToCModel::data(const QModelIndex& index, int role) const
0093 {
0094     QVariant result;
0095     if (index.isValid()) {
0096         int row = index.row();
0097         if (row > -1 && row < d->entries.count()) {
0098             const TextToCModelEntry* entry = d->entries.at(row);
0099             switch(role) {
0100                 case PageNumber:
0101                     result.setValue<int>(entry->pageNumber);
0102                     break;
0103                 case Level:
0104                     result.setValue<int>(entry->level);
0105                     break;
0106                 case Title:
0107                 default:
0108                     // Allowing the fallthrough here (explicitly mentioning our own Title entry)
0109                     // means that the predefined Qt Quick roles are also allowed to be filled
0110                     // with useful data
0111                     result.setValue<QString>(entry->title);
0112                     break;
0113             }
0114         }
0115     }
0116     return result;
0117 }
0118 
0119 int CQTextToCModel::rowCount(const QModelIndex& parent) const
0120 {
0121     if (parent.isValid()) {
0122         return 0;
0123     }
0124     return d->entries.count();
0125 }
0126 
0127 void CQTextToCModel::requestGeneration()
0128 {
0129     if (d->document->characterCount() < 2) {
0130         return;
0131     }
0132     d->updateTimer.stop();
0133     d->updateTimer.start();
0134 }
0135 
0136 void CQTextToCModel::startDoneTimer()
0137 {
0138     //we delay acting on the finishedLayout signal by 1 second. This way we
0139     // don't act on it until every header has had a chance to be layouted
0140     // in words (we assume that a new finishedLayout signal will arrive within that
0141     // 1 second)
0142     d->doneTimer.start();
0143 }
0144 
0145 void CQTextToCModel::timeout()
0146 {
0147     d->updateTimer.stop();
0148     d->documentLayout->scheduleLayout();
0149 }
0150 
0151 void CQTextToCModel::updateToC()
0152 {
0153     beginResetModel();
0154     QTextBlock block = d->document->firstBlock();
0155     qDeleteAll(d->entries.begin(), d->entries.end());
0156     d->entries.clear();
0157 
0158     while (block.isValid()) {
0159         QTextBlockFormat format = block.blockFormat();
0160         if (format.hasProperty(KoParagraphStyle::OutlineLevel)) {
0161             TextToCModelEntry* entry = new TextToCModelEntry();
0162             entry->title = block.text();
0163             entry->level = format.intProperty(KoParagraphStyle::OutlineLevel);
0164             entry->pageNumber = d->resolvePageNumber(block);
0165             d->entries.append(entry);
0166         }
0167         block = block.next();
0168     }
0169     endResetModel();
0170 }
0171 
0172 QObject* CQTextToCModel::canvas() const
0173 {
0174     return d->canvas;
0175 }
0176 
0177 void CQTextToCModel::setCanvas(QObject* newCanvas)
0178 {
0179     beginResetModel();
0180     if (d->documentLayout) {
0181         d->documentLayout->disconnect(this);
0182     }
0183     d->canvas = 0;
0184     d->document = 0;
0185     d->documentLayout = 0;
0186     CQTextDocumentCanvas* canvas = qobject_cast<CQTextDocumentCanvas*>(newCanvas);
0187     if (canvas) {
0188         d->canvas = canvas;
0189         d->document = canvas->document()->mainFrameSet()->document();
0190         d->documentLayout = static_cast<KoTextDocumentLayout *>(d->document->documentLayout());
0191 
0192         // connect to layoutIsDirty
0193         connect(d->documentLayout, SIGNAL(layoutIsDirty()), this, SLOT(requestGeneration()));
0194 
0195         // connect to FinishedLayout
0196         connect(d->documentLayout, SIGNAL(finishedLayout()), this, SLOT(startDoneTimer()));
0197     }
0198     emit canvasChanged();
0199     endResetModel();
0200 }