/* * Copyright (C) 2016-2018 Jolla Ltd. * Copyright (C) 2015-2018 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; public: Private(BooksPageStack* aModel); BooksPos::Stack getStack() const; bool atEnd() 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); void push(int aPage); void pop(); void clear(); void emitQueuedSignals(); private Q_SLOTS: void onModelChanged(); private: BooksPageStack* parentModel() const; void push(BooksPos aPos, int aPage); 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) { 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())); } inline BooksPageStack* BooksPageStack::Private::parentModel() const { return qobject_cast(parent()); } 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]); if (iQueuedSignals) { BooksPageStack* model = parentModel(); 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 bool BooksPageStack::Private::atEnd() const { return (iCurrentIndex + 1) >= 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); BooksPageStack* model = parentModel(); QModelIndex modelIndex(model->createIndex(aIndex, 0)); Q_EMIT model->dataChanged(modelIndex, modelIndex, roles); if (aIndex == iCurrentIndex) { queueSignals(SignalCurrentPageChanged); } } 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 BooksPageStack* model = parentModel(); model->beginRemoveRows(QModelIndex(), entryIndex, oldEntryCount-1); while (iEntries.count() > entryIndex) { iEntries.removeLast(); } model->endRemoveRows(); queueSignals(SignalCountChanged); } 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 BooksPageStack* model = parentModel(); model->beginInsertRows(QModelIndex(), oldEntryCount, n-1); model->endInsertRows(); queueSignals(SignalCountChanged); } } 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(); const int n = iEntries.count(); // The caller makes sure that the operation makes sense HASSERT(!atEnd() || last.iPos != aPos || last.iPage != aPage); if (n >= iCurrentIndex+2) { // Push on top of the current position. if (n > iCurrentIndex+2) { // Drop unnecessary entries BooksPageStack* model = parentModel(); model->beginRemoveRows(QModelIndex(), iCurrentIndex+2, n-1); while (iEntries.count() > iCurrentIndex+2) { iEntries.removeLast(); } model->endRemoveRows(); queueSignals(SignalCountChanged); } // And replace the next one setPageAt(iCurrentIndex+1, aPage); setCurrentIndex(iCurrentIndex+1); } else if (n >= MAX_DEPTH) { // We have reached the depth limit, push the entire stack down const int oldCurrentPage = currentPage(); for (int i=1; ibeginInsertRows(QModelIndex(), i, i); iEntries.append(Entry(aPos, aPage)); model->endInsertRows(); queueSignals(SignalCountChanged); setCurrentIndex(i); } } void BooksPageStack::Private::push(BooksPos aPos) { if (!atEnd() || iEntries.last().iPos != aPos) { push(aPos, findPage(aPos)); } } void BooksPageStack::Private::push(int aPage) { if (!atEnd() || iEntries.last().iPage != aPage) { push(getPosAt(aPage), aPage); } } void BooksPageStack::Private::pop() { const int n = iEntries.count(); if (n > 1) { BooksPageStack* model = parentModel(); model->beginRemoveRows(QModelIndex(), n-1, n-1); iEntries.removeLast(); validateCurrentIndex(); model->endRemoveRows(); queueSignals(SignalCountChanged); } } void BooksPageStack::Private::clear() { const int n = iEntries.count(); if (n > 1) { BooksPageStack* model = parentModel(); Entry currentEntry = iEntries.at(iCurrentIndex); model->beginRemoveRows(QModelIndex(), 1, n-1); while (iEntries.count() > 1) { iEntries.removeLast(); } validateCurrentIndex(); model->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"