diff --git a/app/app.pro b/app/app.pro index c095d69..b9a5ea0 100644 --- a/app/app.pro +++ b/app/app.pro @@ -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 \ diff --git a/app/qml/BooksBookView.qml b/app/qml/BooksBookView.qml index 8cf1b8a..1cd86b5 100644 --- a/app/qml/BooksBookView.qml +++ b/app/qml/BooksBookView.qml @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2016 Jolla Ltd. + Copyright (C) 2015-2017 Jolla Ltd. Contact: Slava Monich 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 + 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")) : "" } } diff --git a/app/qml/BooksPageView.qml b/app/qml/BooksPageView.qml index 844abf4..eb6f130 100644 --- a/app/qml/BooksPageView.qml +++ b/app/qml/BooksPageView.qml @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2016 Jolla Ltd. + Copyright (C) 2015-2017 Jolla Ltd. Contact: Slava Monich 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) } diff --git a/app/qml/BooksPager.qml b/app/qml/BooksPager.qml index f12ee37..811f2ff 100644 --- a/app/qml/BooksPager.qml +++ b/app/qml/BooksPager.qml @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2016 Jolla Ltd. + Copyright (C) 2015-2017 Jolla Ltd. Contact: Slava Monich 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() + } } diff --git a/app/src/BooksBook.cpp b/app/src/BooksBook.cpp index 2531f4f..461adde 100644 --- a/app/src/BooksBook.cpp +++ b/app/src/BooksBook.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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 #include +#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= 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 * * 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 bookRef() const { return iBook; } + int pageStackPos() const; + BooksPos::List pageStack() const; + void setPageStack(BooksPos::List aStack, int aStackPos); + shared_ptr 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 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 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 diff --git a/app/src/BooksBookModel.cpp b/app/src/BooksBookModel.cpp index 3ecfc8d..eb536d6 100644 --- a/app/src/BooksBookModel.cpp +++ b/app/src/BooksBookModel.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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 aBook, - const BooksPos& aPagePos, const BooksPos& aNextPagePos, - const BooksPos& aLastPos, int aPageCount); - ~Task(); + PagingTask(BooksBookModel* aReceiver, shared_ptr 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 aBook, const BooksPos& aPagePos, - const BooksPos& aNextPagePos, const BooksPos& aLastPos, int aPageCount) : +BooksBookModel::PagingTask::PagingTask(BooksBookModel* aModel, + shared_ptr 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 oldBook; shared_ptr 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(""); } 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 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; - } - updateModel(oldModelPageCount); - Q_EMIT pageMarksChanged(); - Q_EMIT jumpToPage(iData->pickPage(page1, page2, oldPageCount)); - } else { - startReset(ReasonUnknown, false); + // Cancel unnecessary paging task + BooksLoadingSignalBlocker block(this); + if (iPagingTask) { + HDEBUG("not so fast please..."); + iPagingTask->release(this); + iPagingTask = NULL; } + updateModel(oldModelPageCount); + iPageStack->setPageMarks(iData->iPageMarks); + Q_EMIT pageMarksChanged(); + 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 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(); diff --git a/app/src/BooksBookModel.h b/app/src/BooksBookModel.h index e24a000..a0b2a6a 100644 --- a/app/src/BooksBookModel.h +++ b/app/src/BooksBookModel.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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 bookRef() const { return iBookRef; } + BooksMargins margins() const; + shared_ptr bookRef() const; shared_ptr bookModel() const; shared_ptr bookTextModel() const; shared_ptr contentsModel() const; shared_ptr footnoteModel(const std::string& aId) const; - shared_ptr textStyle() const { return iTextStyle; } - int linkToPage(const std::string& aLink) const; + shared_ptr 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 iBookRef; - Task* iTask; + PagingTask* iPagingTask; Data* iData; Data* iData2; QSharedPointer iSettings; shared_ptr iTaskQueue; shared_ptr 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 BooksBookModel::bookRef() const + { return iBookRef; } +inline shared_ptr BooksBookModel::textStyle() const + { return iTextStyle; } + #endif // BOOKS_BOOK_MODEL_H diff --git a/app/src/BooksListWatcher.cpp b/app/src/BooksListWatcher.cpp index 1235fbe..67313ea 100644 --- a/app/src/BooksListWatcher.cpp +++ b/app/src/BooksListWatcher.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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,46 +120,47 @@ void BooksListWatcher::positionViewAtIndex(int aIndex) { if (iListView) { HDEBUG(aIndex); - if (iCenterMode < 0) { - bool ok = false; - const QMetaObject* metaObject = iListView->metaObject(); - if (metaObject) { - int index = metaObject->indexOfEnumerator("PositionMode"); - if (index >= 0) { - QMetaEnum metaEnum = metaObject->enumerator(index); - int value = metaEnum.keyToValue("Center", &ok); - if (ok) { - iCenterMode = value; - HDEBUG("Center =" << iCenterMode); - } + doPositionViewAtIndex(aIndex); + } +} + +void BooksListWatcher::doPositionViewAtIndex(int aIndex) +{ + if (iCenterMode < 0) { + bool ok = false; + const QMetaObject* metaObject = iListView->metaObject(); + if (metaObject) { + int index = metaObject->indexOfEnumerator("PositionMode"); + if (index >= 0) { + QMetaEnum metaEnum = metaObject->enumerator(index); + int value = metaEnum.keyToValue("Center", &ok); + if (ok) { + iCenterMode = value; + HDEBUG("Center =" << iCenterMode); } } - HASSERT(ok); - if (!ok) { - // This is what it normally is - iCenterMode = 1; - } } - 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 (aIndex > 0 && getCurrentIndex() == 0) { - // Didn't work from the first try, give it another go - HDEBUG("retrying..."); - positionViewAtIndex(aIndex, iCenterMode); - } - iCanRetry = false; + HASSERT(ok); + if (!ok) { + // This is what it normally is + iCenterMode = 1; } - iPositionIsChanging = false; - updateCurrentIndex(); } + iPositionIsChanging = true; + positionViewAtIndex(aIndex, iCenterMode); + // This is probably a bug in QQuickListView - it first calculates + // the item position and then starts instantiating the delegates. + // 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); + } + 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,13 +205,29 @@ int BooksListWatcher::getCurrentIndex() return -1; } -void BooksListWatcher::updateCurrentIndex() +void BooksListWatcher::tryToRestoreCurrentIndex() { + HASSERT(!iPositionIsChanging); const int index = getCurrentIndex(); if (iCurrentIndex != index) { - HDEBUG(index); - iCurrentIndex = index; - Q_EMIT currentIndexChanged(); + if (iCurrentIndex >= 0) { + doPositionViewAtIndex(iCurrentIndex); + } + } +} + +void BooksListWatcher::updateCurrentIndex() +{ + HASSERT(!iPositionIsChanging); + if (contentWidth() > 0 || contentHeight() > 0) { + const int index = getCurrentIndex(); + if (iCurrentIndex != index) { + iCurrentIndex = index; + HDEBUG(index << contentWidth() << "x" << contentHeight()); + Q_EMIT currentIndexChanged(); + } + } else { + HDEBUG(contentWidth() << "x" << contentHeight()); } } @@ -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(); } } diff --git a/app/src/BooksListWatcher.h b/app/src/BooksListWatcher.h index bb529ae..9e96239 100644 --- a/app/src/BooksListWatcher.h +++ b/app/src/BooksListWatcher.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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; }; diff --git a/app/src/BooksPageStack.cpp b/app/src/BooksPageStack.cpp new file mode 100644 index 0000000..1feec7b --- /dev/null +++ b/app/src/BooksPageStack.cpp @@ -0,0 +1,633 @@ +/* + * Copyright (C) 2016-2017 Jolla Ltd. + * Contact: Slava Monich + * + * 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 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)), + 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*(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 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= 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= 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; iiEntries.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 BooksPageStack::roleNames() const +{ + QHash 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" diff --git a/app/src/BooksPageStack.h b/app/src/BooksPageStack.h new file mode 100644 index 0000000..25b2fc1 --- /dev/null +++ b/app/src/BooksPageStack.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016-2017 Jolla Ltd. + * Contact: Slava Monich + * + * 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 +#include + +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 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 diff --git a/app/src/BooksPageWidget.cpp b/app/src/BooksPageWidget.cpp index c86fd06..4745a7f 100644 --- a/app/src/BooksPageWidget.cpp +++ b/app/src/BooksPageWidget.cpp @@ -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) { diff --git a/app/src/BooksPageWidget.h b/app/src/BooksPageWidget.h index 9b40c5e..b770159 100644 --- a/app/src/BooksPageWidget.h +++ b/app/src/BooksPageWidget.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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(); diff --git a/app/src/BooksPos.h b/app/src/BooksPos.h index c25faa7..495c075 100644 --- a/app/src/BooksPos.h +++ b/app/src/BooksPos.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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 #include #include #include @@ -46,6 +47,7 @@ struct BooksPos { typedef QList List; typedef QList::iterator Iterator; typedef QList::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 */ diff --git a/app/src/BooksShelf.cpp b/app/src/BooksShelf.cpp index f2d81b1..6af1a59 100644 --- a/app/src/BooksShelf.cpp +++ b/app/src/BooksShelf.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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 { diff --git a/app/src/main.cpp b/app/src/main.cpp index ef33649..cb3156e 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Jolla Ltd. + * Copyright (C) 2015-2017 Jolla Ltd. * Contact: Slava Monich * * 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(); 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");