diff --git a/src/private/runtimetests_p.cpp b/src/private/runtimetests_p.cpp index c3286fe..b7ddb8a 100644 --- a/src/private/runtimetests_p.cpp +++ b/src/private/runtimetests_p.cpp @@ -1,689 +1,689 @@ #ifdef ENABLE_EXTRA_VALIDATION #include #include #include #include #include #include #include /** * While this module is under development and the autotests are lacking, * always run strict tests at runtime. * * This is 3 ways to look at the same data: * * * As a tree * * As a linked list * * As a "sliding window" (viewport) */ void _test_validateTree(StateTracker::Content *self, StateTracker::Index* p) { // The asserts below only work on valid models with valid delegates. // If those conditions are not met, it *could* work anyway, but cannot be // validated. /*Q_ASSERT(m_FailedCount >= 0); if (m_FailedCount) { qWarning() << "The tree is fragmented and failed to self heal: disable validation"; return; }*/ // if (p->parent() == self->root() && self->root()->firstChild() == p && p->metadata()->viewTracker()) { // Q_ASSERT(!p->metadata()->viewTracker()->up()); // } // First, let's check the linked list to avoid running more test on really // corrupted data if (auto i = p->firstChild()) { auto idx = i->index(); int count = 1; auto oldI = i; Q_ASSERT(idx.isValid()); while ((oldI = i) && (i = i->nextSibling())) { // If this is a next, then there has to be a previous Q_ASSERT(i->parent() == p); Q_ASSERT(i->previousSibling()); Q_ASSERT(i->previousSibling()->index() == idx); //Q_ASSERT(i->effectiveRow() == idx.row()+1); //FIXME Q_ASSERT(i->previousSibling()->nextSibling() == i); Q_ASSERT(i->previousSibling() == oldI); Q_ASSERT((!oldI->index().isValid()) || i->index().parent() == oldI->index().parent()); Q_ASSERT((!oldI->index().isValid()) || i->effectiveRow() == oldI->effectiveRow()+1); idx = i->index(); Q_ASSERT(idx.isValid()); count++; } Q_ASSERT(p->lastChild()); Q_ASSERT(p == p->firstChild()->parent()); Q_ASSERT(p == p->lastChild()->parent()); Q_ASSERT(p->loadedChildrenCount() == count); } // Do that again in the other direction if (auto i = p->lastChild()) { auto idx = i->index(); auto oldI = i; int count = 1; Q_ASSERT(idx.isValid()); while ((oldI = i) && (i = i->previousSibling())) { Q_ASSERT(i->nextSibling()); Q_ASSERT(i->nextSibling()->index() == idx); Q_ASSERT(i->parent() == p); //Q_ASSERT(i->effectiveRow() == idx.row()-1); //FIXME Q_ASSERT(i->nextSibling()->previousSibling() == i); Q_ASSERT(i->nextSibling() == oldI); idx = i->index(); Q_ASSERT(idx.isValid()); count++; } Q_ASSERT(p->loadedChildrenCount() == count); } //TODO remove once stable // Brute force recursive validations StateTracker::Index *old(nullptr), *newest(nullptr); const auto children = p->allLoadedChildren(); for (auto item : qAsConst(children)) { Q_ASSERT(item->parent() == p); if ((!newest) || item->effectiveRow() < newest->effectiveRow()) newest = item; if ((!old) || item->effectiveRow() > old->effectiveRow()) old = item; // Check that m_FailedCount is valid //Q_ASSERT(!item->metadata()->viewTracker()->hasFailed()); // Test the indices Q_ASSERT(p == self->root() || item->index().internalPointer() == item->index().internalPointer()); Q_ASSERT(p == self->root() || (p->index().isValid()) || p->index().internalPointer() != item->index().internalPointer()); //Q_ASSERT(old == item || old->effectiveRow() > i->effectiveRow()); //FIXME //Q_ASSERT(newest == item || newest->effectiveRow() < i->effectiveRow()); //FIXME // Test that there is no trivial duplicate StateTracker::ModelItem for the same index if(item->previousSibling() && !item->previousSibling()->loadedChildrenCount()) { const auto prev = item->up(); Q_ASSERT(prev == item->previousSibling()); const auto next = prev->down(); Q_ASSERT(next == item); if (prev == item->parent()) { Q_ASSERT(item->effectiveRow() == 0); Q_ASSERT(item->effectiveParentIndex() == prev->index()); } } // Test the virtual linked list between the leafs and branches if(auto next = item->down()) { Q_ASSERT(next->up() == item); Q_ASSERT(next != item); if (next->effectiveParentIndex() == item->effectiveParentIndex()) { const int rc = self->modelTracker()->modelCandidate()->rowCount(item->index()); Q_ASSERT(!rc); } } else { // There is always a next is those conditions are not met unless there // is failed elements creating (auto-corrected) holes in the chains. Q_ASSERT(!item->nextSibling()); Q_ASSERT(!item->loadedChildrenCount()); } if(auto prev = item->up()) { Q_ASSERT(prev->down() == item); Q_ASSERT(prev != item); } else { // There is always a previous if those conditions are not met unless there // is failed elements creating (auto-corrected) holes in the chains. Q_ASSERT(!item->previousSibling()); Q_ASSERT(item->parent() == self->root()); } _test_validateTree(self, item); } // Traverse as a list if (p == self->root()) { StateTracker::Index* oldTTI(nullptr); int count(0), count2(0); for (auto i = self->root()->firstChild(); i; i = i->down()) { Q_ASSERT((!oldTTI) || i->up()); Q_ASSERT(i->up() == oldTTI); oldTTI = i; count++; } // Backward too oldTTI = nullptr; auto last = self->root()->lastChild(); while (last && last->lastChild()) last = last->lastChild(); for (auto i = last; i; i = i->up()) { Q_ASSERT((!oldTTI) || i->down()); Q_ASSERT(i->down() == oldTTI); oldTTI = i; count2++; } Q_ASSERT(count == count2); } // Test that the list edges are valid Q_ASSERT(!(!!p->lastChild() ^ !!p->firstChild())); Q_ASSERT(p->lastChild() == old); Q_ASSERT(p->firstChild() == newest); Q_ASSERT((!old) || !old->nextSibling()); Q_ASSERT((!newest) || !newest->previousSibling()); } void _test_validateLinkedList(StateTracker::Content *self, bool skipVItemState) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif _test_validateTree(self, self->root()); if (!self->root()->firstChild()) { Q_ASSERT(!self->root()->lastChild()); Q_ASSERT(!self->root()->loadedChildrenCount()); Q_ASSERT(!self->root()->previousSibling()); Q_ASSERT(!self->root()->nextSibling()); return; } else { auto first = self->root()->firstChild(); Q_ASSERT(first->metadata()->geometryTracker()->state() != StateTracker::Geometry::State::SIZE); Q_ASSERT(first->metadata()->geometryTracker()->state() != StateTracker::Geometry::State::INIT); } Q_ASSERT(!self->root()->firstChild()->up()); StateTracker::Index *prev(nullptr), *cur(self->root()->firstChild()); static const constexpr qreal maxReal = std::numeric_limits::max(); qreal maxY(0), maxX(0), minY(maxReal), minX(maxReal); bool hadVisible = false; bool visibleFinished = false; //DEBUG // qDebug() << ""; // while ((prev = cur) && (cur = TTI(cur->down()))) { // qDebug() << "TREE" // << (cur->m_State == StateTracker::ModelItem::State::VISIBLE); // } // qDebug() << "DONE"; prev = nullptr; cur = self->root()->firstChild(); StateTracker::Index *firstVisible(nullptr), *lastVisible(nullptr); _test_validate_edges_simple(self); const auto tve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::TopEdge); const auto bve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::BottomEdge); do { Q_ASSERT(cur->up() == prev); Q_ASSERT(cur->index().isValid()); Q_ASSERT(cur->index().model() == self->modelTracker()->modelCandidate()); if (!visibleFinished) { // If hit, it means the visible rect wasn't refreshed. //Q_ASSERT(cur->metadata()->isValid()); //TODO THIS_COMMIT } else //FIXME wrong, only added to prevent forgetting Q_ASSERT(cur->metadata()->isValid()); if (cur->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE && !hadVisible) { Q_ASSERT(!hadVisible); firstVisible = cur; hadVisible = true; Q_ASSERT(tve == cur); } else if (hadVisible && cur->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE) { visibleFinished = true; lastVisible = prev; Q_ASSERT(bve == lastVisible); } // if (cur->state() == StateTracker::ModelItem::State::VISIBLE) { // Q_ASSERT(cur->metadata()->isInSync()); // } Q_ASSERT(cur->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE || hadVisible); auto vi = cur->metadata()->viewTracker(); // if (vi) { // if (vi->m_State == StateTracker::ViewItem::State::ACTIVE) { // Q_ASSERT(cur->state() == StateTracker::ModelItem::State::VISIBLE); // Q_ASSERT(self->viewport()->currentRect().isValid()); // Q_ASSERT(self->viewport()->currentRect().intersects( // vi->geometry() // )); // } // else { // Q_ASSERT(vi->m_State == StateTracker::ViewItem::State::BUFFER); // Q_ASSERT(!self->viewport()->currentRect().intersects( // vi->geometry() // )); // } // } Q_ASSERT((!visibleFinished) || visibleFinished ^ (cur->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE)); // skipVItemState is necessary to test some steps in between the tree and view if (!skipVItemState) { Q_ASSERT(cur->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE || !vi); Q_ASSERT((!visibleFinished) || !vi); } // Check the the previous sibling has no children if (prev && cur->parent() == prev->parent()) { Q_ASSERT(cur->effectiveRow() == prev->effectiveRow() + 1); Q_ASSERT(!self->modelTracker()->modelCandidate()->rowCount(prev->index())); } // Check that there it no missing children from the previous if ((!prev) || (cur->parent() != prev && cur->parent() != prev->parent() && prev->parent() != self->root())) { Q_ASSERT((!prev) || prev->parent()->index().isValid()); // const int prevParRc = prev->d_ptr->m_pModel->rowCount(prev->parent()->index()); // Q_ASSERT(prev->effectiveRow() == prevParRc - 1); } if (vi) { //Q_ASSERT(cur->metadata()->isValid()); //TODO THIS_COMMIT auto geo = cur->metadata()->decoratedGeometry(); // Prevent accidental overlapping until a view with on-purpose // overlapping exists // Q_ASSERT(geo.y() >= maxY); // The `=` because it starts at 0 // Q_ASSERT(cur->metadata()->viewTracker()->item()->y() >= maxY); // The `=` because it starts at 0 // Q_ASSERT(cur->metadata()->viewTracker()->item()->y() == cur->metadata()->geometry().y()); // 0x0 elements are generally evil, but this is more about making // sure it works at all. //Q_ASSERT((geo.y() == 0 && cur->index().row() == 0) || geo.y()); //TODO THIS_COMMIT minX = std::min(minX, geo.x()); minY = std::min(minY, geo.y()); maxX = std::max(maxX, geo.bottomLeft().x()); maxY = std::max(maxY, geo.bottomLeft().y()); } } while ((prev = cur) && (cur = cur->down())); Q_ASSERT(maxY >= minY || !hadVisible); Q_ASSERT(maxX >= minX || !hadVisible); //TODO check buffer if (firstVisible && !skipVItemState) { //Q_ASSERT(prev->metadata()->geometry().y() < self->viewport()->currentRect().y()); } if (lastVisible && !skipVItemState) { //Q_ASSERT(lastVisible->metadata()->decoratedGeometry().y() <= self->viewport()->currentRect().bottomLeft().y()); //TODO THIS_COMMIT } // Q_ASSERT(maxY < self->viewport()->currentRect().bottomLeft().y() + 100); } void _test_validateViewport(StateTracker::Content *self, bool skipVItemState) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif _test_validateLinkedList(self, skipVItemState); int activeCount = 0; Q_ASSERT(!((!self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::BottomEdge)) ^ (!self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::TopEdge)))); Q_ASSERT(!((!self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::LeftEdge)) ^ (!self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::RightEdge)))); if (!self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::TopEdge)) return; if (self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::TopEdge) == self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::BottomEdge)) { auto u1 = self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::TopEdge)->up(); auto d1 = self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::TopEdge)->down(); auto u2 = self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::BottomEdge)->up(); auto d2 = self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::BottomEdge)->down(); Q_ASSERT((!u1) || u1->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE); Q_ASSERT((!u2) || u2->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE); Q_ASSERT((!d1) || d1->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE); Q_ASSERT((!d2) || d2->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE); } auto item = self->edges(IndexMetadata::EdgeType::FREE)->getEdge(Qt::TopEdge); StateTracker::Index *old = nullptr; QRectF oldGeo; do { Q_ASSERT(old != item); Q_ASSERT(item->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE || (skipVItemState && item->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::BUFFER) ); if (!skipVItemState) { Q_ASSERT(item->metadata()->viewTracker()); //Q_ASSERT(item->metadata()->viewTracker()->state() == StateTracker::ViewItem::State::ACTIVE); } Q_ASSERT(item->up() == old); //FIXME don't do this, its temporary so I can add more tests to catch // the cause of this failed (::move not updating the position) // if (old) // item->metadata()->m_State.setPosition(QPointF(0.0, oldGeo.y() + oldGeo.height())); auto geo = item->metadata()->decoratedGeometry(); if (geo.width() || geo.height()) { Q_ASSERT((!oldGeo.isValid()) || oldGeo.y() < geo.y()); Q_ASSERT((!oldGeo.isValid()) || oldGeo.y() + oldGeo.height() == geo.y()); } // qDebug() << "TEST" << activeCount << geo; activeCount++; oldGeo = geo; } while ((old = item) && (item = item->down())); } void StateTracker::Index::_test_validate_chain() const { #ifndef ENABLE_EXTRA_VALIDATION return; #endif auto p = this; //FIXME due to refactor int count = 0; Q_ASSERT((!p->firstChild()) || p->lastChild()); Q_ASSERT((!p->firstChild()) || !p->firstChild()->previousSibling()); Q_ASSERT((!p->lastChild()) || !p->lastChild()->nextSibling()); auto i = p->firstChild(); StateTracker::Index *prev = nullptr; while(i) { Q_ASSERT(i->previousSibling() == prev); Q_ASSERT(i->parent() == p); //Q_ASSERT((!prev) || i->effectiveRow() == prev->effectiveRow()+1); prev = i; i = i->nextSibling(); count++; } Q_ASSERT(count == loadedChildrenCount()); if (!prev) Q_ASSERT(p->firstChild() == p->lastChild()); else Q_ASSERT(prev == p->lastChild()); } void _test_validate_edges(StateTracker::Content *self) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif auto vStart = self->getEdge(IndexMetadata::EdgeType::VISIBLE, Qt::TopEdge); auto vEnd = self->getEdge(IndexMetadata::EdgeType::VISIBLE, Qt::BottomEdge); Q_ASSERT((!vStart) || (vStart && vEnd)); if (vStart) { Q_ASSERT(vStart->indexTracker()->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE); Q_ASSERT(vEnd->indexTracker()->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE); auto prev = vStart->indexTracker()->up(); auto next = vEnd->indexTracker()->down(); Q_ASSERT((!prev) || prev->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::REACHABLE); Q_ASSERT((!next) || next->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::REACHABLE); } } void _test_validate_move( StateTracker::Content *self, StateTracker::Index* parentTTI, StateTracker::Index* startTTI, StateTracker::Index* endTTI, StateTracker::Index* newPrevTTI, StateTracker::Index* newNextTTI, int row) { Q_UNUSED(self) #ifndef ENABLE_EXTRA_VALIDATION return; #endif Q_ASSERT((newPrevTTI || startTTI) && newPrevTTI != startTTI); Q_ASSERT((newNextTTI || endTTI ) && newNextTTI != endTTI ); // Update the tree parent (if necessary) Q_ASSERT(startTTI->parent() == parentTTI); Q_ASSERT(endTTI->parent() == parentTTI); //BEGIN debug if (newPrevTTI && newPrevTTI->parent()) newPrevTTI->parent()->_test_validate_chain(); if (startTTI && startTTI->parent()) startTTI->parent()->_test_validate_chain(); if (endTTI && endTTI->parent()) endTTI->parent()->_test_validate_chain(); if (newNextTTI && newNextTTI->parent()) newNextTTI->parent()->_test_validate_chain(); //END debug if (endTTI->nextSibling()) { Q_ASSERT(endTTI->nextSibling()->previousSibling() ==endTTI); } if (startTTI->previousSibling()) { Q_ASSERT(startTTI->previousSibling()->parent() == startTTI->parent()); Q_ASSERT(startTTI->previousSibling()->nextSibling() ==startTTI); } Q_ASSERT(parentTTI->firstChild()); Q_ASSERT(row || parentTTI->firstChild() == startTTI); } void _test_validate_edges_simple(StateTracker::Content *self) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif //BEGIN test auto tve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::TopEdge); Q_ASSERT((!tve) || (tve->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE)); Q_ASSERT((!tve) || (!tve->up()) || (tve->up()->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE)); auto bve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::BottomEdge); Q_ASSERT((!bve) || (bve->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE)); Q_ASSERT((!bve) || (!bve->down()) || bve->down()->metadata()->modelTracker()->state() != StateTracker::ModelItem::State::VISIBLE); //END test } void _test_validate_geometry_cache(StateTracker::Content *self) { auto bve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::BottomEdge); for (auto i = self->root()->firstChild(); i; i = i->down()) { Q_ASSERT(i->metadata()->geometryTracker()->state() == StateTracker::Geometry::State::VALID); if (i == bve) return; } } void _test_print_state(StateTracker::Content *self) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif auto item = self->root()->firstChild(); if (!item) return; int i = 0; do { qDebug() << i++ << item << item->depth() << item->metadata()->isInSync() << (item->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE); } while((item = item->down())); } void _test_validateUnloaded(StateTracker::Content *self, const QModelIndex& parent, int first, int last) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif Q_ASSERT(self->modelTracker()->modelCandidate()); // This will assert in model_p.cpp for (int i = first; i <= last; i++) { const auto idx = self->modelTracker()->modelCandidate()->index(i, 0, parent); Q_ASSERT(idx.isValid()); //Q_ASSERT(!self->ttiForIndex(idx)); } } static QModelIndex getNextIndex(const QModelIndex& idx) { if (!idx.isValid()) return {}; // There is 2 possibilities, a sibling or a [[great]grand]uncle if (idx.model()->rowCount(idx)) return idx.model()->index(0,0, idx); - auto sib = idx.siblingAtRow(idx.row()+1); + auto sib = idx.sibling(idx.row()+1, idx.column()); if (sib.isValid()) return sib; if (!idx.parent().isValid()) return {}; auto p = idx.parent(); while (p.isValid()) { - sib = p.siblingAtRow(p.row()+1); + sib = p.sibling(p.row()+1, p.column()); if (sib.isValid()) return sib; p = p.parent(); } return {}; } // Validate that there is no holes in the view. void _test_validateContinuity(StateTracker::Content *self) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif auto item = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::TopEdge); auto bve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::BottomEdge); if (!item) return; auto idx = item->index(); do { const auto oldIdx = idx; volatile auto oldItem = item; Q_ASSERT(item->index() == idx); Q_ASSERT(oldIdx != (idx = getNextIndex(idx))); Q_ASSERT(oldItem != (item = item->down())); } while(item && item != bve); } void _test_validateAtEnd(StateTracker::Content *self) { #ifndef ENABLE_EXTRA_VALIDATION return; #endif _test_validateContinuity(self); auto bve = self->edges(IndexMetadata::EdgeType::VISIBLE)->getEdge(Qt::BottomEdge); Q_ASSERT((!self->root()->firstChild()) || bve); //TODO wrong if (!bve) return; const auto next = getNextIndex(bve->index()); Q_ASSERT(!next.isValid()); } void _test_validateModelAboutToReplace(StateTracker::Content *self) { Q_ASSERT(!self->modelTracker()->modelCandidate()); Q_ASSERT(self->modelTracker()->state() == StateTracker::Model::State::NO_MODEL || self->modelTracker()->state() == StateTracker::Model::State::PAUSED); } void StateTracker::Index::_test_bridgeGap(StateTracker::Index *first, StateTracker::Index *second) { if (second && first && second->m_pParent && second->m_pParent->firstChild() ==second && second->m_pParent == first->m_pParent) { second->m_pParent->m_tChildren[0] = first; Q_ASSERT((!first) || second->m_pParent->lastChild()); Q_ASSERT((!first) || !first->previousSibling()); } if ((!first) && second->m_MoveToRow != -1) { Q_ASSERT(second->m_pParent->firstChild()); Q_ASSERT(second->m_pParent->firstChild() ==second || second->m_pParent->firstChild()->m_Index.row() < second->m_MoveToRow); } if (first) Q_ASSERT(first->m_pParent->firstChild()); if (second) Q_ASSERT(second->m_pParent->firstChild()); if (first) Q_ASSERT(first->m_pParent->lastChild()); if (second) Q_ASSERT(second->m_pParent->lastChild()); Q_ASSERT((!second) || (!second->m_pParent->firstChild()) || second->m_pParent->lastChild()); // if (first && second) { //Need to disable other asserts in down() // Q_ASSERT(first->down() == second); // Q_ASSERT(second->up() == first); // } // Close the gap between the old previous and next elements Q_ASSERT((!first ) || first->nextSibling() != first ); Q_ASSERT((!first ) || first->previousSibling() != first ); Q_ASSERT((!second) || second->nextSibling() != second); Q_ASSERT((!second) || second->previousSibling() != second); } #endif //ENABLE_EXTRA_VALIDATION diff --git a/src/private/statetracker/content_p.cpp b/src/private/statetracker/content_p.cpp index aa10c71..6618f49 100644 --- a/src/private/statetracker/content_p.cpp +++ b/src/private/statetracker/content_p.cpp @@ -1,1046 +1,1046 @@ /*************************************************************************** * Copyright (C) 2017-2018 by Emmanuel Lepage Vallee * * Author : Emmanuel Lepage Vallee * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * **************************************************************************/ #include "content_p.h" // KQuickItemViews #include "viewitem_p.h" #include "proximity_p.h" #include "model_p.h" #include "modelitem_p.h" #include #include #include #include #include // Qt #include using EdgeType = IndexMetadata::EdgeType; namespace StateTracker { class Index; class Model; class Content; } class AbstractItemAdapter; class ModelRect; #include #include class ContentPrivate : public QObject { Q_OBJECT public: explicit ContentPrivate(); // Helpers StateTracker::ModelItem* addChildren(const QModelIndex& index); StateTracker::ModelItem* ttiForIndex(const QModelIndex& idx) const; bool isInsertActive(const QModelIndex& p, int first, int last) const; QList setTemporaryIndices(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row); void resetTemporaryIndices(const QList&); void reloadEdges(); StateTracker::ModelItem *m_pRoot {nullptr}; //TODO add a circular buffer to GC the items // * relative index: when to trigger GC // * absolute array index: kept in the StateTracker::ModelItem so it can // remove itself /// All elements with loaded children QHash m_hMapper; QModelIndex getNextIndex(const QModelIndex& idx) const; ModelRect m_lRects[3]; StateTracker::Model *m_pModelTracker; Viewport *m_pViewport; StateTracker::Content* q_ptr; // Update the ModelRect void insertEdge(IndexMetadata *, StateTracker::ModelItem::State); void removeEdge(IndexMetadata *, StateTracker::ModelItem::State); void enterState(IndexMetadata *, StateTracker::ModelItem::State); void leaveState(IndexMetadata *, StateTracker::ModelItem::State); void error (IndexMetadata *, StateTracker::ModelItem::State); void resetState(IndexMetadata *, StateTracker::ModelItem::State); typedef void(ContentPrivate::*StateFS)(IndexMetadata*, StateTracker::ModelItem::State); static const StateFS m_fStateLogging[8][2]; public Q_SLOTS: void slotCleanup(); void slotRowsInserted (const QModelIndex& parent, int first, int last); void slotRowsRemoved (const QModelIndex& parent, int first, int last); void slotLayoutChanged ( ); void slotDataChanged (const QModelIndex& tl, const QModelIndex& br, const QVector &roles ); void slotRowsMoved (const QModelIndex &p, int start, int end, const QModelIndex &dest, int row); }; #define A &ContentPrivate:: // Keep track of the number of instances per state const ContentPrivate::StateFS ContentPrivate::m_fStateLogging[8][2] = { /* ENTER LEAVE */ /*NEW */ { A error , A leaveState }, /*BUFFER */ { A enterState, A leaveState }, /*REMOVED */ { A enterState, A leaveState }, /*REACHABLE*/ { A enterState, A leaveState }, /*VISIBLE */ { A insertEdge, A removeEdge }, /*ERROR */ { A enterState, A leaveState }, /*DANGLING */ { A enterState, A error }, /*MOVING */ { A enterState, A resetState }, }; #undef A void ContentPrivate::insertEdge(IndexMetadata *md, StateTracker::ModelItem::State s) { auto tti = md->modelTracker(); Q_UNUSED(s) const auto first = q_ptr->edges(EdgeType::VISIBLE)->getEdge( Qt::TopEdge ); const auto last = q_ptr->edges(EdgeType::VISIBLE)->getEdge( Qt::BottomEdge ); const auto prev = tti->up(); const auto next = tti->down(); if (first == next) q_ptr->setEdge(EdgeType::VISIBLE, tti, Qt::TopEdge); if (last == prev) q_ptr->setEdge(EdgeType::VISIBLE, tti, Qt::BottomEdge); _DO_TEST(_test_validate_edges_simple, q_ptr) } void ContentPrivate::removeEdge(IndexMetadata *md, StateTracker::ModelItem::State s) { Q_UNUSED(s) auto tti = md->modelTracker(); const auto first = q_ptr->edges(EdgeType::VISIBLE)->getEdge(Qt::TopEdge); const auto last = q_ptr->edges(EdgeType::VISIBLE)->getEdge(Qt::BottomEdge); // The item was somewhere in between, not an edge case if ((tti != first) && (tti != last)) return; auto prev = tti->up(); auto next = tti->down(); if (tti == first) q_ptr->setEdge(EdgeType::VISIBLE, (next && next->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE) ? next : nullptr, Qt::TopEdge ); if (tti == last) q_ptr->setEdge(EdgeType::VISIBLE, (prev && prev->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE) ? prev : nullptr, Qt::BottomEdge ); _DO_TEST(_test_validate_edges_simple, q_ptr) } /** * This method is called when an item moves into a new area and the "real" * new state needs to be determined. */ void ContentPrivate::resetState(IndexMetadata *i, StateTracker::ModelItem::State) { i->modelTracker()->rebuildState(); } ContentPrivate::ContentPrivate() {} StateTracker::Content::Content(Viewport* parent) : QObject(parent), d_ptr(new ContentPrivate()) { d_ptr->m_pViewport = parent; d_ptr->m_pRoot = new StateTracker::ModelItem(parent); d_ptr->m_pModelTracker = new StateTracker::Model(this); d_ptr->q_ptr = this; } StateTracker::Content::~Content() { delete d_ptr->m_pModelTracker; d_ptr->m_pModelTracker = nullptr; delete d_ptr->m_pRoot; } void ContentPrivate::slotRowsInserted(const QModelIndex& parent, int first, int last) { // This method uses modelCandidate() because it needs to be called during // initilization (thus not always directly by QAbstractItemModel::rowsInserted Q_ASSERT(((!parent.isValid()) || parent.model() == m_pModelTracker->modelCandidate()) && first <= last); // It is the job of isInsertActive to decide what's correct if (!isInsertActive(parent, first, last)) { _DO_TEST(_test_validateLinkedList, q_ptr) return; } const auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; //FIXME it is possible if the anchor is at the bottom that the parent // needs to be loaded. But this is currently too not supported. if (!pitem) { _DO_TEST(_test_validateLinkedList, q_ptr) return; } StateTracker::Index *prev = nullptr; //FIXME use up() if (first && pitem) prev = pitem->childrenLookup(m_pModelTracker->modelCandidate()->index(first-1, 0, parent)); // There is no choice here but to load a larger subset to avoid holes, in // theory, edges(EdgeType::FREE)->m_Edges will prevent runaway loading if (pitem->firstChild()) last = std::max(last, pitem->firstChild()->effectiveRow() - 1); if (prev && prev->down()) { m_pViewport->s_ptr->notifyInsert(prev->down()->metadata()); //Q_ASSERT(!TTI(prev->down())->metadata()->isValid()); } else if ((!prev) && (!pitem) && m_pRoot->firstChild()) { Q_ASSERT((!first) && !parent.isValid()); m_pViewport->s_ptr->notifyInsert(m_pRoot->firstChild()->metadata()); //Q_ASSERT(!TTI(m_pRoot->firstChild())->metadata()->isValid()); } else if (pitem && pitem != m_pRoot && pitem->down()) { //Q_ASSERT(!first); m_pViewport->s_ptr->notifyInsert(pitem->down()->metadata()); //TODO check if this one is still correct, right now notifyInsert update the geo //Q_ASSERT(!TTI(pitem->down())->metadata()->isValid()); } //FIXME support smaller ranges for (int i = first; i <= last; i++) { auto idx = m_pModelTracker->modelCandidate()->index(i, 0, parent); Q_ASSERT(idx.isValid() && idx.parent() != idx && idx.model() == m_pModelTracker->modelCandidate()); // If the insertion is sandwiched between loaded items, not doing it // will corrupt the view, but if it's a "tail" insertion, then they // can be discarded. if (!(q_ptr->edges(EdgeType::FREE)->m_Edges & (Qt::BottomEdge | Qt::TopEdge))) { const auto nextIdx = getNextIndex(idx); const auto nextTTI = nextIdx.isValid() ? ttiForIndex(nextIdx) : nullptr; if (!nextTTI) { m_pViewport->s_ptr->refreshVisible(); _DO_TEST(_test_validateLinkedList, q_ptr) return; //FIXME break } } auto e = addChildren(idx); if (pitem->firstChild() && pitem->firstChild()->effectiveRow() == idx.row()+1) { Q_ASSERT(idx.parent() == pitem->firstChild()->effectiveParentIndex()); // m_pViewport->s_ptr->notifyInsert(&TTI(pitem->firstChild())->m_Geometry); StateTracker::Index::insertChildBefore(e, pitem->firstChild(), pitem); } else { StateTracker::Index::insertChildAfter(e, prev, pitem); //FIXME incorrect // m_pViewport->s_ptr->notifyInsert(&TTI(prev)->m_Geometry); } //FIXME It can happen if the previous is out of the visible range Q_ASSERT( e->previousSibling() || e->nextSibling() || e->effectiveRow() == 0); //TODO merge with bridgeGap if (prev) { StateTracker::Index::bridgeGap(prev, e); _DO_TEST_IDX(_test_validate_chain, prev->parent()) } // This is required before ::ATTACH because otherwise ::down() wont work Q_ASSERT((!pitem->firstChild()) || !pitem->firstChild()->hasTemporaryIndex()); //TODO const bool needDownMove = (!pitem->lastChild()) || e->effectiveRow() > pitem->lastChild()->effectiveRow(); const bool needUpMove = (!pitem->firstChild()) || e->effectiveRow() < pitem->firstChild()->effectiveRow(); _DO_TEST_IDX(_test_validate_chain, e->parent()) // The item is about the current parent first item if (needUpMove) { // Q_ASSERT(false); //TODO merge with the other bridgeGap above m_pViewport->s_ptr->notifyInsert(m_pRoot->firstChild()->metadata()); Q_ASSERT((!m_pRoot->firstChild()) || !m_pRoot->firstChild()->metadata()->isValid()); StateTracker::Index::insertChildBefore(e, nullptr, pitem); } _DO_TEST_IDX(_test_validate_chain, e->parent()) Q_ASSERT(e->metadata()->geometryTracker()->state() == StateTracker::Geometry::State::INIT); Q_ASSERT(e->state() != StateTracker::ModelItem::State::VISIBLE); // NEW -> REACHABLE, this should never fail if (!e->metadata()->performAction(IndexMetadata::LoadAction::ATTACH)) { qDebug() << "\n\nATTACH FAILED"; _DO_TEST(_test_validateLinkedList, q_ptr) break; } if (needUpMove) { Q_ASSERT(pitem != e); if (auto pe = e->up()) pe->metadata() << IndexMetadata::LoadAction::MOVE; } Q_ASSERT((!pitem->lastChild()) || !pitem->lastChild()->hasTemporaryIndex()); //TODO if (needDownMove) { if (auto ne = e->down()) { Q_ASSERT(!ne->metadata()->isValid()); ne->metadata() << IndexMetadata::LoadAction::MOVE; Q_ASSERT(!ne->metadata()->isValid()); } } int rc = m_pModelTracker->modelCandidate()->rowCount(idx); if (rc && q_ptr->edges(EdgeType::FREE)->m_Edges & Qt::BottomEdge) { slotRowsInserted(idx, 0, rc-1); } // Validate early to prevent propagating garbage that's nearly impossible // to debug. if (pitem && pitem != m_pRoot && !i) { Q_ASSERT(e->up() == pitem); Q_ASSERT(e == pitem->down()); } prev = e; } m_pViewport->s_ptr->refreshVisible(); _DO_TEST(_test_validateLinkedList, q_ptr) _DO_TEST_IDX(_test_validate_chain, pitem) Q_EMIT q_ptr->contentChanged(); } void ContentPrivate::slotRowsRemoved(const QModelIndex& parent, int first, int last) { Q_ASSERT((!parent.isValid()) || parent.model() == m_pModelTracker->modelCandidate()); if (!q_ptr->isActive(parent, first, last)) { _DO_TEST(_test_validateLinkedList, q_ptr) _DO_TEST(_test_validateUnloaded, q_ptr, parent, first, last) return; } auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; if (!pitem) return; //TODO make sure the state machine support them //StateTracker::ModelItem *prev(nullptr), *next(nullptr); //FIXME use up() //if (first && pitem) // prev = pitem->m_hLookup.value(model()->index(first-1, 0, parent)); //next = pitem->m_hLookup.value(model()->index(last+1, 0, parent)); //FIXME support smaller ranges for (int i = first; i <= last; i++) { auto idx = m_pModelTracker->modelCandidate()->index(i, 0, parent); if (auto elem = pitem->childrenLookup(idx)) { elem->metadata() << IndexMetadata::LoadAction::HIDE << IndexMetadata::LoadAction::DETACH; } } Q_EMIT q_ptr->contentChanged(); } //TODO optimize this void ContentPrivate::slotDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector &roles) { Q_ASSERT(((!tl.isValid()) || tl.model() == m_pModelTracker->modelCandidate())); Q_ASSERT(((!br.isValid()) || br.model() == m_pModelTracker->modelCandidate())); if (!q_ptr->isActive(tl.parent(), tl.row(), br.row())) return; for (int i = tl.row(); i <= br.row(); i++) { const auto idx = m_pModelTracker->modelCandidate()->index(i, tl.column(), tl.parent()); if (auto tti = ttiForIndex(idx)) { if (tti->metadata()->viewTracker()) { tti->metadata()->contextAdapter()->updateRoles(roles); tti->metadata() << IndexMetadata::ViewAction::UPDATE; } } } } void ContentPrivate::slotLayoutChanged() { if (auto rc = m_pModelTracker->modelCandidate()->rowCount()) slotRowsInserted({}, 0, rc - 1); Q_EMIT q_ptr->contentChanged(); } void ContentPrivate::slotRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { Q_ASSERT((!parent.isValid()) || parent.model() == m_pModelTracker->modelCandidate()); Q_ASSERT((!destination.isValid()) || destination.model() == m_pModelTracker->modelCandidate()); // There is literally nothing to do if (parent == destination && start == row) return; // Whatever has to be done only affect a part that's not currently tracked. if (!q_ptr->isActive(destination, row, row)) { //Q_ASSERT(false); //TODO so I don't forget return; } auto tmp = setTemporaryIndices(parent, start, end, destination, row); // As the actual view is implemented as a daisy chained list, only moving // the edges is necessary for the StateTracker::ModelItem. Each StateTracker::ViewItem // need to be moved. const auto idxStart = m_pModelTracker->modelCandidate()->index(start, 0, parent); const auto idxEnd = m_pModelTracker->modelCandidate()->index(end , 0, parent); Q_ASSERT(idxStart.isValid() && idxEnd.isValid()); //FIXME once partial ranges are supported, this is no longer always valid auto startTTI = ttiForIndex(idxStart); auto endTTI = ttiForIndex(idxEnd); if (end - start == 1) Q_ASSERT(startTTI->nextSibling() == endTTI); //FIXME so I don't forget, it will mess things up if silently ignored Q_ASSERT(startTTI->parent() == endTTI->parent()); //TODO not implemented Q_ASSERT(startTTI && endTTI); //TODO partially loaded move are not implemented auto oldPreviousTTI = startTTI->up(); auto oldNextTTI = endTTI->down(); Q_ASSERT((!oldPreviousTTI) || oldPreviousTTI->down() == startTTI); Q_ASSERT((!oldNextTTI) || oldNextTTI->up() == endTTI); auto newNextIdx = m_pModelTracker->modelCandidate()->index(row, 0, destination); // You cannot move things into an empty model Q_ASSERT((!row) || newNextIdx.isValid()); StateTracker::Index *newNextTTI(nullptr), *newPrevTTI(nullptr); // Rewind until a next element is found, this happens when destination is empty if (!newNextIdx.isValid() && destination.parent().isValid()) { Q_ASSERT(m_pModelTracker->modelCandidate()->rowCount(destination) == row); auto par = destination.parent(); do { if (m_pModelTracker->modelCandidate()->rowCount(par.parent()) > par.row()) { newNextIdx = m_pModelTracker->modelCandidate()->index(par.row(), 0, par.parent()); break; } par = par.parent(); } while (par.isValid()); newNextTTI = ttiForIndex(newNextIdx); } else { newNextTTI = ttiForIndex(newNextIdx); newPrevTTI = newNextTTI ? newNextTTI->up() : nullptr; } if (!row) { auto otherI = ttiForIndex(destination); Q_ASSERT((!newPrevTTI) || otherI == newPrevTTI); } // When there is no next element, then the parent has to be extracted manually if (!(newNextTTI || newPrevTTI)) { if (!row) newPrevTTI = ttiForIndex(destination); else { newPrevTTI = ttiForIndex( destination.model()->index(row-1, 0, destination) ); } } StateTracker::ModelItem* newParentTTI = ttiForIndex(destination); Q_ASSERT(newParentTTI || !destination.isValid()); //TODO not coded yet newParentTTI = newParentTTI ? newParentTTI : m_pRoot; // Remove everything //TODO add batching again StateTracker::Index* tti = endTTI; StateTracker::Index* dest = newNextTTI && newNextTTI->parent() == newParentTTI ? newNextTTI : nullptr; QList tmp2; do { tmp2 << tti; } while(endTTI != startTTI && (tti = tti->previousSibling()) != startTTI); if (endTTI != startTTI) //FIXME use a better condition tmp2 << startTTI; Q_ASSERT( (end - start) == tmp2.size() - 1); // Check if the point where //TODO this isn't good enough bool isRangeVisible = (newPrevTTI && newPrevTTI->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE) || (newNextTTI && newNextTTI->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE); const auto topEdge = q_ptr->edges(EdgeType::VISIBLE)->getEdge(Qt::TopEdge); const auto bottomEdge = q_ptr->edges(EdgeType::VISIBLE)->getEdge(Qt::TopEdge); bool needRefreshVisibleTop = false; bool needRefreshVisibleBottom = false; /*bool needRefreshBufferTop = false; //TODO lateral move is not implemented bool needRefreshBufferBottom = false;*/ for (auto item : qAsConst(tmp2)) { needRefreshVisibleTop |= item != topEdge; needRefreshVisibleBottom |= item != bottomEdge; item->metadata() << IndexMetadata::GeometryAction::MOVE; item->remove(); StateTracker::Index::insertChildBefore(item, dest, newParentTTI); Q_ASSERT((!dest) || item->down() == dest); if (dest) dest->metadata() << IndexMetadata::GeometryAction::MOVE; _DO_TEST(_test_validate_edges_simple, q_ptr) m_pViewport->s_ptr->notifyInsert(item->metadata()); dest = item; if (isRangeVisible) { if (item->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::BUFFER) item->metadata() << IndexMetadata::LoadAction::ATTACH; item->metadata() << IndexMetadata::LoadAction::SHOW; } } _DO_TEST(_test_validate_move, q_ptr, newParentTTI, startTTI, endTTI, newPrevTTI, newNextTTI, row) resetTemporaryIndices(tmp); // The visible edges are now probably incorectly placed, reload them if (needRefreshVisibleTop || needRefreshVisibleBottom) { reloadEdges(); m_pViewport->s_ptr->refreshVisible(); } //WARNING The indices still are in transition mode, do not use their value } QList ContentPrivate::setTemporaryIndices(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { //FIXME This exists but for now ignoring it hasn't caused too many problems if (parent != destination) { qWarning() << "setTemporaryIndices called with corrupted arguments"; return {}; } QList ret; // Before moving them, set a temporary now/col value because it wont be set // on the index until before endMoveRows is called (but after this // method returns). const auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; for (int i = start; i <= end; i++) { auto idx = m_pModelTracker->modelCandidate()->index(i, 0, parent); //TODO do not use the hashmap, it is already known auto elem = pitem->childrenLookup(idx); Q_ASSERT(elem); elem->setTemporaryIndex( destination, row + (i - start), idx.column() ); ret << elem; } for (int i = row; i <= row + (end - start); i++) { auto idx = m_pModelTracker->modelCandidate()->index(i, 0, parent); //TODO do not use the hashmap, it is already known auto elem = pitem->childrenLookup(idx); Q_ASSERT(elem); elem->setTemporaryIndex( destination, row + (end - start) + 1, idx.column() ); ret << elem; } return ret; } void ContentPrivate::resetTemporaryIndices(const QList& indices) { for (auto i : qAsConst(indices)) i->resetTemporaryIndex(); } void StateTracker::Content::resetEdges() { for (int i = 0; i < (int) EdgeType::NONE; i++) for (const auto e : {Qt::BottomEdge, Qt::TopEdge, Qt::LeftEdge, Qt::RightEdge}) setEdge((EdgeType)i, nullptr, e); } // Go O(N) for now. Optimize when it becomes a problem (read: soon) void ContentPrivate::reloadEdges() { //FIXME optimize this enum Range : uint16_t { TOP = 0x0 << 0, BUFFER_TOP = 0x1 << 0, VISIBLE = 0x1 << 1, BUFFER_BOTTOM = 0x1 << 2, BOTTOM = 0x1 << 3, IS_VISIBLE = 0x1 << 4, IS_NOT_VISIBLE = 0x1 << 5, IS_BUFFER = 0x1 << 6, IS_NOT_BUFFER = 0x1 << 7, }; Range pos = Range::TOP; for (auto item = m_pRoot->firstChild(); item; item = item->down()) { const uint16_t isVisible = item->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::VISIBLE ? IS_VISIBLE : IS_NOT_VISIBLE; const uint16_t isBuffer = item->metadata()->modelTracker()->state() == StateTracker::ModelItem::State::BUFFER ? IS_BUFFER : IS_NOT_BUFFER; switch(pos | isVisible | isBuffer) { case Range::TOP | Range::IS_BUFFER: pos = Range::BUFFER_TOP; q_ptr->setEdge(EdgeType::BUFFERED, item, Qt::TopEdge); break; case Range::TOP | Range::IS_VISIBLE: case Range::BUFFER_TOP | Range::IS_VISIBLE: pos = Range::VISIBLE; q_ptr->setEdge(EdgeType::VISIBLE, item, Qt::TopEdge); break; case Range::VISIBLE | Range::IS_NOT_VISIBLE: case Range::VISIBLE | Range::IS_NOT_BUFFER: q_ptr->setEdge(EdgeType::VISIBLE, item->up(), Qt::BottomEdge); pos = Range::BOTTOM; break; case Range::VISIBLE | Range::IS_NOT_VISIBLE | Range::IS_BUFFER: pos = Range::BUFFER_BOTTOM; break; case Range::VISIBLE | Range::IS_NOT_BUFFER | Range::IS_NOT_VISIBLE: q_ptr->setEdge(EdgeType::BUFFERED, item->up(), Qt::BottomEdge); pos = Range::BOTTOM; break; } } } void ContentPrivate::enterState(IndexMetadata *tti, StateTracker::ModelItem::State s) { Q_UNUSED(tti); Q_UNUSED(s); //TODO count the number of active objects in each states for the "frame" load balancing } void ContentPrivate::leaveState(IndexMetadata *tti, StateTracker::ModelItem::State s) { Q_UNUSED(tti); Q_UNUSED(s); //TODO count the number of active objects in each states for the "frame" load balancing } void ContentPrivate::error(IndexMetadata *, StateTracker::ModelItem::State) { Q_ASSERT(false); } StateTracker::Index *StateTracker::Content::firstItem() const { return root()->firstChild(); } StateTracker::Index *StateTracker::Content::lastItem() const { auto candidate = d_ptr->m_pRoot->lastChild(); while (candidate && candidate->lastChild()) candidate = candidate->lastChild(); return candidate; } bool ContentPrivate::isInsertActive(const QModelIndex& p, int first, int last) const { Q_UNUSED(last) //TODO auto pitem = p.isValid() ? m_hMapper.value(p) : m_pRoot; StateTracker::Index *prev(nullptr); //FIXME use up() if (first && pitem) prev = pitem->childrenLookup(m_pModelTracker->modelCandidate()->index(first-1, 0, p)); if (first && !prev) return false; if (q_ptr->edges(EdgeType::FREE)->m_Edges & (Qt::TopEdge|Qt::BottomEdge)) return true; const auto parent = p.isValid() ? ttiForIndex(p) : m_pRoot; // It's controversial as it means if the items would otherwise be // visible because their parent "virtual" position + insertion height // could happen to be in the view but in this case the scrollbar would // move. Mobile views tend to not shuffle the current content around so // from that point of view it is correct, but if the item is in the // buffer, then it will move so doing this is a bit buggy. return parent != nullptr; } /// Return true if the indices affect the current view bool StateTracker::Content::isActive(const QModelIndex& parent, int first, int last) { if (!parent.isValid()) { // There is nothing loaded, so everything is active (so far) if (!d_ptr->m_pRoot->firstChild()) return true; // There is room for more. Assuming the code that makes sure all items // that can be loaded are, then it only happens when there is room for // more. if (edges(EdgeType::FREE)->m_Edges & (Qt::BottomEdge | Qt::TopEdge)) return true; const QRect loadedRect( d_ptr->m_pRoot->firstChild()->index().column(), d_ptr->m_pRoot->firstChild()->index().row(), d_ptr->m_pRoot->lastChild ()->index().column() + 1, d_ptr->m_pRoot->lastChild ()->index().row() + 1 ); const QRect changedRect( 0, first, 1, //FIXME use columnCount+!? last ); return loadedRect.intersects(changedRect); } //FIXME This method is incomplete // Q_ASSERT(false); //TODO THIS_COMMIT return true; } /// Add new entries to the mapping StateTracker::ModelItem* ContentPrivate::addChildren(const QModelIndex& index) { Q_ASSERT(index.isValid() && !m_hMapper.contains(index)); auto e = new StateTracker::ModelItem(m_pViewport); e->setModelIndex(index); m_hMapper[index] = e; return e; } void ContentPrivate::slotCleanup() { // The whole slotCleanup cycle isn't necessary, it wont find anything. if (!m_pRoot->firstChild()) return; m_pModelTracker << StateTracker::Model::Action::RESET; m_pRoot->metadata() << IndexMetadata::LoadAction::HIDE << IndexMetadata::LoadAction::DETACH; m_hMapper.clear(); m_pRoot = new StateTracker::ModelItem(m_pViewport); } StateTracker::ModelItem* ContentPrivate::ttiForIndex(const QModelIndex& idx) const { if (!idx.isValid()) return nullptr; const auto parent = (!idx.parent().isValid()) ? m_pRoot : m_hMapper.value(idx.parent()); return parent ? static_cast(parent->childrenLookup(idx)) : nullptr; } IndexMetadata *StateTracker::Content::metadataForIndex(const QModelIndex& idx) const { const auto tti = d_ptr->ttiForIndex(idx); return tti ? tti->metadata() : nullptr; } void StateTracker::Content::setAvailableEdges(Qt::Edges es, EdgeType t) { edges(t)->m_Edges = es; } Qt::Edges StateTracker::Content::availableEdges(EdgeType t) const { return edges(t)->m_Edges; } IndexMetadata *StateTracker::Content::getEdge(EdgeType t, Qt::Edge e) const { const auto ret = edges(t)->getEdge(e); return ret ? ret->metadata() : nullptr; } ModelRect* StateTracker::Content::edges(EdgeType e) const { Q_ASSERT((int) e >= 0 && (int)e <= 2); return (ModelRect*) &d_ptr->m_lRects[(int)e]; } // Add more validation to detect possible invalid edges being set void StateTracker::Content:: setEdge(EdgeType et, StateTracker::Index* tti, Qt::Edge e) { //DEBUG this only works with proxy models, not JIT/AOT strategies // if (tti && et == EdgeType::VISIBLE) // Q_ASSERT(TTI(tti)->metadata()->isValid()); edges(et)->setEdge(tti, e); // Only test when `tti` is true because when removing the last item, it // will always need to remove both the top and bottom, so removing the // top will fail the test. if (tti) { _DO_TEST(_test_validate_edges_simple, this) } } StateTracker::Index *StateTracker::Content:: find(StateTracker::Index *from, Qt::Edge direction, std::function cond) const { auto f = from; while(f && cond(f) && f->next(direction) && (f = f->next(direction))); return f; } StateTracker::Model *StateTracker::Content::modelTracker() const { return d_ptr->m_pModelTracker; } //DEPRECATED use StateTracker::Proximity QModelIndex ContentPrivate::getNextIndex(const QModelIndex& idx) const { // There is 2 possibilities, a sibling or a [[great]grand]uncle if (m_pModelTracker->modelCandidate()->rowCount(idx)) return m_pModelTracker->modelCandidate()->index(0,0, idx); - auto sib = idx.siblingAtRow(idx.row()+1); + auto sib = idx.sibling(idx.row()+1, idx.column()); if (sib.isValid()) return sib; if (!idx.parent().isValid()) return {}; auto p = idx.parent(); while (p.isValid()) { - sib = p.siblingAtRow(p.row()+1); + sib = p.sibling(p.row()+1, p.column()); if (sib.isValid()) return sib; p = p.parent(); } return {}; } void StateTracker::Content::connectModel(QAbstractItemModel *m) { QObject::connect(m, &QAbstractItemModel::rowsInserted, d_ptr, &ContentPrivate::slotRowsInserted ); QObject::connect(m, &QAbstractItemModel::rowsAboutToBeRemoved, d_ptr, &ContentPrivate::slotRowsRemoved ); QObject::connect(m, &QAbstractItemModel::layoutAboutToBeChanged, d_ptr, &ContentPrivate::slotCleanup); QObject::connect(m, &QAbstractItemModel::layoutChanged, d_ptr, &ContentPrivate::slotLayoutChanged); QObject::connect(m, &QAbstractItemModel::modelAboutToBeReset, d_ptr, &ContentPrivate::slotCleanup); QObject::connect(m, &QAbstractItemModel::modelReset, d_ptr, &ContentPrivate::slotLayoutChanged); QObject::connect(m, &QAbstractItemModel::rowsAboutToBeMoved, d_ptr, &ContentPrivate::slotRowsMoved); QObject::connect(m, &QAbstractItemModel::dataChanged, d_ptr, &ContentPrivate::slotDataChanged); #ifdef ENABLE_EXTRA_VALIDATION QObject::connect(m, &QAbstractItemModel::rowsMoved, d_ptr, [this](){_DO_TEST(_test_validateLinkedList, this);}); QObject::connect(m, &QAbstractItemModel::rowsRemoved, d_ptr, [this](){_DO_TEST(_test_validateLinkedList, this);}); #endif } void StateTracker::Content::disconnectModel(QAbstractItemModel *m) { QObject::disconnect(m, &QAbstractItemModel::rowsInserted, d_ptr, &ContentPrivate::slotRowsInserted); QObject::disconnect(m, &QAbstractItemModel::rowsAboutToBeRemoved, d_ptr, &ContentPrivate::slotRowsRemoved); QObject::disconnect(m, &QAbstractItemModel::layoutAboutToBeChanged, d_ptr, &ContentPrivate::slotCleanup); QObject::disconnect(m, &QAbstractItemModel::layoutChanged, d_ptr, &ContentPrivate::slotLayoutChanged); QObject::disconnect(m, &QAbstractItemModel::modelAboutToBeReset, d_ptr, &ContentPrivate::slotCleanup); QObject::disconnect(m, &QAbstractItemModel::modelReset, d_ptr, &ContentPrivate::slotLayoutChanged); QObject::disconnect(m, &QAbstractItemModel::rowsAboutToBeMoved, d_ptr, &ContentPrivate::slotRowsMoved); QObject::disconnect(m, &QAbstractItemModel::dataChanged, d_ptr, &ContentPrivate::slotDataChanged); #ifdef ENABLE_EXTRA_VALIDATION // QObject::disconnect(m, &QAbstractItemModel::rowsMoved, d_ptr, // [this](){d_ptr->_test_validateLinkedList();}); // QObject::disconnect(m, &QAbstractItemModel::rowsRemoved, d_ptr, // [this](){d_ptr->_test_validateLinkedList();}); #endif } StateTracker::Index *StateTracker::Content::root() const { return d_ptr->m_pRoot; } void StateTracker::Content::resetRoot() { root()->metadata() << IndexMetadata::LoadAction::RESET; for (int i = 0; i < 3; i++) d_ptr->m_lRects[i] = {}; d_ptr->m_hMapper.clear(); delete d_ptr->m_pRoot; d_ptr->m_pRoot = new StateTracker::ModelItem(d_ptr->m_pViewport); } void StateTracker::Content::perfromStateChange(Event e, IndexMetadata *md, StateTracker::ModelItem::State s) { const auto ms = e == Event::LEAVE_STATE ? s : md->modelTracker()->state(); (d_ptr->*ContentPrivate::m_fStateLogging[(int)ms][(int)e])(md, s); } void StateTracker::Content::forceInsert(const QModelIndex& idx) { //TODO add some safety check or find a way to get rid of this hack d_ptr->slotRowsInserted(idx.parent(), idx.row(), idx.row()); } // Also make sure not to load the same element twice void StateTracker::Content::forceInsert(const QModelIndex& parent, int first, int last) { auto parNode = parent.isValid() ? d_ptr->m_hMapper[parent] : d_ptr->m_pRoot; // If the parent isn't loaded, there is no risk of collision if (!parNode) { //TODO loading the parent chain of `parent` is needed d_ptr->slotRowsInserted(parent, first, last); return; } // If the parent has no children elements, then there is no collisions if (!parNode->firstChild()) { d_ptr->slotRowsInserted(parent, first, last); return; } // If the range isn't overlapping, there is no collisions if (parNode->lastChild()->effectiveRow() < first || parNode->firstChild()->effectiveRow() > last) { d_ptr->slotRowsInserted(parent, first, last); return; } // If there is a continuity encompassing `first` and `last` then there // is nothing to do. if (parNode->firstChild()->continuityTracker()->size() >= last) { return; } //TODO the case above only works if firstChild()->row() == 0. If another // case is hit, then it will assert in slotRowsInserted d_ptr->slotRowsInserted(parent, first, last); } #include diff --git a/src/private/statetracker/proximity_p.cpp b/src/private/statetracker/proximity_p.cpp index fb50e2d..fae1df6 100644 --- a/src/private/statetracker/proximity_p.cpp +++ b/src/private/statetracker/proximity_p.cpp @@ -1,219 +1,219 @@ /*************************************************************************** * Copyright (C) 2018 by Emmanuel Lepage Vallee * * Author : Emmanuel Lepage Vallee * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * **************************************************************************/ #include "proximity_p.h" #include "index_p.h" using State = StateTracker::Proximity::State; class ProximityPrivate { public: void nothing() {} QModelIndexList up () const; QModelIndexList down() const; typedef void(ProximityPrivate::*StateF)(); static const State m_fStateMap [6][3]; static const StateF m_fStateMachine[6][3]; State m_State[4] { State::UNKNOWN, State::UNKNOWN, State::UNKNOWN, State::UNKNOWN, }; StateTracker::Index *m_pSelf; IndexMetadata *q_ptr; }; #define S StateTracker::Proximity::State:: const StateTracker::Proximity::State ProximityPrivate::m_fStateMap[6][3] = { /* QUERY DISCARD MOVE */ /* UNKNOWN */ { S UNKNOWN, S UNKNOWN, S UNKNOWN }, /* LOADED */ { S UNKNOWN, S UNKNOWN, S UNKNOWN }, /* MOVED */ { S UNKNOWN, S UNKNOWN, S UNKNOWN }, /* UNLOADED_TOP */ { S UNKNOWN, S UNKNOWN, S UNKNOWN }, /* UNLOADED_BOTTOM */ { S UNKNOWN, S UNKNOWN, S UNKNOWN }, /* UNLOADED_BOTH */ { S UNKNOWN, S UNKNOWN, S UNKNOWN }, }; #undef S #define A &ProximityPrivate:: const ProximityPrivate::StateF ProximityPrivate::m_fStateMachine[6][3] = { /* QUERY DISCARD MOVE */ /* UNKNOWN */ { A nothing, A nothing, A nothing }, /* LOADED */ { A nothing, A nothing, A nothing }, /* MOVED */ { A nothing, A nothing, A nothing }, /* UNLOADED_TOP */ { A nothing, A nothing, A nothing }, /* UNLOADED_BOTTOM */ { A nothing, A nothing, A nothing }, /* UNLOADED_BOTH */ { A nothing, A nothing, A nothing }, }; #undef A StateTracker::Proximity::Proximity(IndexMetadata *q, StateTracker::Index *self) : d_ptr(new ProximityPrivate()) { d_ptr->q_ptr = q; d_ptr->m_pSelf = self; } void StateTracker::Proximity::performAction(IndexMetadata::ProximityAction a, Qt::Edge e) { Q_UNUSED(e) Q_UNUSED(a) Q_ASSERT(false); } bool StateTracker::Proximity::canLoadMore(Qt::Edge e) { Q_UNUSED(e) Q_ASSERT(false); return false; } QModelIndexList StateTracker::Proximity::getNext(Qt::Edge e) { //TODO right now this will return garbage, this will need some more code to handle Q_ASSERT(d_ptr->m_pSelf->lifeCycleState() != StateTracker::Index::LifeCycleState::TRANSITION); //TODO switch (e) { case Qt::TopEdge: return d_ptr->up(); case Qt::BottomEdge: return d_ptr->down(); case Qt::LeftEdge: case Qt::RightEdge: Q_ASSERT(false); //TODO } return {}; } QModelIndexList ProximityPrivate::up() const { QModelIndexList ret; const int r = m_pSelf->effectiveRow(); if (r) { // When there is a previous sibling, return its last (grant) children // or return that sibling. auto idx = m_pSelf->index().model()->index( r - 1, m_pSelf->index().column(), m_pSelf->effectiveParentIndex() ); if (!idx.isValid()) return ret; ret << idx; Q_ASSERT(idx.isValid()); // Find the deepest (grang)children while (auto rc = m_pSelf->index().model()->rowCount(idx)) { ret << (idx = m_pSelf->index().model()->index( rc - 1, 0, m_pSelf->effectiveParentIndex() )); Q_ASSERT(idx.isValid()); } return ret; } else { // Up is either the parent or a (grand) children of the parent auto idx = m_pSelf->effectiveParentIndex(); if (!idx.isValid()) return ret; ret << idx; // Find the deepest (grang)children while (auto rc = m_pSelf->index().model()->rowCount(idx)) { if (!idx.isValid()) break; ret << (idx = m_pSelf->index().model()->index( rc - 1, 0, m_pSelf->effectiveParentIndex() )); } return ret; } //TODO recurse parent->rowCount..->last Q_ASSERT(m_pSelf->parent() && m_pSelf->parent()->lifeCycleState() == StateTracker::Index::LifeCycleState::ROOT ); return ret; } QModelIndexList ProximityPrivate::down() const { int effRow = m_pSelf->effectiveRow(); QModelIndex par = m_pSelf->effectiveParentIndex(); // In *theory*, all relevant parents should be loaded, so it's always // returning a single item // Return the first child if (m_pSelf->index().model()->rowCount(m_pSelf->index())) return {m_pSelf->index().model()->index(0, 0, m_pSelf->index())}; // Return the next sibling if (m_pSelf->index().model()->rowCount(par)-1 > effRow) return {m_pSelf->index().model()->index(effRow+1, 0, par)}; while (par.isValid()) { - auto sib = par.siblingAtRow(par.row()+1); + auto sib = par.sibling(par.row()+1, par.column()); if (sib.isValid()) return {sib}; par = par.parent(); } return {}; } /* if (auto rc = m_pModel->rowCount(idx)) return m_pModel->index(0,0, idx); auto sib = idx.siblingAtRow(idx.row()+1); if (sib.isValid()) return sib; if (!idx.parent().isValid()) return {}; auto p = idx.parent(); while (p.isValid()) { sib = p.siblingAtRow(p.row()+1); if (sib.isValid()) return sib; p = p.parent(); } return {}; */