Implemented history (position stack)

Allows the user to return back after selecting a cross-page link
This commit is contained in:
Slava Monich 2017-08-03 18:48:17 +03:00
parent 59f49be0c3
commit 9ac726c523
17 changed files with 1183 additions and 329 deletions

View file

@ -109,6 +109,7 @@ SOURCES += \
src/BooksImportModel.cpp \
src/BooksListWatcher.cpp \
src/BooksLoadingProperty.cpp \
src/BooksPageStack.cpp \
src/BooksPageWidget.cpp \
src/BooksPaintContext.cpp \
src/BooksPathModel.cpp \
@ -150,6 +151,7 @@ HEADERS += \
src/BooksItem.h \
src/BooksListWatcher.h \
src/BooksLoadingProperty.h \
src/BooksPageStack.h \
src/BooksPageWidget.h \
src/BooksPaintContext.h \
src/BooksPathModel.h \

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2015-2016 Jolla Ltd.
Copyright (C) 2015-2017 Jolla Ltd.
Contact: Slava Monich <slava.monich@jolla.com>
You may use this file under the terms of BSD license as follows:
@ -43,8 +43,8 @@ SilicaFlickable {
signal pageClicked(var page)
property int orientation: Orientation.Portrait
property int _currentPage: bookListWatcher.currentIndex
property bool _loading: minLoadingDelay.running || bookModel.loading
property alias stackModel: bookModel.pageStack
property bool loading: bookModel.loading
property var _currentState: _visibilityStates[Settings.pageDetails % _visibilityStates.length]
readonly property var _visibilityStates: [
{ pager: false, page: false, title: false, tools: false },
@ -52,18 +52,6 @@ SilicaFlickable {
{ pager: true, page: true, title: true, tools: true }
]
// NOTE: These have to match ResetReason in BooksBookModel
readonly property var _loadingTextLabel: [
//% "Formatting..."
qsTrId("harbour-books-book-view-formatting"),
//% "Loading..."
qsTrId("harbour-books-book-view-loading"),
//% "Applying larger fonts..."
qsTrId("harbour-books-book-view-applying_larger_fonts"),
//% "Applying smaller fonts..."
qsTrId("harbour-books-book-view-applying_smaller_fonts")
]
interactive: (!linkMenu || !linkMenu.visible) &&
(!imageView || !imageView.visible) &&
(!footnoteView || !footnoteView.visible)
@ -101,49 +89,14 @@ SilicaFlickable {
}
}
Timer {
id: minLoadingDelay
interval: 1000
}
Timer {
id: resetPager
interval: 0
onTriggered: {
if (_currentPage >= 0) {
console.log("resetting pager to", _currentPage)
pager.currentPage = _currentPage
}
}
}
BookModel {
id: bookModel
book: root.book ? root.book : null
size: bookListWatcher.size
currentPage: _currentPage
size: bookViewWatcher.size
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
topMargin: Theme.itemSizeSmall
bottomMargin: Theme.itemSizeSmall
onJumpToPage: bookView.jumpTo(index)
onCurrentPageChanged: {
if (linkMenu) linkMenu.hide()
if (currentPage >= 0 && bookView._jumpingTo < 0) {
pager.currentPage = currentPage
}
}
onLoadingChanged: {
if (loading && !pageCount) {
minLoadingDelay.start()
bookView._jumpingTo = -1
}
}
}
ListWatcher {
id: bookListWatcher
listView: bookView
}
SilicaListView {
@ -154,9 +107,50 @@ SilicaFlickable {
orientation: ListView.Horizontal
snapMode: ListView.SnapOneItem
spacing: Theme.paddingMedium
opacity: _loading ? 0 : 1
opacity: loading ? 0 : 1
visible: opacity > 0
interactive: root.interactive
readonly property int currentPage: stackModel.currentPage
property bool completed
Component.onCompleted: {
//console.log(currentPage)
bookViewWatcher.positionViewAtIndex(currentPage)
completed = true
}
onCurrentPageChanged: {
//console.log(currentPage, completed, flicking)
if (completed && !flicking) {
bookViewWatcher.positionViewAtIndex(currentPage)
}
}
onFlickingChanged: {
if (!flicking) {
bookViewWatcher.updateModel()
}
}
ListWatcher {
id: bookViewWatcher
listView: bookView
onCurrentIndexChanged: {
if (listView.completed && !listView.flicking && currentIndex >= 0) {
//console.log(currentIndex, listView.completed, listView.flicking)
updateModel()
}
}
function updateModel() {
if (linkMenu) linkMenu.hide()
//console.trace()
stackModel.currentPage = currentIndex
if (!pager.pressed) {
pager.currentPage = currentIndex
}
}
}
delegate: BooksPageView {
width: bookView.width
height: bookView.height
@ -172,12 +166,13 @@ SilicaFlickable {
pageNumberVisible: _currentState.page
title: bookModel.title
onJumpToPage: bookView.jumpTo(page)
onPushPosition: stackModel.pushPosition(position) // bookView.jumpTo(page)
onPageClicked: {
root.pageClicked(index)
Settings.pageDetails = (Settings.pageDetails + 1) % _visibilityStates.length
}
onImagePressed: {
if (_currentPage == index) {
if (bookViewWatcher.currentIndex == index) {
if (!imageView) {
imageView = imageViewComponent.createObject(root)
}
@ -185,7 +180,7 @@ SilicaFlickable {
}
}
onBrowserLinkPressed: {
if (_currentPage == index) {
if (bookViewWatcher.currentIndex == index) {
if (!linkMenu) {
linkMenu = linkMenuComponent.createObject(root)
}
@ -193,7 +188,7 @@ SilicaFlickable {
}
}
onFootnotePressed: {
if (_currentPage == index) {
if (bookViewWatcher.currentIndex == index) {
if (!footnoteView) {
footnoteView = footnoteViewComponent.createObject(root)
}
@ -202,15 +197,15 @@ SilicaFlickable {
}
}
property int _jumpingTo: -1
property int jumpingTo: -1
function jumpTo(page) {
if (page >=0 && page !== _currentPage) {
_jumpingTo = page
positionViewAtIndex(page, ListView.Center)
if (page >=0 && page !== bookViewWatcher.currentIndex) {
jumpingTo = page
bookViewWatcher.positionViewAtIndex(page)
pager.currentPage = page
_jumpingTo = -1
if (_currentPage !== page) {
console.log("oops, still at", _currentPage)
jumpingTo = -1
if (bookViewWatcher.currentIndex !== page) {
console.log("oops, still at", currentPage)
resetPager.restart()
}
}
@ -218,6 +213,15 @@ SilicaFlickable {
Behavior on opacity { FadeAnimation {} }
Timer {
id: resetPager
interval: 0
onTriggered: {
console.log("resetting pager to", bookViewWatcher.currentIndex)
pager.currentPage = bookViewWatcher.currentIndex
}
}
BooksPageTools {
id: pageTools
anchors {
@ -228,7 +232,7 @@ SilicaFlickable {
leftMargin: bookModel.leftMargin
rightMargin: bookModel.rightMargin
opacity: _currentState.tools ? 1 : 0
visible: opacity > 0 && book && bookModel.pageCount && !_loading
visible: opacity > 0 && book && bookModel.pageCount && !loading
Behavior on opacity { FadeAnimation {} }
onIncreaseFontSize: bookModel.increaseFontSize()
onDecreaseFontSize: bookModel.decreaseFontSize()
@ -237,9 +241,14 @@ SilicaFlickable {
BooksPager {
id: pager
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
bottomMargin: (Theme.itemSizeExtraSmall + 2*(bookModel.bottomMargin - height))/4
}
leftMargin: bookModel.leftMargin
rightMargin: bookModel.rightMargin
stack: stackModel
pageCount: bookModel.pageCount
width: parent.width
opacity: (_currentState.pager && book && bookModel.pageCount) ? 0.75 : 0
@ -261,20 +270,20 @@ SilicaFlickable {
text: bookModel.title
height: Theme.itemSizeExtraSmall
color: Theme.highlightColor
opacity: _loading ? 0.6 : 0
opacity: loading ? 0.6 : 0
}
BusyIndicator {
id: busyIndicator
anchors.centerIn: parent
size: BusyIndicatorSize.Large
running: _loading
running: loading
}
BooksFitLabel {
anchors.fill: busyIndicator
text: bookModel.progress > 0 ? bookModel.progress : ""
opacity: (_loading && bookModel.progress > 0) ? 1 : 0
opacity: (loading && bookModel.progress > 0) ? 1 : 0
}
Button {
@ -286,7 +295,7 @@ SilicaFlickable {
horizontalCenter: parent.horizontalCenter
}
onClicked: root.closeBook()
enabled: _loading && bookModel.resetReason === BookModel.ReasonLoading
enabled: loading && bookModel.resetReason === BookModel.ReasonLoading
visible: opacity > 0
opacity: enabled ? 1.0 : 0.0
Behavior on opacity { FadeAnimation { } }
@ -301,9 +310,19 @@ SilicaFlickable {
}
horizontalAlignment: Text.AlignHCenter
color: Theme.highlightColor
opacity: _loading ? 1 : 0
opacity: loading ? 1 : 0
visible: opacity > 0
Behavior on opacity { FadeAnimation {} }
text: bookModel ? _loadingTextLabel[bookModel.resetReason] : ""
text: bookModel ? (bookModel.resetReason == BookModel.ReasonLoading ?
//% "Loading..."
qsTrId("harbour-books-book-view-loading") :
bookModel.resetReason == BookModel.ReasonIncreasingFontSize ?
//% "Applying larger fonts..."
qsTrId("harbour-books-book-view-applying_larger_fonts") :
bookModel.resetReason == BookModel.ReasonDecreasingFontSize ?
//% "Applying smaller fonts..."
qsTrId("harbour-books-book-view-applying_smaller_fonts") :
//% "Formatting..."
qsTrId("harbour-books-book-view-formatting")) : ""
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2015-2016 Jolla Ltd.
Copyright (C) 2015-2017 Jolla Ltd.
Contact: Slava Monich <slava.monich@jolla.com>
You may use this file under the terms of BSD license as follows:
@ -54,6 +54,7 @@ Item {
signal footnotePressed(var touchX, var touchY, var text, var url)
signal browserLinkPressed(var url)
signal jumpToPage(var page)
signal pushPosition(var position)
PageWidget {
id: widget
@ -63,6 +64,7 @@ Item {
onImagePressed: view.imagePressed(imageId, rect)
onActiveTouch: pressImage.animate(touchX, touchY)
onJumpToPage: view.jumpToPage(page)
onPushPosition: view.pushPosition(position)
onShowFootnote: view.footnotePressed(touchX,touchY,text,imageId)
}

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2015-2016 Jolla Ltd.
Copyright (C) 2015-2017 Jolla Ltd.
Contact: Slava Monich <slava.monich@jolla.com>
You may use this file under the terms of BSD license as follows:
@ -38,29 +38,82 @@ Item {
id: root
height: slider.height
property alias pageCount: slider.maximumValue
property var stack
property int pageCount
property real leftMargin: Theme.horizontalPageMargin
property real rightMargin: Theme.horizontalPageMargin
property alias currentPage: slider.value
property alias pressed: slider.pressed
property alias leftMargin: slider.leftMargin
property alias rightMargin: slider.rightMargin
signal pageChanged(var page)
MouseArea {
id: navigateBackArea
property bool down: pressed && containsMouse
width: navigateBack.width + root.leftMargin
height: navigateBack.height
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
onClicked: stack.back()
}
IconButton {
id: navigateBack
icon.source: "image://theme/icon-m-left?" + Settings.primaryPageToolColor
down: navigateBackArea.down || (pressed && containsMouse)
anchors {
left: parent.left
leftMargin: root.leftMargin
verticalCenter: parent.verticalCenter
}
onClicked: stack.back()
}
BooksPageSlider {
id: slider
anchors {
left: parent.left
right: parent.right
left: navigateBack.right
right: navigateForwardArea.left
bottom: parent.bottom
}
stepSize: 1
minimumValue: 0
maximumValue: pageCount > 0 ? pageCount - 1 : 0
valueText: ""
label: ""
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
primaryColor: Settings.primaryPageToolColor
secondaryColor: Settings.primaryPageToolColor
highlightColor: Settings.highlightPageToolColor
secondaryHighlightColor: Settings.highlightPageToolColor
onSliderValueChanged: root.pageChanged(value)
}
MouseArea {
id: navigateForwardArea
property bool down: pressed && containsMouse
width: navigateForward.width + root.rightMargin
height: navigateForward.height
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
onClicked: stack.forward()
}
IconButton {
id: navigateForward
icon.source: "image://theme/icon-m-right?" + Settings.primaryPageToolColor
down: navigateForwardArea.down || (pressed && containsMouse)
anchors {
right: parent.right
rightMargin: root.rightMargin
verticalCenter: parent.verticalCenter
}
onClicked: stack.forward()
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -14,7 +14,7 @@
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* * 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.
*
@ -58,6 +58,7 @@
#include <unistd.h>
#include <errno.h>
#define BOOK_STATE_PAGE_STACK_INDEX "pageStackIndex"
#define BOOK_STATE_FONT_SIZE_ADJUST "fontSizeAdjust"
#define BOOK_STATE_POSITION "position"
#define BOOK_COVER_SUFFIX ".cover."
@ -328,10 +329,40 @@ BooksBook::BooksBook(const BooksStorage& aStorage, QString aRelativePath,
// Load the state
QVariantMap state;
if (HarbourJson::load(iStateFilePath, state)) {
iLastPos = BooksPos::fromVariant(state.value(BOOK_STATE_POSITION));
iFontSizeAdjust = state.value(BOOK_STATE_FONT_SIZE_ADJUST).toInt();
#ifdef BOOK_STATE_PAGE_STACK_INDEX
iPageStackPos = state.value(BOOK_STATE_PAGE_STACK_INDEX).toInt();
#endif
// Current position can be stored in two formats - a single
// position (older format) or a list of position (newer one).
// We have to detect which one we are dealing with
QVariant position(state.value(BOOK_STATE_POSITION));
BooksPos bookPos(BooksPos::fromVariant(position));
if (bookPos.valid()) {
// Old format (single position)
iPageStack.append(bookPos);
} else {
// New format (list of positions)
QVariantList list(position.toList());
const int count = list.count();
for (int k=0; k<count; k++) {
bookPos = BooksPos::fromVariant(list.at(k));
if (bookPos.valid()) {
iPageStack.append(bookPos);
}
}
}
}
}
// Validate the state
if (iPageStack.isEmpty()) {
iPageStack.append(BooksPos(0,0,0));
}
if (iPageStackPos < 0) {
iPageStackPos = 0;
} else if (iPageStackPos >= iPageStack.count()) {
iPageStackPos = iPageStack.count() - 1;
}
// Refcounted BooksBook objects are managed by C++ code
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
}
@ -339,6 +370,7 @@ BooksBook::BooksBook(const BooksStorage& aStorage, QString aRelativePath,
void BooksBook::init()
{
iFontSizeAdjust = 0;
iPageStackPos = 0;
iCoverTask = NULL;
iCoverTasksDone = false;
iCopyingOut = false;
@ -419,10 +451,23 @@ bool BooksBook::setFontSizeAdjust(int aFontSizeAdjust)
}
}
void BooksBook::setLastPos(const BooksPos& aPos)
void BooksBook::setPageStack(BooksPos::List aStack, int aStackPos)
{
if (iLastPos != aPos) {
iLastPos = aPos;
if (aStackPos < 0) {
aStackPos = 0;
} else if (aStackPos >= aStack.count()) {
aStackPos = aStack.count() - 1;
}
bool changed = false;
if (iPageStack != aStack) {
iPageStack = aStack;
changed = true;
}
if (iPageStackPos != aStackPos) {
iPageStackPos = aStackPos;
changed = true;
}
if (changed) {
requestSave();
}
}
@ -536,8 +581,14 @@ void BooksBook::saveState()
if (!iStateFilePath.isEmpty()) {
QVariantMap state;
HarbourJson::load(iStateFilePath, state);
state.insert(BOOK_STATE_POSITION, iLastPos.toVariant());
QVariantList positions;
const int n = iPageStack.count();
for (int i=0; i<n; i++) positions.append(iPageStack.at(i).toVariant());
state.insert(BOOK_STATE_POSITION, positions);
state.insert(BOOK_STATE_FONT_SIZE_ADJUST, iFontSizeAdjust);
#ifdef BOOK_STATE_PAGE_STACK_INDEX
state.insert(BOOK_STATE_PAGE_STACK_INDEX, iPageStackPos);
#endif
if (HarbourJson::save(iStateFilePath, state)) {
HDEBUG("wrote" << iStateFilePath);
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -14,7 +14,7 @@
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* * 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.
*
@ -74,16 +74,17 @@ public:
static BooksBook* newBook(const BooksStorage& aStorage, QString aRelPath,
QString aFileName);
QString title() const { return iTitle; }
QString authors() const { return iAuthors; }
int fontSizeAdjust() const { return iFontSizeAdjust; }
QString title() const;
QString authors() const;
int fontSizeAdjust() const;
bool setFontSizeAdjust(int aFontSizeAdjust);
BooksPos lastPos() const { return iLastPos; }
void setLastPos(const BooksPos& aPos);
shared_ptr<Book> bookRef() const { return iBook; }
int pageStackPos() const;
BooksPos::List pageStack() const;
void setPageStack(BooksPos::List aStack, int aStackPos);
shared_ptr<Book> bookRef() const;
bool copyingOut() const { return iCopyingOut; }
bool loadingCover() const { return !iCoverTasksDone; }
bool copyingOut() const;
bool loadingCover() const;
bool hasCoverImage() const;
bool requestCoverImage();
void cancelCoverRequest();
@ -129,7 +130,8 @@ private:
private:
QAtomicInt iRef;
int iFontSizeAdjust;
BooksPos iLastPos;
int iPageStackPos;
BooksPos::List iPageStack;
BooksStorage iStorage;
shared_ptr<Book> iBook;
QImage iCoverImage;
@ -149,6 +151,22 @@ private:
QML_DECLARE_TYPE(BooksBook)
inline QString BooksBook::title() const
{ return iTitle; }
inline QString BooksBook::authors() const
{ return iAuthors; }
inline int BooksBook::fontSizeAdjust() const
{ return iFontSizeAdjust; }
inline int BooksBook::pageStackPos() const
{ return iPageStackPos; }
inline BooksPos::List BooksBook::pageStack() const
{ return iPageStack; }
inline shared_ptr<Book> BooksBook::bookRef() const
{ return iBook; }
inline bool BooksBook::copyingOut() const
{ return iCopyingOut; }
inline bool BooksBook::loadingCover() const
{ return !iCoverTasksDone; }
inline bool BooksBook::isCanceled(CopyOperation* aObserver)
{ return aObserver && aObserver->isCanceled(); }
inline QImage BooksBook::coverImage() const

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -45,10 +45,6 @@ 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;
@ -56,65 +52,17 @@ public:
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
// BooksBookModel::PagingTask
// ==========================================================================
class BooksBookModel::Task : public BooksTask
class BooksBookModel::PagingTask : public BooksTask
{
Q_OBJECT
public:
Task(BooksBookModel* aReceiver, shared_ptr<Book> aBook,
const BooksPos& aPagePos, const BooksPos& aNextPagePos,
const BooksPos& aLastPos, int aPageCount);
~Task();
PagingTask(BooksBookModel* aReceiver, shared_ptr<Book> aBook);
~PagingTask();
void performTask();
@ -127,38 +75,27 @@ public:
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) :
BooksBookModel::PagingTask::PagingTask(BooksBookModel* aModel,
shared_ptr<Book> aBook) :
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)
iData(NULL)
{
aModel->connect(this, SIGNAL(done()), SLOT(onResetDone()));
aModel->connect(this, SIGNAL(progress(int)), SLOT(onResetProgress(int)),
Qt::QueuedConnection);
}
BooksBookModel::Task::~Task()
BooksBookModel::PagingTask::~PagingTask()
{
delete iData;
}
void BooksBookModel::Task::performTask()
void BooksBookModel::PagingTask::performTask()
{
if (!isCanceled()) {
iData = new BooksBookModel::Data(iPaint.width(), iPaint.height());
@ -183,9 +120,6 @@ void BooksBookModel::Task::performTask()
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");
@ -203,17 +137,19 @@ enum BooksBookModelRole {
BooksBookModel::BooksBookModel(QObject* aParent) :
QAbstractListModel(aParent),
iResetReason(ReasonUnknown),
iCurrentPage(-1),
iProgress(0),
iBook(NULL),
iTask(NULL),
iPagingTask(NULL),
iData(NULL),
iData2(NULL),
iSettings(BooksSettings::sharedInstance()),
iTaskQueue(BooksTaskQueue::defaultQueue())
iTaskQueue(BooksTaskQueue::defaultQueue()),
iPageStack(new BooksPageStack(this))
{
iTextStyle = iSettings->textStyle(fontSizeAdjust());
connect(iSettings.data(), SIGNAL(textStyleChanged()), SLOT(onTextStyleChanged()));
connect(iPageStack, SIGNAL(changed()), SLOT(onPageStackChanged()));
connect(iPageStack, SIGNAL(currentIndexChanged()), SLOT(onPageStackChanged()));
HDEBUG("created");
#if QT_VERSION < 0x050000
setRoleNames(roleNames());
@ -222,7 +158,7 @@ BooksBookModel::BooksBookModel(QObject* aParent) :
BooksBookModel::~BooksBookModel()
{
if (iTask) iTask->release(this);
if (iPagingTask) iPagingTask->release(this);
if (iBook) {
iBook->disconnect(this);
iBook->release();
@ -235,7 +171,6 @@ BooksBookModel::~BooksBookModel()
void BooksBookModel::setBook(BooksBook* aBook)
{
shared_ptr<Book> oldBook;
shared_ptr<Book> newBook;
if (iBook != aBook) {
const QString oldTitle(iTitle);
@ -246,14 +181,16 @@ void BooksBookModel::setBook(BooksBook* aBook)
if (aBook) {
(iBook = aBook)->retain();
iBookRef = newBook;
iTitle = iBook->title();
iTitle = aBook->title();
iTextStyle = iSettings->textStyle(fontSizeAdjust());
connect(iBook, SIGNAL(fontSizeAdjustChanged()), SLOT(onTextStyleChanged()));
iPageStack->setStack(aBook->pageStack(), aBook->pageStackPos());
connect(aBook, SIGNAL(fontSizeAdjustChanged()), SLOT(onTextStyleChanged()));
HDEBUG(iTitle);
} else {
iBook = NULL;
iBookRef.reset();
iTitle = QString();
iPageStack->clear();
HDEBUG("<none>");
}
startReset(ReasonLoading, true);
@ -268,7 +205,7 @@ void BooksBookModel::setBook(BooksBook* aBook)
bool BooksBookModel::loading() const
{
return (iTask != NULL);
return (iPagingTask != NULL);
}
bool BooksBookModel::increaseFontSize()
@ -281,19 +218,12 @@ bool BooksBookModel::decreaseFontSize()
return iBook && iBook->setFontSizeAdjust(iBook->fontSizeAdjust()-1);
}
void BooksBookModel::setCurrentPage(int aPage)
void BooksBookModel::onPageStackChanged()
{
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();
if (iBook) {
BooksPos::Stack stack = iPageStack->getStack();
HDEBUG(stack.iList << stack.iPos);
iBook->setPageStack(stack.iList, stack.iPos);
}
}
@ -314,24 +244,18 @@ int BooksBookModel::fontSizeAdjust() const
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();
return iData ? BooksPos::posAt(iData->iPageMarks, aPage) : BooksPos();
}
int BooksBookModel::linkToPage(const std::string& aLink) const
BooksPos BooksBookModel::linkPosition(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 BooksPos(label.ParagraphNumber, 0, 0);
}
}
return -1;
return BooksPos();
}
shared_ptr<BookModel> BooksBookModel::bookModel() const
@ -410,6 +334,7 @@ void BooksBookModel::updateModel(int aPrevPageCount)
{
const int newPageCount = pageCount();
if (aPrevPageCount != newPageCount) {
HDEBUG(aPrevPageCount << "->" << newPageCount);
if (newPageCount > aPrevPageCount) {
beginInsertRows(QModelIndex(), aPrevPageCount, newPageCount-1);
endInsertRows();
@ -433,36 +358,20 @@ void BooksBookModel::setSize(QSize aSize)
} 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;
if (iPagingTask) {
HDEBUG("not so fast please...");
iPagingTask->release(this);
iPagingTask = NULL;
}
updateModel(oldModelPageCount);
iPageStack->setPageMarks(iData->iPageMarks);
Q_EMIT pageMarksChanged();
Q_EMIT jumpToPage(iData->pickPage(page1, page2, oldPageCount));
} else {
startReset(ReasonUnknown, false);
}
Q_EMIT jumpToPage(iPageStack->currentPage());
} else {
startReset(ReasonUnknown, false);
}
@ -487,9 +396,8 @@ void BooksBookModel::onTextStyleChanged()
void BooksBookModel::startReset(ResetReason aResetReason, bool aFullReset)
{
BooksPos dummy;
BooksLoadingSignalBlocker block(this);
const BooksPos thisPage = pageMark(iCurrentPage);
const BooksPos nextPage = pageMark(iCurrentPage+1);
if (aResetReason == ReasonUnknown) {
if (iResetReason == ReasonUnknown) {
if (!iData && !iData2) {
@ -499,9 +407,9 @@ void BooksBookModel::startReset(ResetReason aResetReason, bool aFullReset)
aResetReason = iResetReason;
}
}
if (iTask) {
iTask->release(this);
iTask = NULL;
if (iPagingTask) {
iPagingTask->release(this);
iPagingTask = NULL;
}
const int oldPageCount(pageCount());
if (oldPageCount > 0) {
@ -520,9 +428,8 @@ void BooksBookModel::startReset(ResetReason aResetReason, bool aFullReset)
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);
iPagingTask = new PagingTask(this, iBook->bookRef());
iTaskQueue->submit(iPagingTask);
}
if (oldPageCount > 0) {
@ -531,11 +438,6 @@ void BooksBookModel::startReset(ResetReason aResetReason, bool aFullReset)
Q_EMIT pageCountChanged();
}
if (iCurrentPage != 0) {
iCurrentPage = 0;
Q_EMIT currentPageChanged();
}
if (iProgress != 0) {
iProgress = 0;
Q_EMIT progressChanged();
@ -551,7 +453,7 @@ 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) {
if (iPagingTask == sender() && aProgress > iProgress) {
iProgress = aProgress;
Q_EMIT progressChanged();
}
@ -559,22 +461,22 @@ void BooksBookModel::onResetProgress(int aProgress)
void BooksBookModel::onResetDone()
{
HASSERT(sender() == iTask);
HASSERT(iTask->iData);
HASSERT(sender() == iPagingTask);
HASSERT(iPagingTask->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;
iData = iPagingTask->iData;
iPagingTask->iData = NULL;
iPagingTask->release(this);
iPagingTask = NULL;
updateModel(oldPageCount);
Q_EMIT jumpToPage(page);
iPageStack->setPageMarks(iData->iPageMarks);
Q_EMIT jumpToPage(iPageStack->currentPage());
Q_EMIT pageMarksChanged();
if (oldBookModel != bookModel()) {
Q_EMIT bookModelChanged();

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -42,6 +42,7 @@
#include "BooksPos.h"
#include "BooksPaintContext.h"
#include "BooksLoadingProperty.h"
#include "BooksPageStack.h"
#include "ZLTextStyle.h"
#include "bookmodel/BookModel.h"
@ -63,11 +64,11 @@ class BooksBookModel: public QAbstractListModel, private BooksLoadingProperty
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged)
Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged)
Q_PROPERTY(int leftMargin READ leftMargin WRITE setLeftMargin NOTIFY leftMarginChanged)
Q_PROPERTY(int rightMargin READ rightMargin WRITE setRightMargin NOTIFY rightMarginChanged)
Q_PROPERTY(int topMargin READ topMargin WRITE setTopMargin NOTIFY topMarginChanged)
Q_PROPERTY(int bottomMargin READ bottomMargin WRITE setBottomMargin NOTIFY bottomMarginChanged)
Q_PROPERTY(BooksPageStack* pageStack READ pageStack CONSTANT)
Q_PROPERTY(BooksBook* book READ book WRITE setBook NOTIFY bookChanged)
Q_PROPERTY(ResetReason resetReason READ resetReason NOTIFY resetReasonChanged)
@ -88,25 +89,23 @@ public:
bool loading() const;
int pageCount() const;
int progress() const { return iProgress; }
QString title() const { return iTitle; }
int width() const { return iSize.width(); }
int height() const { return iSize.height(); }
ResetReason resetReason() const { return iResetReason; }
int progress() const;
QString title() const;
int width() const;
int height() const;
ResetReason resetReason() const;
BooksPageStack* pageStack() const;
QSize size() const { return iSize; }
QSize size() const;
void setSize(QSize aSize);
int currentPage() const { return iCurrentPage; }
void setCurrentPage(int aPage);
BooksBook* book() const { return iBook; }
BooksBook* book() const;
void setBook(BooksBook* aBook);
int leftMargin() const { return iMargins.iLeft; }
int rightMargin() const { return iMargins.iRight; }
int topMargin() const { return iMargins.iTop; }
int bottomMargin() const { return iMargins.iBottom; }
int leftMargin() const;
int rightMargin() const;
int topMargin() const;
int bottomMargin() const;
void setLeftMargin(int aMargin);
void setRightMargin(int aMargin);
@ -115,14 +114,14 @@ public:
BooksPos::List pageMarks() const;
BooksPos pageMark(int aPage) const;
BooksMargins margins() const { return iMargins; }
shared_ptr<Book> bookRef() const { return iBookRef; }
BooksMargins margins() const;
shared_ptr<Book> bookRef() const;
shared_ptr<BookModel> bookModel() const;
shared_ptr<ZLTextModel> bookTextModel() const;
shared_ptr<ZLTextModel> contentsModel() const;
shared_ptr<ZLTextModel> footnoteModel(const std::string& aId) const;
shared_ptr<ZLTextStyle> textStyle() const { return iTextStyle; }
int linkToPage(const std::string& aLink) const;
shared_ptr<ZLTextStyle> textStyle() const;
BooksPos linkPosition(const std::string& aLink) const;
int fontSizeAdjust() const;
// QAbstractListModel
@ -139,6 +138,7 @@ private Q_SLOTS:
void onResetProgress(int aProgress);
void onResetDone();
void onTextStyleChanged();
void onPageStackChanged();
Q_SIGNALS:
void sizeChanged();
@ -149,7 +149,6 @@ Q_SIGNALS:
void pageCountChanged();
void pageMarksChanged();
void progressChanged();
void currentPageChanged();
void leftMarginChanged();
void rightMarginChanged();
void topMarginChanged();
@ -160,24 +159,55 @@ Q_SIGNALS:
private:
class Data;
class Task;
class PagingTask;
ResetReason iResetReason;
int iCurrentPage;
int iProgress;
QSize iSize;
QString iTitle;
BooksMargins iMargins;
BooksBook* iBook;
shared_ptr<Book> iBookRef;
Task* iTask;
PagingTask* iPagingTask;
Data* iData;
Data* iData2;
QSharedPointer<BooksSettings> iSettings;
shared_ptr<BooksTaskQueue> iTaskQueue;
shared_ptr<ZLTextStyle> iTextStyle;
BooksPageStack* iPageStack;
};
QML_DECLARE_TYPE(BooksBookModel)
inline int BooksBookModel::progress() const
{ return iProgress; }
inline QString BooksBookModel::title() const
{ return iTitle; }
inline int BooksBookModel::width() const
{ return iSize.width(); }
inline int BooksBookModel::height() const
{ return iSize.height(); }
inline BooksBookModel::ResetReason BooksBookModel::resetReason() const
{ return iResetReason; }
inline BooksPageStack* BooksBookModel::pageStack() const
{ return iPageStack; }
inline QSize BooksBookModel::size() const
{ return iSize; }
inline BooksBook* BooksBookModel::book() const
{ return iBook; }
inline int BooksBookModel::leftMargin() const
{ return iMargins.iLeft; }
inline int BooksBookModel::rightMargin() const
{ return iMargins.iRight; }
inline int BooksBookModel::topMargin() const
{ return iMargins.iTop; }
inline int BooksBookModel::bottomMargin() const
{ return iMargins.iBottom; }
inline BooksMargins BooksBookModel::margins() const
{ return iMargins; }
inline shared_ptr<Book> BooksBookModel::bookRef() const
{ return iBookRef; }
inline shared_ptr<ZLTextStyle> BooksBookModel::textStyle() const
{ return iTextStyle; }
#endif // BOOKS_BOOK_MODEL_H

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -37,6 +37,8 @@
#define LISTVIEW_CONTENT_X "contentX"
#define LISTVIEW_CONTENT_Y "contentY"
#define LISTVIEW_CONTENT_WIDTH "contentWidth"
#define LISTVIEW_CONTENT_HEIGHT "contentHeight"
#define LISTVIEW_INDEX_AT "indexAt"
#define LISTVIEW_POSITION_VIEW_AT_INDEX "positionViewAtIndex"
@ -48,7 +50,6 @@ BooksListWatcher::BooksListWatcher(QObject* aParent) :
iListView(NULL),
iCenterMode(-1),
iPositionIsChanging(false),
iCanRetry(true),
iResizeTimer(new QTimer(this))
{
iResizeTimer->setSingleShot(true);
@ -63,7 +64,6 @@ void BooksListWatcher::setListView(QQuickItem* aView)
if (iListView) iListView->disconnect(this);
iListView = aView;
iCenterMode = -1;
iCanRetry = true;
if (iListView) {
connect(iListView,
SIGNAL(widthChanged()),
@ -120,6 +120,12 @@ void BooksListWatcher::positionViewAtIndex(int aIndex)
{
if (iListView) {
HDEBUG(aIndex);
doPositionViewAtIndex(aIndex);
}
}
void BooksListWatcher::doPositionViewAtIndex(int aIndex)
{
if (iCenterMode < 0) {
bool ok = false;
const QMetaObject* metaObject = iListView->metaObject();
@ -142,25 +148,20 @@ void BooksListWatcher::positionViewAtIndex(int aIndex)
}
iPositionIsChanging = true;
positionViewAtIndex(aIndex, iCenterMode);
if (iCanRetry) {
// This is probably a bug in QQuickListView - it first calculates
// the item position and then starts instantiating the delegates.
// The very first time the item position always turns out to be
// zero because the average item size isn't known yet. So if we
// are trying to position the list at a non-zero index and instead
// we got positioned at zero, try it again. It doesn't make sense
// to retry more than once though.
// If there are no delegates yet, then the average item size is zero
// and the resulting item position will always turn out to be zero.
// So if we are trying to position the list at a non-zero index and
// instead we got positioned at zero, try it again.
if (aIndex > 0 && getCurrentIndex() == 0) {
// Didn't work from the first try, give it another go
HDEBUG("retrying...");
positionViewAtIndex(aIndex, iCenterMode);
}
iCanRetry = false;
}
iPositionIsChanging = false;
updateCurrentIndex();
}
}
void BooksListWatcher::positionViewAtIndex(int aIndex, int aMode)
{
@ -181,6 +182,16 @@ qreal BooksListWatcher::contentY()
return getRealProperty(LISTVIEW_CONTENT_Y);
}
qreal BooksListWatcher::contentWidth()
{
return getRealProperty(LISTVIEW_CONTENT_WIDTH);
}
qreal BooksListWatcher::contentHeight()
{
return getRealProperty(LISTVIEW_CONTENT_HEIGHT);
}
int BooksListWatcher::getCurrentIndex()
{
if (iListView) {
@ -194,14 +205,30 @@ int BooksListWatcher::getCurrentIndex()
return -1;
}
void BooksListWatcher::updateCurrentIndex()
void BooksListWatcher::tryToRestoreCurrentIndex()
{
HASSERT(!iPositionIsChanging);
const int index = getCurrentIndex();
if (iCurrentIndex != index) {
if (iCurrentIndex >= 0) {
doPositionViewAtIndex(iCurrentIndex);
}
}
}
void BooksListWatcher::updateCurrentIndex()
{
HASSERT(!iPositionIsChanging);
if (contentWidth() > 0 || contentHeight() > 0) {
const int index = getCurrentIndex();
if (iCurrentIndex != index) {
HDEBUG(index);
iCurrentIndex = index;
HDEBUG(index << contentWidth() << "x" << contentHeight());
Q_EMIT currentIndexChanged();
}
} else {
HDEBUG(contentWidth() << "x" << contentHeight());
}
}
void BooksListWatcher::updateSize()
@ -211,6 +238,7 @@ void BooksListWatcher::updateSize()
if (iSize != size) {
const QSize oldSize(iSize);
iSize = size;
tryToRestoreCurrentIndex();
Q_EMIT sizeChanged();
if (oldSize.width() != iSize.width()) {
Q_EMIT widthChanged();
@ -272,6 +300,7 @@ void BooksListWatcher::onContentSizeChanged()
{
HASSERT(sender() == iListView);
if (!iPositionIsChanging) {
tryToRestoreCurrentIndex();
updateCurrentIndex();
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -64,10 +64,14 @@ public:
private:
qreal contentX();
qreal contentY();
qreal contentWidth();
qreal contentHeight();
qreal getRealProperty(const char *name, qreal defaultValue = 0.0);
int getCurrentIndex();
void doPositionViewAtIndex(int aIndex);
void positionViewAtIndex(int aIndex, int aMode);
void updateCurrentIndex();
void tryToRestoreCurrentIndex();
void updateSize();
private Q_SLOTS:
@ -93,7 +97,6 @@ private:
QQuickItem* iListView;
int iCenterMode;
bool iPositionIsChanging;
bool iCanRetry;
QTimer* iResizeTimer;
};

633
app/src/BooksPageStack.cpp Normal file
View file

@ -0,0 +1,633 @@
/*
* Copyright (C) 2016-2017 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 "BooksPageStack.h"
#include "HarbourDebug.h"
#define SignalModelChanged (1 << 0)
#define SignalCountChanged (1 << 1)
#define SignalCurrentIndexChanged (1 << 2)
#define SignalCurrentPageChanged (1 << 3)
// ==========================================================================
// BooksPageStack::Entry
// ==========================================================================
class BooksPageStack::Entry {
public:
BooksPos iPos;
int iPage;
public:
Entry() : iPos(0, 0, 0), iPage(0) {}
Entry(const BooksPos& aPos, int aPage) : iPos(aPos), iPage(aPage) {}
Entry(const Entry& aEntry) : iPos(aEntry.iPos), iPage(aEntry.iPage) {}
const Entry& operator = (const Entry& Entry)
{
iPage = Entry.iPage;
iPos = Entry.iPos;
return *this;
}
bool operator == (const Entry& aEntry) const
{
return iPage == aEntry.iPage && iPos == aEntry.iPos;
}
bool operator != (const Entry& aEntry) const
{
return iPage != aEntry.iPage || iPos != aEntry.iPos;
}
};
// ==========================================================================
// BooksPageStack::Private
// ==========================================================================
class BooksPageStack::Private : public QObject {
Q_OBJECT
public:
QList<Entry> iEntries;
int iCurrentIndex;
int iQueuedSignals;
enum {
MAX_DEPTH = 10
};
private:
BooksPos::List iPageMarks;
BooksPageStack* iModel;
public:
Private(BooksPageStack* aModel);
BooksPos::Stack getStack() const;
bool isValidIndex(int aIndex) const;
void setCurrentIndex(int aIndex);
int currentPage() const;
int pageAt(int aIndex) const;
void setCurrentPage(int aPage);
void setPageAt(int aIndex, int aPage);
void setStack(BooksPos::List aStack, int aCurrentPos);
void setPageMarks(BooksPos::List aPageMarks);
void push(BooksPos aPos, int aPage);
void push(BooksPos);
void push(int aPage);
void pop();
void clear();
void emitQueuedSignals();
private Q_SLOTS:
void onModelChanged();
private:
BooksPos getPosAt(int aIndex) const;
int findPage(BooksPos aPos) const;
int makeIndexValid(int aIndex) const;
void pageChanged(int aIndex);
void checkCurrentIndex(int aLastIndex);
void checkCurrentPage(int aLastCurrentPage);
void checkCount(int aLastCount);
int validateCurrentIndex();
void queueSignals(int aSignals);
};
BooksPageStack::Private::Private(BooksPageStack* aModel) :
QObject(aModel),
iCurrentIndex(0),
iQueuedSignals(0),
iModel(aModel)
{
iEntries.append(Entry());
connect(aModel,
SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)),
SLOT(onModelChanged()));
connect(aModel,
SIGNAL(rowsInserted(QModelIndex,int,int)),
SLOT(onModelChanged()));
connect(aModel,
SIGNAL(rowsRemoved(QModelIndex,int,int)),
SLOT(onModelChanged()));
connect(aModel,
SIGNAL(modelReset()),
SLOT(onModelChanged()));
}
void BooksPageStack::Private::emitQueuedSignals()
{
static const struct SignalInfo {
int signal;
void (BooksPageStack::*fn)();
} signalInfo [] = {
{ SignalModelChanged, &BooksPageStack::changed },
{ SignalCountChanged, &BooksPageStack::countChanged },
{ SignalCurrentIndexChanged, &BooksPageStack::currentIndexChanged },
{ SignalCurrentPageChanged, &BooksPageStack::currentPageChanged}
};
const uint n = sizeof(signalInfo)/sizeof(signalInfo[0]);
for (uint i=0; i<n && iQueuedSignals; i++) {
if (iQueuedSignals & signalInfo[i].signal) {
iQueuedSignals &= ~signalInfo[i].signal;
Q_EMIT (iModel->*(signalInfo[i].fn))();
}
}
}
void BooksPageStack::Private::onModelChanged()
{
queueSignals(SignalModelChanged);
}
inline void BooksPageStack::Private::queueSignals(int aSignals)
{
iQueuedSignals |= aSignals;
}
int BooksPageStack::Private::makeIndexValid(int aIndex) const
{
if (aIndex < 0) {
return 0;
} else if (aIndex >= iEntries.count()) {
return iEntries.count() - 1;
}
return aIndex;
}
inline bool BooksPageStack::Private::isValidIndex(int aIndex) const
{
return aIndex >= 0 && aIndex < iEntries.count();
}
inline void BooksPageStack::Private::checkCurrentIndex(int aLastIndex)
{
if (validateCurrentIndex() != aLastIndex) {
queueSignals(SignalCurrentIndexChanged);
}
}
inline void BooksPageStack::Private::checkCurrentPage(int aLastCurrentPage)
{
if (currentPage() != aLastCurrentPage) {
queueSignals(SignalCurrentPageChanged);
}
}
inline void BooksPageStack::Private::checkCount(int aLastCount)
{
if (iEntries.count() != aLastCount) {
queueSignals(SignalCountChanged);
}
}
int BooksPageStack::Private::validateCurrentIndex()
{
const int validIndex = makeIndexValid(iCurrentIndex);
if (iCurrentIndex != validIndex) {
iCurrentIndex = validIndex;
queueSignals(SignalCurrentIndexChanged);
}
return iCurrentIndex;
}
inline int BooksPageStack::Private::currentPage() const
{
return iEntries.at(iCurrentIndex).iPage;
}
int BooksPageStack::Private::pageAt(int aIndex) const
{
if (isValidIndex(aIndex)) {
return iEntries.at(aIndex).iPage;
}
return 0;
}
void BooksPageStack::Private::setCurrentIndex(int aIndex)
{
const int newIndex = makeIndexValid(aIndex);
if (iCurrentIndex != newIndex) {
const int oldCurrentPage = currentPage();
iCurrentIndex = newIndex;
HDEBUG(iCurrentIndex);
checkCurrentPage(oldCurrentPage);
queueSignals(SignalCurrentIndexChanged);
}
}
void BooksPageStack::Private::setPageAt(int aIndex, int aPage)
{
Entry entry = iEntries.at(aIndex);
if (entry.iPage != aPage) {
entry.iPage = aPage;
const int np = iPageMarks.count();
if (np > 0) {
entry.iPos = (aPage < 0) ? iPageMarks.at(0) :
(aPage >= np) ? iPageMarks.at(np-1) :
iPageMarks.at(aPage);
} else {
entry.iPos.set(0, 0, 0);
}
iEntries[aIndex] = entry;
pageChanged(aIndex);
}
}
inline void BooksPageStack::Private::setCurrentPage(int aPage)
{
setPageAt(iCurrentIndex, aPage);
}
void BooksPageStack::Private::pageChanged(int aIndex)
{
QVector<int> roles;
roles.append(PageRole);
QModelIndex modelIndex(iModel->createIndex(aIndex, 0));
Q_EMIT iModel->dataChanged(modelIndex, modelIndex, roles);
}
void BooksPageStack::Private::setStack(BooksPos::List aStack, int aStackPos)
{
if (aStack.isEmpty()) {
aStack = BooksPos::List();
aStack.append(BooksPos(0, 0, 0));
}
// First entry (always exists)
BooksPos pos = aStack.at(0);
Entry lastEntry(pos, findPage(pos));
if (iEntries.at(0) != lastEntry) {
iEntries[0] = lastEntry;
pageChanged(0);
} else {
iEntries[0] = lastEntry;
}
// Update other entries
int entryIndex = 1, stackIndex = 1;
const int oldEntryCount = iEntries.count();
while (entryIndex < oldEntryCount && stackIndex < aStack.count()) {
pos = aStack.at(stackIndex++);
Entry entry(pos, findPage(pos));
if (iEntries.at(entryIndex) != entry) {
iEntries[entryIndex] = entry;
pageChanged(entryIndex);
} else {
iEntries[entryIndex] = entry;
}
lastEntry = entry;
entryIndex++;
}
if (entryIndex < oldEntryCount) {
// We have run out of stack entries, remove remainig rows
iModel->beginRemoveRows(QModelIndex(), entryIndex, oldEntryCount-1);
while (iEntries.count() > entryIndex) {
iEntries.removeLast();
}
iModel->endRemoveRows();
Q_EMIT iModel->countChanged();
} else {
// Add new entries if necessary
while (stackIndex < aStack.count()) {
pos = aStack.at(stackIndex++);
Entry entry(pos, findPage(pos));
if (entry != lastEntry) {
iEntries.append(entry);
lastEntry = entry;
}
}
const int n = iEntries.count();
if (n > oldEntryCount) {
// We have added some entries, update the model
iModel->beginInsertRows(QModelIndex(), oldEntryCount, n-1);
iModel->endInsertRows();
Q_EMIT iModel->countChanged();
}
}
setCurrentIndex(aStackPos);
validateCurrentIndex();
}
void BooksPageStack::Private::setPageMarks(BooksPos::List aPageMarks)
{
if (iPageMarks != aPageMarks) {
iPageMarks = aPageMarks;
const int prevCurrentPage = currentPage();
HDEBUG(iPageMarks);
const int n = iEntries.count();
for (int i=0; i<n; i++) {
Entry entry = iEntries.at(i);
const int page = findPage(entry.iPos);
if (entry.iPage != page) {
entry.iPage = page;
iEntries[i] = entry;
pageChanged(i);
}
}
checkCurrentPage(prevCurrentPage);
}
}
int BooksPageStack::Private::findPage(BooksPos aPos) const
{
BooksPos::ConstIterator it = qBinaryFind(iPageMarks, aPos);
if (it == iPageMarks.end()) {
it = qLowerBound(iPageMarks, aPos);
if (it == iPageMarks.end()) {
return iPageMarks.count() - 1;
} else if (it == iPageMarks.begin()) {
return 0;
} else {
return (it - iPageMarks.begin()) - 1;
}
} else {
return it - iPageMarks.begin();
}
}
BooksPos BooksPageStack::Private::getPosAt(int aIndex) const
{
if (iPageMarks.isEmpty()) {
return BooksPos(0, 0, 0);
} else if (aIndex < 0) {
return iPageMarks.first();
} else if (aIndex >= iPageMarks.count()) {
return iPageMarks.last();
} else {
return iPageMarks.at(aIndex);
}
}
void BooksPageStack::Private::push(BooksPos aPos, int aPage)
{
Entry last = iEntries.last();
if (last.iPos != aPos || last.iPage != aPage) {
// We push on top of the current position. If we have reached
// the depth limit, we push the entire stack down
const int n = iEntries.count();
if (n >= MAX_DEPTH) {
for (int i=1; i<n; i++) {
iEntries[i-1] = iEntries[i];
pageChanged(i-1);
}
iEntries[n-1] = Entry(aPos, aPage);
pageChanged(n-1);
} else {
if (n >= iCurrentIndex+2) {
if (n > iCurrentIndex+2) {
// Drop unnecessary entries
iModel->beginRemoveRows(QModelIndex(), iCurrentIndex+2, n-1);
while (iEntries.count() > iCurrentIndex+2) {
iEntries.removeLast();
}
iModel->endRemoveRows();
Q_EMIT iModel->countChanged();
queueSignals(SignalCountChanged);
}
// And replace the next one
setPageAt(iCurrentIndex+1, aPage);
} else {
// Just push the new one
const int i = iCurrentIndex+1;
iModel->beginInsertRows(QModelIndex(), i, i);
iEntries.append(Entry(aPos, aPage));
iModel->endInsertRows();
queueSignals(SignalCountChanged);
}
// Move the current index
setCurrentIndex(iCurrentIndex+1);
}
}
}
void BooksPageStack::Private::push(BooksPos aPos)
{
if (iEntries.last().iPos != aPos) {
push(aPos, findPage(aPos));
}
}
void BooksPageStack::Private::push(int aPage)
{
if (iEntries.last().iPage != aPage) {
push(getPosAt(aPage), aPage);
}
}
void BooksPageStack::Private::pop()
{
const int n = iEntries.count();
if (n > 1) {
iModel->beginRemoveRows(QModelIndex(), n-1, n-1);
iEntries.removeLast();
validateCurrentIndex();
iModel->endRemoveRows();
queueSignals(SignalCountChanged);
}
}
void BooksPageStack::Private::clear()
{
const int n = iEntries.count();
if (n > 1) {
Entry currentEntry = iEntries.at(iCurrentIndex);
iModel->beginRemoveRows(QModelIndex(), 1, n-1);
while (iEntries.count() > 1) {
iEntries.removeLast();
}
validateCurrentIndex();
iModel->endRemoveRows();
if (iEntries.at(0) != currentEntry) {
iEntries[0] = currentEntry;
pageChanged(0);
}
queueSignals(SignalCountChanged);
}
}
BooksPos::Stack BooksPageStack::Private::getStack() const
{
const int n = iEntries.count();
BooksPos::Stack stack;
stack.iList.reserve(n);
for (int i=0; i<n; i++) {
stack.iList.append(iEntries.at(i).iPos);
}
stack.iPos = iCurrentIndex;
return stack;
}
// ==========================================================================
// BooksPageStack
// ==========================================================================
BooksPageStack::BooksPageStack(QObject* aParent) :
QAbstractListModel(aParent),
iPrivate(new Private(this))
{
#if QT_VERSION < 0x050000
setRoleNames(roleNames());
#endif
}
int BooksPageStack::count() const
{
return iPrivate->iEntries.count();
}
int BooksPageStack::currentIndex() const
{
return iPrivate->iCurrentIndex;
}
int BooksPageStack::currentPage() const
{
return iPrivate->currentPage();
}
void BooksPageStack::setCurrentIndex(int aIndex)
{
iPrivate->setCurrentIndex(aIndex);
iPrivate->emitQueuedSignals();
}
void BooksPageStack::setCurrentPage(int aPage)
{
iPrivate->setCurrentPage(aPage);
iPrivate->emitQueuedSignals();
}
void BooksPageStack::back()
{
iPrivate->setCurrentIndex(iPrivate->iCurrentIndex - 1);
iPrivate->emitQueuedSignals();
}
void BooksPageStack::forward()
{
iPrivate->setCurrentIndex(iPrivate->iCurrentIndex + 1);
iPrivate->emitQueuedSignals();
}
BooksPos::Stack BooksPageStack::getStack() const
{
return iPrivate->getStack();
}
void BooksPageStack::setStack(BooksPos::List aStack, int aCurrentPos)
{
iPrivate->setStack(aStack, aCurrentPos);
iPrivate->emitQueuedSignals();
}
void BooksPageStack::setPageMarks(BooksPos::List aPageMarks)
{
iPrivate->setPageMarks(aPageMarks);
iPrivate->emitQueuedSignals();
}
int BooksPageStack::pageAt(int aIndex)
{
return iPrivate->pageAt(aIndex);
}
void BooksPageStack::pushPage(int aPage)
{
HDEBUG(aPage);
iPrivate->push(aPage);
iPrivate->emitQueuedSignals();
}
void BooksPageStack::pushPosition(BooksPos aPos)
{
HDEBUG("" << aPos);
iPrivate->push(aPos);
iPrivate->emitQueuedSignals();
}
void BooksPageStack::pop()
{
HDEBUG("");
iPrivate->pop();
iPrivate->emitQueuedSignals();
}
void BooksPageStack::clear()
{
HDEBUG("");
iPrivate->clear();
iPrivate->emitQueuedSignals();
}
QHash<int,QByteArray> BooksPageStack::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(PageRole, "page");
return roles;
}
int BooksPageStack::rowCount(const QModelIndex&) const
{
return iPrivate->iEntries.count();
}
QVariant BooksPageStack::data(const QModelIndex& aIndex, int aRole) const
{
const int row = aIndex.row();
if (iPrivate->isValidIndex(row) && aRole == PageRole) {
return iPrivate->iEntries.at(row).iPage;
}
return QVariant();
}
bool BooksPageStack::setData(const QModelIndex& aIndex, const QVariant& aValue,
int aRole)
{
const int row = aIndex.row();
if (iPrivate->isValidIndex(row) && aRole == PageRole) {
bool ok = false;
const int page = aValue.toInt(&ok);
if (page >= 0 && ok) {
iPrivate->setPageAt(row, page);
iPrivate->emitQueuedSignals();
return true;
}
}
return false;
}
#include "BooksPageStack.moc"

97
app/src/BooksPageStack.h Normal file
View file

@ -0,0 +1,97 @@
/*
* Copyright (C) 2016-2017 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.
*/
#ifndef BOOKS_STACK_MODEL_H
#define BOOKS_STACK_MODEL_H
#include "BooksTypes.h"
#include "BooksPos.h"
#include <QAbstractListModel>
#include <QtQml>
class BooksPageStack: public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged)
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
enum Role {
PageRole = Qt::UserRole // "page"
};
public:
explicit BooksPageStack(QObject* aParent = NULL);
int count() const;
int currentIndex() const;
void setCurrentIndex(int aIndex);
int currentPage() const;
void setCurrentPage(int aPage);
BooksPos::Stack getStack() const;
void setStack(BooksPos::List aStack, int aCurrentPos);
void setPageMarks(BooksPos::List aPageMarks);
// QAbstractListModel
QHash<int,QByteArray> roleNames() const;
int rowCount(const QModelIndex& aParent) const;
QVariant data(const QModelIndex& aIndex, int aRole) const;
bool setData(const QModelIndex& aIndex, const QVariant& aValue, int aRole);
Q_INVOKABLE int pageAt(int aIndex);
Q_INVOKABLE void pushPage(int aPage);
Q_INVOKABLE void pushPosition(BooksPos aPos);
Q_INVOKABLE void pop();
Q_INVOKABLE void clear();
Q_INVOKABLE void back();
Q_INVOKABLE void forward();
Q_SIGNALS:
void changed();
void countChanged();
void currentIndexChanged();
void currentPageChanged();
private:
class Entry;
class Private;
Private* iPrivate;
};
QML_DECLARE_TYPE(BooksPageStack)
#endif // BOOKS_STACK_MODEL_H

View file

@ -723,10 +723,10 @@ void BooksPageWidget::onLongPressTaskDone()
}
} else if (task->iKind == INTERNAL_HYPERLINK) {
if (iModel) {
int page = iModel->linkToPage(task->iLink);
if (page >= 0) {
HDEBUG("link to page" << page);
Q_EMIT jumpToPage(page);
BooksPos pos = iModel->linkPosition(task->iLink);
if (pos.valid()) {
HDEBUG("link to" << pos);
Q_EMIT pushPosition(pos);
}
}
} else if (task->iKind == FOOTNOTE) {

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -101,6 +101,7 @@ Q_SIGNALS:
void activeTouch(int touchX, int touchY);
void jumpToPage(int page);
void showFootnote(int touchX, int touchY, QString text, QString imageId);
void pushPosition(BooksPos position);
private Q_SLOTS:
void onWidthChanged();

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -34,6 +34,7 @@
#ifndef BOOKS_POSITION_H
#define BOOKS_POSITION_H
#include <QMetaType>
#include <QVariant>
#include <QDebug>
#include <QList>
@ -46,6 +47,7 @@ struct BooksPos {
typedef QList<BooksPos> List;
typedef QList<BooksPos>::iterator Iterator;
typedef QList<BooksPos>::const_iterator ConstIterator;
struct Stack { List iList; int iPos; };
BooksPos() :
iParagraphIndex(-1),
@ -80,6 +82,13 @@ struct BooksPos {
return iParagraphIndex >= 0 && iElementIndex >= 0 && iCharIndex >= 0;
}
void set(int aParagraphIndex, int aElementIndex, int aCharIndex)
{
iParagraphIndex = aParagraphIndex;
iElementIndex = aElementIndex;
iCharIndex = aCharIndex;
}
QVariant toVariant() const
{
QVariantList list;
@ -124,8 +133,7 @@ struct BooksPos {
(iParagraphIndex > aPos.iParagraphIndex) ? false :
(iElementIndex < aPos.iElementIndex) ? true :
(iElementIndex > aPos.iElementIndex) ? false :
(iCharIndex < aPos.iCharIndex) ? true :
(iCharIndex > aPos.iCharIndex) ? false : true;
(iCharIndex < aPos.iCharIndex);
}
bool operator > (const BooksPos& aPos) const
@ -159,7 +167,7 @@ struct BooksPos {
QString toString() const
{
return QString("BooksPos(%1,%2,%3)").arg(iParagraphIndex).
return QString("(%1,%2,%3)").arg(iParagraphIndex).
arg(iElementIndex).arg(iCharIndex);
}
@ -173,4 +181,6 @@ struct BooksPos {
inline QDebug& operator<<(QDebug& aDebug, const BooksPos& aPos)
{ aDebug << qPrintable(aPos.toString()); return aDebug; }
Q_DECLARE_METATYPE(BooksPos)
#endif /* BOOKS_POSITION_H */

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -1119,7 +1119,7 @@ void BooksShelf::onCopyTaskDone()
copy = task->iDestItem->book();
if (copy) {
copy->retain();
copy->setLastPos(src->lastPos());
copy->setPageStack(src->pageStack(), src->pageStackPos());
copy->setCoverImage(src->coverImage());
copy->requestCoverImage();
} else {

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2016 Jolla Ltd.
* Copyright (C) 2015-2017 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
@ -32,6 +32,7 @@
*/
#include "BooksDefs.h"
#include "BooksPos.h"
#include "BooksShelf.h"
#include "BooksBook.h"
#include "BooksBookModel.h"
@ -39,6 +40,7 @@
#include "BooksConfig.h"
#include "BooksImportModel.h"
#include "BooksPathModel.h"
#include "BooksPageStack.h"
#include "BooksStorageModel.h"
#include "BooksPageWidget.h"
#include "BooksListWatcher.h"
@ -68,12 +70,14 @@
Q_DECL_EXPORT int main(int argc, char **argv)
{
QGuiApplication* app = SailfishApp::application(argc, argv);
qRegisterMetaType<BooksPos>();
BOOKS_QML_REGISTER(BooksShelf, "Shelf");
BOOKS_QML_REGISTER(BooksBook, "Book");
BOOKS_QML_REGISTER(BooksBookModel, "BookModel");
BOOKS_QML_REGISTER(BooksCoverModel, "CoverModel");
BOOKS_QML_REGISTER(BooksImportModel, "BooksImportModel");
BOOKS_QML_REGISTER(BooksPathModel, "BooksPathModel");
BOOKS_QML_REGISTER(BooksPageStack, "BooksPageStack");
BOOKS_QML_REGISTER(BooksStorageModel, "BookStorage");
BOOKS_QML_REGISTER(BooksPageWidget, "PageWidget");
BOOKS_QML_REGISTER(BooksListWatcher, "ListWatcher");