611 lines
18 KiB
C++
611 lines
18 KiB
C++
/*
|
|
* Copyright (C) 2015-2016 Jolla Ltd.
|
|
* Contact: Slava Monich <slava.monich@jolla.com>
|
|
*
|
|
* You may use this file under the terms of the BSD license as follows:
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in
|
|
* the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Jolla Ltd nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this
|
|
* software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "BooksBookModel.h"
|
|
#include "BooksTextStyle.h"
|
|
|
|
#include "HarbourDebug.h"
|
|
|
|
#include "ZLTextHyphenator.h"
|
|
|
|
// ==========================================================================
|
|
// BooksBookModel::Data
|
|
// ==========================================================================
|
|
class BooksBookModel::Data {
|
|
public:
|
|
Data(int aWidth, int aHeight) : iWidth(aWidth), iHeight(aHeight) {}
|
|
|
|
int pickPage(const BooksPos& aPagePos) const;
|
|
int pickPage(const BooksPos& aPagePos, const BooksPos& aNextPagePos,
|
|
int aPageCount) const;
|
|
|
|
public:
|
|
int iWidth;
|
|
int iHeight;
|
|
shared_ptr<BookModel> iBookModel;
|
|
BooksPos::List iPageMarks;
|
|
};
|
|
|
|
int BooksBookModel::Data::pickPage(const BooksPos& aPagePos) const
|
|
{
|
|
int page = 0;
|
|
if (aPagePos.valid()) {
|
|
BooksPos::ConstIterator it = qFind(iPageMarks, aPagePos);
|
|
if (it == iPageMarks.end()) {
|
|
it = qUpperBound(iPageMarks, aPagePos);
|
|
page = (int)(it - iPageMarks.begin()) - 1;
|
|
HDEBUG("using page" << page << "for" << aPagePos);
|
|
} else {
|
|
page = it - iPageMarks.begin();
|
|
HDEBUG("found" << aPagePos << "at page" << page);
|
|
}
|
|
}
|
|
return page;
|
|
}
|
|
|
|
int BooksBookModel::Data::pickPage(const BooksPos& aPagePos,
|
|
const BooksPos& aNextPagePos, int aPageCount) const
|
|
{
|
|
int page = 0;
|
|
if (aPagePos.valid()) {
|
|
if (!aNextPagePos.valid()) {
|
|
// Last page stays the last
|
|
page = iPageMarks.count() - 1;
|
|
HDEBUG("last page" << page);
|
|
} else {
|
|
BooksPos::ConstIterator it = qFind(iPageMarks, aPagePos);
|
|
if (it == iPageMarks.end()) {
|
|
// Two 90-degrees rotations should return the reader
|
|
// back to the same page. That's what this is about.
|
|
const BooksPos& pos = (iPageMarks.count() > aPageCount) ?
|
|
aPagePos : aNextPagePos;
|
|
it = qUpperBound(iPageMarks, pos);
|
|
page = (int)(it - iPageMarks.begin());
|
|
if (page > 0) page--;
|
|
HDEBUG("using page" << page << "for" << pos);
|
|
} else {
|
|
page = it - iPageMarks.begin();
|
|
HDEBUG("found" << aPagePos << "at page" << page);
|
|
}
|
|
}
|
|
}
|
|
return page;
|
|
}
|
|
|
|
// ==========================================================================
|
|
// BooksBookModel::Task
|
|
// ==========================================================================
|
|
|
|
class BooksBookModel::Task : public BooksTask
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
Task(BooksBookModel* aReceiver, shared_ptr<Book> aBook,
|
|
const BooksPos& aPagePos, const BooksPos& aNextPagePos,
|
|
const BooksPos& aLastPos, int aPageCount);
|
|
~Task();
|
|
|
|
void performTask();
|
|
|
|
Q_SIGNALS:
|
|
void progress(int aProgress);
|
|
|
|
public:
|
|
shared_ptr<Book> iBook;
|
|
shared_ptr<ZLTextStyle> iTextStyle;
|
|
BooksMargins iMargins;
|
|
BooksPaintContext iPaint;
|
|
BooksBookModel::Data* iData;
|
|
BooksPos iPagePos;
|
|
BooksPos iNextPagePos;
|
|
BooksPos iLastPos;
|
|
int iOldPageCount;
|
|
int iPage;
|
|
};
|
|
|
|
BooksBookModel::Task::Task(BooksBookModel* aModel,
|
|
shared_ptr<Book> aBook, const BooksPos& aPagePos,
|
|
const BooksPos& aNextPagePos, const BooksPos& aLastPos, int aPageCount) :
|
|
iBook(aBook),
|
|
iTextStyle(aModel->textStyle()),
|
|
iMargins(aModel->margins()),
|
|
iPaint(aModel->width(), aModel->height()),
|
|
iData(NULL),
|
|
iPagePos(aPagePos),
|
|
iNextPagePos(aNextPagePos),
|
|
iLastPos(aLastPos),
|
|
iOldPageCount(aPageCount),
|
|
iPage(-1)
|
|
{
|
|
aModel->connect(this, SIGNAL(done()), SLOT(onResetDone()));
|
|
aModel->connect(this, SIGNAL(progress(int)), SLOT(onResetProgress(int)),
|
|
Qt::QueuedConnection);
|
|
}
|
|
|
|
BooksBookModel::Task::~Task()
|
|
{
|
|
delete iData;
|
|
}
|
|
|
|
void BooksBookModel::Task::performTask()
|
|
{
|
|
if (!isCanceled()) {
|
|
iData = new BooksBookModel::Data(iPaint.width(), iPaint.height());
|
|
iData->iBookModel = new BookModel(iBook);
|
|
shared_ptr<ZLTextModel> model(iData->iBookModel->bookTextModel());
|
|
ZLTextHyphenator::Instance().load(iBook->language());
|
|
if (!isCanceled()) {
|
|
BooksTextView view(iPaint, iTextStyle, iMargins);
|
|
view.setModel(model);
|
|
if (model->paragraphsNumber() > 0) {
|
|
BooksPos mark = view.rewind();
|
|
iData->iPageMarks.append(mark);
|
|
Q_EMIT progress(iData->iPageMarks.count());
|
|
while (!isCanceled() && view.nextPage()) {
|
|
mark = view.position();
|
|
iData->iPageMarks.append(mark);
|
|
Q_EMIT progress(iData->iPageMarks.count());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!isCanceled()) {
|
|
HDEBUG(iData->iPageMarks.count() << "page(s)" << qPrintable(
|
|
QString("%1x%2").arg(iData->iWidth).arg(iData->iHeight)));
|
|
iPage = iPagePos.valid() ?
|
|
iData->pickPage(iPagePos, iNextPagePos, iOldPageCount) :
|
|
iData->pickPage(iLastPos);
|
|
} else {
|
|
HDEBUG("giving up" << qPrintable(QString("%1x%2").arg(iPaint.width()).
|
|
arg(iPaint.height())) << "paging");
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// BooksBookModel
|
|
// ==========================================================================
|
|
|
|
enum BooksBookModelRole {
|
|
BooksBookModelPageIndex = Qt::UserRole
|
|
};
|
|
|
|
BooksBookModel::BooksBookModel(QObject* aParent) :
|
|
QAbstractListModel(aParent),
|
|
iResetReason(ReasonUnknown),
|
|
iCurrentPage(-1),
|
|
iProgress(0),
|
|
iBook(NULL),
|
|
iTask(NULL),
|
|
iData(NULL),
|
|
iData2(NULL),
|
|
iSettings(BooksSettings::sharedInstance()),
|
|
iTaskQueue(BooksTaskQueue::defaultQueue())
|
|
{
|
|
iTextStyle = iSettings->textStyle(fontSizeAdjust());
|
|
connect(iSettings.data(), SIGNAL(textStyleChanged()), SLOT(onTextStyleChanged()));
|
|
HDEBUG("created");
|
|
#if QT_VERSION < 0x050000
|
|
setRoleNames(roleNames());
|
|
#endif
|
|
}
|
|
|
|
BooksBookModel::~BooksBookModel()
|
|
{
|
|
if (iTask) iTask->release(this);
|
|
if (iBook) {
|
|
iBook->disconnect(this);
|
|
iBook->release();
|
|
iBook = NULL;
|
|
}
|
|
delete iData;
|
|
delete iData2;
|
|
HDEBUG("destroyed");
|
|
}
|
|
|
|
void BooksBookModel::setBook(BooksBook* aBook)
|
|
{
|
|
shared_ptr<Book> oldBook;
|
|
shared_ptr<Book> newBook;
|
|
if (iBook != aBook) {
|
|
const QString oldTitle(iTitle);
|
|
if (iBook) {
|
|
iBook->disconnect(this);
|
|
iBook->release();
|
|
}
|
|
if (aBook) {
|
|
(iBook = aBook)->retain();
|
|
iBookRef = newBook;
|
|
iTitle = iBook->title();
|
|
iTextStyle = iSettings->textStyle(fontSizeAdjust());
|
|
connect(iBook, SIGNAL(fontSizeAdjustChanged()), SLOT(onTextStyleChanged()));
|
|
HDEBUG(iTitle);
|
|
} else {
|
|
iBook = NULL;
|
|
iBookRef.reset();
|
|
iTitle = QString();
|
|
HDEBUG("<none>");
|
|
}
|
|
startReset(ReasonLoading, true);
|
|
if (oldTitle != iTitle) {
|
|
Q_EMIT titleChanged();
|
|
}
|
|
Q_EMIT textStyleChanged();
|
|
Q_EMIT bookModelChanged();
|
|
Q_EMIT bookChanged();
|
|
}
|
|
}
|
|
|
|
bool BooksBookModel::loading() const
|
|
{
|
|
return (iTask != NULL);
|
|
}
|
|
|
|
bool BooksBookModel::increaseFontSize()
|
|
{
|
|
return iBook && iBook->setFontSizeAdjust(iBook->fontSizeAdjust()+1);
|
|
}
|
|
|
|
bool BooksBookModel::decreaseFontSize()
|
|
{
|
|
return iBook && iBook->setFontSizeAdjust(iBook->fontSizeAdjust()-1);
|
|
}
|
|
|
|
void BooksBookModel::setCurrentPage(int aPage)
|
|
{
|
|
if (iCurrentPage != aPage) {
|
|
iCurrentPage = aPage;
|
|
if (iData &&
|
|
iCurrentPage >= 0 &&
|
|
iCurrentPage < iData->iPageMarks.count()) {
|
|
iBook->setLastPos(iData->iPageMarks.at(iCurrentPage));
|
|
HDEBUG(aPage << iBook->lastPos());
|
|
} else {
|
|
HDEBUG(aPage);
|
|
}
|
|
Q_EMIT currentPageChanged();
|
|
}
|
|
}
|
|
|
|
int BooksBookModel::pageCount() const
|
|
{
|
|
return iData ? iData->iPageMarks.count() : 0;
|
|
}
|
|
|
|
BooksPos::List BooksBookModel::pageMarks() const
|
|
{
|
|
return iData ? iData->iPageMarks : BooksPos::List();
|
|
}
|
|
|
|
int BooksBookModel::fontSizeAdjust() const
|
|
{
|
|
return iBook ? iBook->fontSizeAdjust() : 0;
|
|
}
|
|
|
|
BooksPos BooksBookModel::pageMark(int aPage) const
|
|
{
|
|
if (aPage >= 0 && iData) {
|
|
const int n = iData->iPageMarks.count();
|
|
if (aPage < n) {
|
|
return iData->iPageMarks.at(aPage);
|
|
}
|
|
}
|
|
return BooksPos();
|
|
}
|
|
|
|
int BooksBookModel::linkToPage(const std::string& aLink) const
|
|
{
|
|
if (iData && !iData->iBookModel.isNull()) {
|
|
BookModel::Label label = iData->iBookModel->label(aLink);
|
|
if (label.ParagraphNumber >= 0) {
|
|
return iData->pickPage(BooksPos(label.ParagraphNumber, 0, 0));
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
shared_ptr<BookModel> BooksBookModel::bookModel() const
|
|
{
|
|
return iData ? iData->iBookModel : NULL;
|
|
}
|
|
|
|
shared_ptr<ZLTextModel> BooksBookModel::bookTextModel() const
|
|
{
|
|
shared_ptr<ZLTextModel> model;
|
|
if (iData && !iData->iBookModel.isNull()) {
|
|
model = iData->iBookModel->bookTextModel();
|
|
}
|
|
return model;
|
|
}
|
|
|
|
shared_ptr<ZLTextModel> BooksBookModel::footnoteModel(const std::string& aId) const
|
|
{
|
|
shared_ptr<ZLTextModel> model;
|
|
if (iData && !iData->iBookModel.isNull()) {
|
|
model = iData->iBookModel->footnoteModel(aId);
|
|
}
|
|
return model;
|
|
}
|
|
|
|
shared_ptr<ZLTextModel> BooksBookModel::contentsModel() const
|
|
{
|
|
shared_ptr<ZLTextModel> model;
|
|
if (iData && !iData->iBookModel.isNull()) {
|
|
model = iData->iBookModel->contentsModel();
|
|
}
|
|
return model;
|
|
}
|
|
|
|
void BooksBookModel::setLeftMargin(int aMargin)
|
|
{
|
|
if (iMargins.iLeft != aMargin) {
|
|
iMargins.iLeft = aMargin;
|
|
HDEBUG(aMargin);
|
|
startReset();
|
|
Q_EMIT leftMarginChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::setRightMargin(int aMargin)
|
|
{
|
|
if (iMargins.iRight != aMargin) {
|
|
iMargins.iRight = aMargin;
|
|
HDEBUG(aMargin);
|
|
startReset();
|
|
Q_EMIT rightMarginChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::setTopMargin(int aMargin)
|
|
{
|
|
if (iMargins.iTop != aMargin) {
|
|
iMargins.iTop = aMargin;
|
|
HDEBUG(aMargin);
|
|
startReset();
|
|
Q_EMIT topMarginChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::setBottomMargin(int aMargin)
|
|
{
|
|
if (iMargins.iBottom != aMargin) {
|
|
iMargins.iBottom = aMargin;
|
|
HDEBUG(aMargin);
|
|
startReset();
|
|
Q_EMIT bottomMarginChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::updateModel(int aPrevPageCount)
|
|
{
|
|
const int newPageCount = pageCount();
|
|
if (aPrevPageCount != newPageCount) {
|
|
if (newPageCount > aPrevPageCount) {
|
|
beginInsertRows(QModelIndex(), aPrevPageCount, newPageCount-1);
|
|
endInsertRows();
|
|
} else {
|
|
beginRemoveRows(QModelIndex(), newPageCount, aPrevPageCount-1);
|
|
endRemoveRows();
|
|
}
|
|
Q_EMIT pageCountChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::setSize(QSize aSize)
|
|
{
|
|
if (iSize != aSize) {
|
|
iSize = aSize;
|
|
const int w = width();
|
|
const int h = height();
|
|
HDEBUG(aSize);
|
|
if (iData && iData->iWidth == w && iData->iHeight == h) {
|
|
HDEBUG("size didn't change");
|
|
} else if (iData2 && iData2->iWidth == w && iData2->iHeight == h) {
|
|
HDEBUG("switching to backup layout");
|
|
const int oldModelPageCount = pageCount();
|
|
int oldPageCount;
|
|
BooksPos page1, page2;
|
|
if (iTask) {
|
|
// Layout has been switched back before the paging task
|
|
// has completed
|
|
HDEBUG("not so fast please...");
|
|
oldPageCount = iTask->iOldPageCount;
|
|
page1 = iTask->iPagePos;
|
|
page2 = iTask->iNextPagePos;
|
|
} else {
|
|
oldPageCount = oldModelPageCount;
|
|
page1 = pageMark(iCurrentPage);
|
|
page2 = pageMark(iCurrentPage+1);
|
|
}
|
|
Data* tmp = iData;
|
|
iData = iData2;
|
|
iData2 = tmp;
|
|
if (iData) {
|
|
// Cancel unnecessary paging task
|
|
if (iTask) {
|
|
BooksLoadingSignalBlocker block(this);
|
|
iTask->release(this);
|
|
iTask = NULL;
|
|
}
|
|
updateModel(oldModelPageCount);
|
|
Q_EMIT pageMarksChanged();
|
|
Q_EMIT jumpToPage(iData->pickPage(page1, page2, oldPageCount));
|
|
} else {
|
|
startReset(ReasonUnknown, false);
|
|
}
|
|
} else {
|
|
startReset(ReasonUnknown, false);
|
|
}
|
|
Q_EMIT sizeChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::onTextStyleChanged()
|
|
{
|
|
HDEBUG(iTitle);
|
|
shared_ptr<ZLTextStyle> newStyle = iSettings->textStyle(fontSizeAdjust());
|
|
const int newFontSize = newStyle->fontSize();
|
|
const int oldFontSize = iTextStyle->fontSize();
|
|
const ResetReason reason =
|
|
(newFontSize > oldFontSize) ? ReasonIncreasingFontSize :
|
|
(newFontSize < oldFontSize) ? ReasonDecreasingFontSize :
|
|
ReasonUnknown;
|
|
iTextStyle = newStyle;
|
|
startReset(reason);
|
|
Q_EMIT textStyleChanged();
|
|
}
|
|
|
|
void BooksBookModel::startReset(ResetReason aResetReason, bool aFullReset)
|
|
{
|
|
BooksLoadingSignalBlocker block(this);
|
|
const BooksPos thisPage = pageMark(iCurrentPage);
|
|
const BooksPos nextPage = pageMark(iCurrentPage+1);
|
|
if (aResetReason == ReasonUnknown) {
|
|
if (iResetReason == ReasonUnknown) {
|
|
if (!iData && !iData2) {
|
|
aResetReason = ReasonLoading;
|
|
}
|
|
} else {
|
|
aResetReason = iResetReason;
|
|
}
|
|
}
|
|
if (iTask) {
|
|
iTask->release(this);
|
|
iTask = NULL;
|
|
}
|
|
const int oldPageCount(pageCount());
|
|
if (oldPageCount > 0) {
|
|
beginResetModel();
|
|
}
|
|
|
|
delete iData2;
|
|
if (aFullReset) {
|
|
delete iData;
|
|
iData2 = NULL;
|
|
} else {
|
|
iData2 = iData;
|
|
}
|
|
iData = NULL;
|
|
|
|
if (iBook && width() > 0 && height() > 0) {
|
|
HDEBUG("starting" << qPrintable(QString("%1x%2").arg(width()).
|
|
arg(height())) << "paging");
|
|
iTask = new Task(this, iBook->bookRef(), thisPage, nextPage,
|
|
iBook->lastPos(), oldPageCount);
|
|
iTaskQueue->submit(iTask);
|
|
}
|
|
|
|
if (oldPageCount > 0) {
|
|
endResetModel();
|
|
Q_EMIT pageMarksChanged();
|
|
Q_EMIT pageCountChanged();
|
|
}
|
|
|
|
if (iCurrentPage != 0) {
|
|
iCurrentPage = 0;
|
|
Q_EMIT currentPageChanged();
|
|
}
|
|
|
|
if (iProgress != 0) {
|
|
iProgress = 0;
|
|
Q_EMIT progressChanged();
|
|
}
|
|
|
|
if (iResetReason != aResetReason) {
|
|
iResetReason = aResetReason;
|
|
Q_EMIT resetReasonChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::onResetProgress(int aProgress)
|
|
{
|
|
// progress -> onResetProgress is a queued connection, we may received
|
|
// this event from the task that has already been canceled.
|
|
if (iTask == sender() && aProgress > iProgress) {
|
|
iProgress = aProgress;
|
|
Q_EMIT progressChanged();
|
|
}
|
|
}
|
|
|
|
void BooksBookModel::onResetDone()
|
|
{
|
|
HASSERT(sender() == iTask);
|
|
HASSERT(iTask->iData);
|
|
HASSERT(!iData);
|
|
|
|
const int oldPageCount(pageCount());
|
|
shared_ptr<BookModel> oldBookModel(bookModel());
|
|
BooksLoadingSignalBlocker block(this);
|
|
int page = iTask->iPage;
|
|
|
|
iData = iTask->iData;
|
|
iTask->iData = NULL;
|
|
iTask->release(this);
|
|
iTask = NULL;
|
|
|
|
updateModel(oldPageCount);
|
|
Q_EMIT jumpToPage(page);
|
|
Q_EMIT pageMarksChanged();
|
|
if (oldBookModel != bookModel()) {
|
|
Q_EMIT bookModelChanged();
|
|
}
|
|
if (iResetReason != ReasonUnknown) {
|
|
iResetReason = ReasonUnknown;
|
|
Q_EMIT resetReasonChanged();
|
|
}
|
|
}
|
|
|
|
QHash<int,QByteArray> BooksBookModel::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
roles.insert(BooksBookModelPageIndex, "pageIndex");
|
|
return roles;
|
|
}
|
|
|
|
int BooksBookModel::rowCount(const QModelIndex&) const
|
|
{
|
|
return pageCount();
|
|
}
|
|
|
|
QVariant BooksBookModel::data(const QModelIndex& aIndex, int aRole) const
|
|
{
|
|
const int i = aIndex.row();
|
|
if (i >= 0 && i < pageCount()) {
|
|
switch (aRole) {
|
|
case BooksBookModelPageIndex: return i;
|
|
}
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
#include "BooksBookModel.moc"
|