diff --git a/src/qmlwidgets/flickableview.h b/src/qmlwidgets/flickableview.h index e0cb90f2..2cd48370 100644 --- a/src/qmlwidgets/flickableview.h +++ b/src/qmlwidgets/flickableview.h @@ -1,174 +1,172 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * 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 . * **************************************************************************/ #pragma once #include #include //For the Q_DECLARE_METATYPE // Qt #include #include class QQmlComponent; class QQmlContext; class QItemSelectionModel; class FlickableViewPrivate; /** * This widget bridges the simple Flickable "cartesian plan" widget with a * model view. * * It allows the various view (hierarchy, list, table, tree) to share a common * API and be drop-in replacement for each other if the model topology is ever * extended. * - * This is a base class and is not intended to be used directly. - * * This is an abstract class and expect the implementations to properly * manage the `ModelIndexItem` elements. In return, this class provides an * unified way to handle the selection and drag&drop boiler plate code. */ class FlickableView : public SimpleFlickable { Q_OBJECT public: /** * Abstract base class of the visual representation of a QModelIndex. */ class ModelIndexItem { public: virtual ~ModelIndexItem() {} explicit ModelIndexItem(FlickableView* view); /// Geometry relative to the FlickableView::view() virtual QRectF geometry() const = 0; /// Visibility relative to the displayed window of the FlickableView::view() virtual bool isVisible() const = 0; /// Get a weak pointer into itself so the implementation can notify of deletion virtual QWeakPointer reference() const = 0; /// Allow implementations to be notified when it becomes selected virtual void setSelected(bool) {} /// The model index virtual QPersistentModelIndex index() const = 0; // Spacial navigation virtual ModelIndexItem* up () const { return nullptr ;} virtual ModelIndexItem* down () const { return nullptr ;} virtual ModelIndexItem* left () const { return nullptr ;} virtual ModelIndexItem* right() const { return nullptr ;} virtual int row () const { return index().row () ;} virtual int column() const { return index().column() ;} //TODO ::above() and ::firstBelow() and ::lastBelow() /// The number of parent items virtual int depth() const {return 0;} /// Reference to the item own view FlickableView* view() const; virtual QQuickItem* item() const { return nullptr; } /// Call to notify that the geometry changed (for the selection delegate) void updateGeometry(); private: FlickableView* m_pView {nullptr}; }; Q_PROPERTY(QSharedPointer model READ model WRITE setModel NOTIFY modelChanged) Q_PROPERTY(QAbstractItemModel* rawModel WRITE setRawModel) Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate) Q_PROPERTY(Qt::Corner gravity READ gravity WRITE setGravity) Q_PROPERTY(QQmlComponent* highlight READ highlight WRITE setHighlight) Q_PROPERTY(QSharedPointer selectionModel READ selectionModel WRITE setSelectionModel NOTIFY selectionModelChanged) Q_PROPERTY(bool sortingEnabled READ isSortingEnabled WRITE setSortingEnabled) Q_PROPERTY(bool empty READ isEmpty NOTIFY countChanged) // QML support for selectionModels is rather bad since many Q_INVOKABLE are missing Q_PROPERTY(QModelIndex currentIndex READ currentIndex WRITE setCurrentIndex) explicit FlickableView(QQuickItem* parent = nullptr); virtual ~FlickableView(); virtual void setModel(QSharedPointer model); QSharedPointer model() const; void setRawModel(QAbstractItemModel* m); virtual void setDelegate(QQmlComponent* delegate); QQmlComponent* delegate() const; QQmlContext* rootContext() const; Qt::Corner gravity() const; void setGravity(Qt::Corner g); QQmlComponent* highlight() const; void setHighlight(QQmlComponent* h); QSharedPointer selectionModel() const; void setSelectionModel(QSharedPointer m); QModelIndex currentIndex() const; Q_INVOKABLE void setCurrentIndex(const QModelIndex& index, QItemSelectionModel::SelectionFlags f = QItemSelectionModel::ClearAndSelect ); bool isSortingEnabled() const; void setSortingEnabled(bool val); bool isEmpty() const; protected: virtual void refresh(); void applyRoles(QQmlContext* ctx, const QModelIndex& self) const; QPair loadDelegate(QQuickItem* parentI, QQmlContext* parentCtx, const QModelIndex& self) const; /** * To be implemented by the final class. */ virtual ModelIndexItem* createItem() const = 0; /** * Get the VolatileTreeItem associated with a model index. * * Note that if the index is not currently visible or buferred, it will * return nullptr. */ virtual ModelIndexItem* itemForIndex(const QModelIndex& idx) const = 0; Q_SIGNALS: void currentIndexChanged(const QModelIndex& index); void modelChanged(QSharedPointer model); void selectionModelChanged() const; void countChanged(); private: FlickableViewPrivate* d_ptr; Q_DECLARE_PRIVATE(FlickableView) }; Q_DECLARE_METATYPE(FlickableView*) Q_DECLARE_METATYPE(QSharedPointer) diff --git a/src/qmlwidgets/treeview2.cpp b/src/qmlwidgets/treeview2.cpp index c866993d..f72fa4d7 100644 --- a/src/qmlwidgets/treeview2.cpp +++ b/src/qmlwidgets/treeview2.cpp @@ -1,1651 +1,1656 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * 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 "treeview2.h" #include #include #define V_ITEM(i) static_cast(i) /* * Design: * * This class implement 3 embedded state machines. * * The first machine has a single instance and manage the whole view. It allows * different behavior (corner case) to be handled in a stateful way without "if". * It tracks a moving window of elements coarsely equivalent to the number of * elements on screen. * * The second layer reflects the model and corresponds to a QModelIndex currently * being tracked by the system. They are lazy-loaded and mostly unaware of the * model topology. While they are hierarchical, they are so compared to themselves, * not the actual model hierarchy. This allows the model to have unlimited depth * without any performance impact. * * The last layer of state machine represents the visual elements. Being separated * from the model view allows the QQuickItem elements to fail to load without * spreading havoc. This layer also implements an abstraction that allows the * tree to be browsed as a list. This greatly simplifies the widget code. The * abstract class has to be implemented by the view implementations. All the * heavy lifting is done in this class and the implementation can assume all is * valid and perform minimal validation. * */ class VisualTreeItemPrivate { public: }; /* * The visual elements state changes. * * Note that the ::FAILED elements will always try to self-heal themselves and * go back into FAILED once the self-healing itself failed. */ #define S VisualTreeItem::State:: const VisualTreeItem::State VisualTreeItem::m_fStateMap[7][7] = { /* ATTACH ENTER_BUFFER ENTER_VIEW UPDATE MOVE LEAVE_BUFFER DETACH */ /*POOLING */ { S POOLING, S BUFFER, S ERROR , S ERROR , S ERROR , S ERROR , S DANGLING }, /*POOLED */ { S POOLED , S BUFFER, S ERROR , S ERROR , S ERROR , S ERROR , S DANGLING }, /*BUFFER */ { S ERROR , S ERROR , S ACTIVE, S BUFFER, S ERROR , S POOLING, S DANGLING }, /*ACTIVE */ { S ERROR , S BUFFER, S ERROR , S ACTIVE, S ACTIVE, S POOLING, S POOLING }, /*FAILED */ { S ERROR , S BUFFER, S ACTIVE, S ACTIVE, S ACTIVE, S POOLING, S DANGLING }, /*DANGLING*/ { S ERROR , S ERROR , S ERROR , S ERROR , S ERROR , S ERROR , S DANGLING }, /*ERROR */ { S ERROR , S ERROR , S ERROR , S ERROR , S ERROR , S ERROR , S DANGLING }, }; #undef S #define A &VisualTreeItem:: const VisualTreeItem::StateF VisualTreeItem::m_fStateMachine[7][7] = { /* ATTACH ENTER_BUFFER ENTER_VIEW UPDATE MOVE LEAVE_BUFFER DETACH */ /*POOLING */ { A error , A error , A error , A error , A error , A error , A error }, /*POOLED */ { A nothing, A attach , A error , A error , A error , A error , A destroy }, /*BUFFER */ { A error , A error , A move , A refresh, A error , A detach , A destroy }, /*ACTIVE */ { A error , A nothing, A error , A refresh, A move , A detach , A detach }, /*FAILED */ { A error , A attach , A attach , A attach , A attach , A detach , A destroy }, /*DANGLING*/ { A error , A error , A error , A error , A error , A error , A destroy }, /*error */ { A error , A error , A error , A error , A error , A error , A destroy }, }; #undef A /** * Hold the QPersistentModelIndex and the metadata associated with them. */ struct TreeTraversalItems { explicit TreeTraversalItems(TreeTraversalItems* parent, TreeView2Private* d): m_pParent(parent), d_ptr(d) {} enum class State { BUFFER = 0, /*!< Not in use by any visible indexes, but pre-loaded */ REMOVED = 1, /*!< Currently in a removal transaction */ REACHABLE = 2, /*!< The [grand]parent of visible indexes */ VISIBLE = 3, /*!< The element is visible on screen */ ERROR = 4, /*!< Something went wrong */ DANGLING = 5, /*!< Being destroyed */ }; enum class Action { SHOW = 0, /*!< Make visible on screen (or buffer) */ HIDE = 1, /*!< Remove from the screen (or buffer) */ ATTACH = 2, /*!< Track, but do not show */ DETACH = 3, /*!< Stop tracking for changes */ UPDATE = 4, /*!< Update the element */ MOVE = 5, /*!< Update the depth and lookup */ }; typedef bool(TreeTraversalItems::*StateF)(); static const State m_fStateMap [6][6]; static const StateF m_fStateMachine[6][6]; bool performAction(Action); bool nothing(); bool error (); bool show (); bool hide (); bool attach (); bool detach (); bool refresh(); bool index (); bool destroy(); //TODO use a btree, not an hash QHash m_hLookup; uint m_Depth {0}; State m_State {State::BUFFER}; // Keep the parent to be able to get back to the root TreeTraversalItems* m_pParent; // There is no need to keep an ordered vector, random access is not // necessary. However to keep the double chained list in sync, the edges // are. TreeTraversalItems* m_pFirstChild {nullptr}; TreeTraversalItems* m_pLastChild {nullptr}; // Those are only for the elements in the same depth level TreeTraversalItems* m_pPrevious {nullptr}; TreeTraversalItems* m_pNext {nullptr}; QPersistentModelIndex m_Index; VisualTreeItem* m_pTreeItem {nullptr}; TreeView2Private* d_ptr; }; #define S TreeTraversalItems::State:: const TreeTraversalItems::State TreeTraversalItems::m_fStateMap[6][6] = { /* SHOW HIDE ATTACH DETACH UPDATE MOVE */ /*BUFFER */ { S VISIBLE, S BUFFER , S REACHABLE, S DANGLING , S BUFFER , S BUFFER }, /*REMOVED */ { S ERROR , S ERROR , S ERROR , S BUFFER , S ERROR , S ERROR }, /*REACHABLE*/ { S VISIBLE, S REACHABLE, S ERROR , S BUFFER , S ERROR , S REACHABLE }, /*VISIBLE */ { S VISIBLE, S REACHABLE, S ERROR , S BUFFER , S VISIBLE, S VISIBLE }, /*ERROR */ { S ERROR , S ERROR , S ERROR , S ERROR , S ERROR , S ERROR }, /*DANGLING */ { S ERROR , S ERROR , S ERROR , S ERROR , S ERROR , S ERROR }, }; #undef S #define A &TreeTraversalItems:: const TreeTraversalItems::StateF TreeTraversalItems::m_fStateMachine[6][6] = { /* SHOW HIDE ATTACH DETACH UPDATE MOVE */ /*BUFFER */ { A show , A nothing, A attach, A destroy , A refresh, A index }, /*REMOVED */ { A error , A error , A error , A detach , A error , A error }, /*REACHABLE*/ { A show , A nothing, A error , A detach , A error , A index }, /*VISIBLE */ { A nothing, A hide , A error , A detach , A refresh, A index }, /*ERROR */ { A error , A error , A error , A error , A error , A error }, /*DANGLING */ { A error , A error , A error , A error , A error , A error }, }; #undef A class TreeView2Private : public QObject { Q_OBJECT public: enum class State { UNFILLED, /*!< There is less items that the space available */ ANCHORED, /*!< Some items are out of view, but it's at the beginning */ SCROLLED, /*!< It's scrolled to a random point */ AT_END , /*!< It's at the end of the items */ ERROR , /*!< Something went wrong */ }; enum class Action { INSERTION = 0, REMOVAL = 1, MOVE = 2, RESET_SCROLL = 3, SCROLL = 4, }; typedef bool(TreeView2Private::*StateF)(); static const State m_fStateMap [5][5]; static const StateF m_fStateMachine[5][5]; bool m_UniformRowHeight {false}; bool m_UniformColumnWidth {false}; bool m_Collapsible {true }; bool m_AutoExpand {false}; int m_MaxDepth { -1 }; int m_CacheBuffer { 10 }; int m_PoolSize { 10 }; int m_FailedCount { 0 }; TreeView2::RecyclingMode m_RecyclingMode { TreeView2::RecyclingMode::NoRecycling }; State m_State {State::UNFILLED}; TreeTraversalItems* m_pRoot {new TreeTraversalItems(nullptr, this)}; /// All elements with loaded children QHash m_hMapper; // Helpers bool isActive(const QModelIndex& parent, int first, int last); void initTree(const QModelIndex& parent); TreeTraversalItems* addChildren(TreeTraversalItems* parent, const QModelIndex& index); void bridgeGap(TreeTraversalItems* first, TreeTraversalItems* second, bool insert = false); void bridgeGap(VisualTreeItem* first, VisualTreeItem* second, bool insert = false); void createGap(VisualTreeItem* first, VisualTreeItem* last ); void setTemporaryIndices(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row); void resetTemporaryIndices(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row); // Tests void _test_validateTree(TreeTraversalItems* p); TreeView2* q_ptr; private: bool nothing (); bool resetScoll (); bool refresh (); bool refreshFront(); bool refreshBack (); bool error () __attribute__ ((noreturn)); public Q_SLOTS: void cleanup(); 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 ); void slotRowsMoved (const QModelIndex &p, int start, int end, const QModelIndex &dest, int row); void slotRowsMoved2 (const QModelIndex &p, int start, int end, const QModelIndex &dest, int row); }; #define S TreeView2Private::State:: const TreeView2Private::State TreeView2Private::m_fStateMap[5][5] = { /* INSERTION REMOVAL MOVE RESET_SCROLL SCROLL */ /*UNFILLED */ { S ANCHORED, S UNFILLED , S UNFILLED , S UNFILLED, S UNFILLED }, /*ANCHORED */ { S ANCHORED, S ANCHORED , S ANCHORED , S ANCHORED, S SCROLLED }, /*SCROLLED */ { S SCROLLED, S SCROLLED , S SCROLLED , S ANCHORED, S SCROLLED }, /*AT_END */ { S AT_END , S AT_END , S AT_END , S ANCHORED, S SCROLLED }, /*ERROR */ { S ERROR , S ERROR , S ERROR , S ERROR , S ERROR }, }; #undef S #define A &TreeView2Private:: const TreeView2Private::StateF TreeView2Private::m_fStateMachine[5][5] = { /* INSERTION REMOVAL MOVE RESET_SCROLL SCROLL */ /*UNFILLED*/ { A refreshFront, A refreshFront , A refreshFront , A nothing , A nothing }, /*ANCHORED*/ { A refreshFront, A refreshFront , A refreshFront , A nothing , A refresh }, /*SCROLLED*/ { A refresh , A refresh , A refresh , A resetScoll, A refresh }, /*AT_END */ { A refreshBack , A refreshBack , A refreshBack , A resetScoll, A refresh }, /*ERROR */ { A error , A error , A error , A error , A error }, }; #undef A TreeView2::TreeView2(QQuickItem* parent) : FlickableView(parent), d_ptr(new TreeView2Private()) { d_ptr->q_ptr = this; } TreeView2::~TreeView2() { delete d_ptr->m_pRoot; delete d_ptr; } void TreeView2::setModel(QSharedPointer m) { if (m == model()) return; if (auto oldM = model()) { disconnect(oldM.data(), &QAbstractItemModel::rowsInserted, d_ptr, &TreeView2Private::slotRowsInserted); disconnect(oldM.data(), &QAbstractItemModel::rowsAboutToBeRemoved, d_ptr, &TreeView2Private::slotRowsRemoved); disconnect(oldM.data(), &QAbstractItemModel::layoutAboutToBeChanged, d_ptr, &TreeView2Private::cleanup); disconnect(oldM.data(), &QAbstractItemModel::layoutChanged, d_ptr, &TreeView2Private::slotLayoutChanged); disconnect(oldM.data(), &QAbstractItemModel::modelAboutToBeReset, d_ptr, &TreeView2Private::cleanup); disconnect(oldM.data(), &QAbstractItemModel::modelReset, d_ptr, &TreeView2Private::slotLayoutChanged); disconnect(oldM.data(), &QAbstractItemModel::rowsAboutToBeMoved, d_ptr, &TreeView2Private::slotRowsMoved); disconnect(oldM.data(), &QAbstractItemModel::rowsMoved, d_ptr, &TreeView2Private::slotRowsMoved2); disconnect(oldM.data(), &QAbstractItemModel::dataChanged, d_ptr, &TreeView2Private::slotDataChanged); d_ptr->slotRowsRemoved({}, 0, oldM->rowCount()-1); } d_ptr->m_hMapper.clear(); delete d_ptr->m_pRoot; d_ptr->m_pRoot = new TreeTraversalItems(nullptr, d_ptr); FlickableView::setModel(m); if (!m) return; connect(model().data(), &QAbstractItemModel::rowsInserted, d_ptr, &TreeView2Private::slotRowsInserted ); connect(model().data(), &QAbstractItemModel::rowsAboutToBeRemoved, d_ptr, &TreeView2Private::slotRowsRemoved ); connect(model().data(), &QAbstractItemModel::layoutAboutToBeChanged, d_ptr, &TreeView2Private::cleanup); connect(model().data(), &QAbstractItemModel::layoutChanged, d_ptr, &TreeView2Private::slotLayoutChanged); connect(model().data(), &QAbstractItemModel::modelAboutToBeReset, d_ptr, &TreeView2Private::cleanup); connect(model().data(), &QAbstractItemModel::modelReset, d_ptr, &TreeView2Private::slotLayoutChanged); connect(model().data(), &QAbstractItemModel::rowsAboutToBeMoved, d_ptr, &TreeView2Private::slotRowsMoved); connect(model().data(), &QAbstractItemModel::rowsMoved, d_ptr, &TreeView2Private::slotRowsMoved2); connect(model().data(), &QAbstractItemModel::dataChanged, d_ptr, &TreeView2Private::slotDataChanged ); if (auto rc = m->rowCount()) d_ptr->slotRowsInserted({}, 0, rc - 1); } bool TreeView2::hasUniformRowHeight() const { return d_ptr->m_UniformRowHeight; } void TreeView2::setUniformRowHeight(bool value) { d_ptr->m_UniformRowHeight = value; } bool TreeView2::hasUniformColumnWidth() const { return d_ptr->m_UniformColumnWidth; } void TreeView2::setUniformColumnColumnWidth(bool value) { d_ptr->m_UniformColumnWidth = value; } bool TreeView2::isCollapsible() const { return d_ptr->m_Collapsible; } void TreeView2::setCollapsible(bool value) { d_ptr->m_Collapsible = value; } bool TreeView2::isAutoExpand() const { return d_ptr->m_AutoExpand; } void TreeView2::setAutoExpand(bool value) { d_ptr->m_AutoExpand = value; } int TreeView2::maxDepth() const { return d_ptr->m_MaxDepth; } void TreeView2::setMaxDepth(int depth) { d_ptr->m_MaxDepth = depth; } int TreeView2::cacheBuffer() const { return d_ptr->m_CacheBuffer; } void TreeView2::setCacheBuffer(int value) { d_ptr->m_CacheBuffer = value; } int TreeView2::poolSize() const { return d_ptr->m_PoolSize; } void TreeView2::setPoolSize(int value) { d_ptr->m_PoolSize = value; } TreeView2::RecyclingMode TreeView2::recyclingMode() const { return d_ptr->m_RecyclingMode; } void TreeView2::setRecyclingMode(TreeView2::RecyclingMode mode) { d_ptr->m_RecyclingMode = mode; } void TreeView2::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) { FlickableView::geometryChanged(newGeometry, oldGeometry); d_ptr->m_pRoot->performAction(TreeTraversalItems::Action::MOVE); contentItem()->setWidth(newGeometry.width()); } void TreeView2::reloadChildren(const QModelIndex& index) const { if (auto i = static_cast(itemForIndex(index))) { - const auto p = i->m_pParent; + const auto p = i->m_pTTI; if (!p) return; auto c = p->m_pFirstChild; while (c && c != p->m_pLastChild) { if (c->m_pTreeItem) { c->m_pTreeItem->performAction( VisualTreeItem::Action::UPDATE ); c->m_pTreeItem->performAction( VisualTreeItem::Action::MOVE ); } c = c->m_pNext; } } } QQuickItem* TreeView2::parentTreeItem(const QModelIndex& index) const { if (auto i = static_cast(itemForIndex(index))) { - if (i->m_pParent && i->m_pParent->m_pParent && i->m_pParent->m_pParent->m_pTreeItem) - return i->m_pParent->m_pParent->m_pTreeItem->item(); + if (i->m_pTTI && i->m_pTTI->m_pParent && i->m_pTTI->m_pParent->m_pTreeItem) + return i->m_pTTI->m_pParent->m_pTreeItem->item(); } return nullptr; } /// Return true if the indices affect the current view bool TreeView2Private::isActive(const QModelIndex& parent, int first, int last) { return true; //FIXME if (m_State == State::UNFILLED) return true; //FIXME only insert elements with loaded children into m_hMapper auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; if (parent.isValid() && pitem == m_pRoot) return false; if ((!pitem->m_pLastChild) || (!pitem->m_pFirstChild)) return true; if (pitem->m_pLastChild->m_Index.row() >= first) return true; if (pitem->m_pFirstChild->m_Index.row() <= last) return true; return false; } /// Add new entries to the mapping TreeTraversalItems* TreeView2Private::addChildren(TreeTraversalItems* parent, const QModelIndex& index) { Q_ASSERT(index.isValid()); Q_ASSERT(index.parent() != index); auto e = new TreeTraversalItems(parent, this); e->m_Index = index; const int oldSize(m_hMapper.size()), oldSize2(parent->m_hLookup.size()); m_hMapper [index] = e; parent->m_hLookup[index] = e; // If the size did not grow, something leaked Q_ASSERT(m_hMapper.size() == oldSize+1); Q_ASSERT(parent->m_hLookup.size() == oldSize2+1); return e; } /// Make sure all elements exists all the way to the root void TreeView2Private::initTree(const QModelIndex& parent) { Q_UNUSED(parent); // } void TreeView2Private::cleanup() { m_pRoot->performAction(TreeTraversalItems::Action::DETACH); m_hMapper.clear(); m_pRoot = new TreeTraversalItems(nullptr, this); m_FailedCount = 0; } FlickableView::ModelIndexItem* TreeView2::itemForIndex(const QModelIndex& idx) const { if (!idx.isValid()) return nullptr; if (!idx.parent().isValid()) { const auto tti = d_ptr->m_pRoot->m_hLookup.value(idx); return tti ? tti->m_pTreeItem : nullptr; } if (auto parent = d_ptr->m_hMapper.value(idx.parent())) { const auto tti = parent->m_hLookup.value(idx); return tti ? tti->m_pTreeItem : nullptr; } return nullptr; } void TreeView2::reload() { if (d_ptr->m_hMapper.isEmpty()) return; } void TreeView2Private::_test_validateTree(TreeTraversalItems* p) { #ifdef QT_NO_DEBUG_OUTPUT return; #endif // 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->m_pParent == m_pRoot && m_pRoot->m_pFirstChild == p) { Q_ASSERT(!p->m_pTreeItem->up()); } // First, let's check the linked list to avoid running more test on really // corrupted data if (auto i = p->m_pFirstChild) { auto idx = i->m_Index; int count = 1; auto oldI = i; while ((oldI = i) && (i = i->m_pNext)) { // If this is a next, then there has to be a previous Q_ASSERT(i->m_pParent == p); Q_ASSERT(i->m_pPrevious); Q_ASSERT(i->m_pPrevious->m_Index == idx); //Q_ASSERT(i->m_Index.row() == idx.row()+1); //FIXME Q_ASSERT(i->m_pPrevious->m_pNext == i); Q_ASSERT(i->m_pPrevious == oldI); idx = i->m_Index; count++; } Q_ASSERT(p == p->m_pFirstChild->m_pParent); Q_ASSERT(p == p->m_pLastChild->m_pParent); Q_ASSERT(p->m_hLookup.size() == count); } // Do that again in the other direction if (auto i = p->m_pLastChild) { auto idx = i->m_Index; auto oldI = i; int count = 1; while ((oldI = i) && (i = i->m_pPrevious)) { Q_ASSERT(i->m_pNext); Q_ASSERT(i->m_pNext->m_Index == idx); Q_ASSERT(i->m_pParent == p); //Q_ASSERT(i->m_Index.row() == idx.row()-1); //FIXME Q_ASSERT(i->m_pNext->m_pPrevious == i); Q_ASSERT(i->m_pNext == oldI); idx = i->m_Index; count++; } Q_ASSERT(p->m_hLookup.size() == count); } //TODO remove once stable // Brute force recursive validations TreeTraversalItems *old(nullptr), *newest(nullptr); for (auto i = p->m_hLookup.constBegin(); i != p->m_hLookup.constEnd(); i++) { if ((!newest) || i.key().row() < newest->m_Index.row()) newest = i.value(); if ((!old) || i.key().row() > old->m_Index.row()) old = i.value(); // Check that m_FailedCount is valid Q_ASSERT(i.value()->m_pTreeItem->m_State != VisualTreeItem::State::FAILED); // Test the indices Q_ASSERT(p == m_pRoot || i.key().internalPointer() == i.value()->m_Index.internalPointer()); Q_ASSERT(p == m_pRoot || (p->m_Index.isValid()) || p->m_Index.internalPointer() != i.key().internalPointer()); //Q_ASSERT(old == i.value() || old->m_Index.row() > i.key().row()); //FIXME //Q_ASSERT(newest == i.value() || newest->m_Index.row() < i.key().row()); //FIXME // Test that there is no trivial duplicate TreeTraversalItems for the same index if(i.value()->m_pPrevious && i.value()->m_pPrevious->m_hLookup.isEmpty()) { const auto prev = V_ITEM(i.value()->m_pTreeItem->up()); - Q_ASSERT(prev->m_pParent == i.value()->m_pPrevious); + Q_ASSERT(prev->m_pTTI == i.value()->m_pPrevious); const auto next = V_ITEM(prev->down()); - Q_ASSERT(next->m_pParent == i.value()); + Q_ASSERT(next->m_pTTI == i.value()); } // Test the virtual linked list between the leafs and branches if(auto next = V_ITEM(i.value()->m_pTreeItem->down())) { Q_ASSERT(next->up() == i.value()->m_pTreeItem); - Q_ASSERT(next->m_pParent != i.value()); + Q_ASSERT(next->m_pTTI != i.value()); } 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(!i.value()->m_pNext); Q_ASSERT(i.value()->m_hLookup.isEmpty()); } if(auto prev = V_ITEM(i.value()->m_pTreeItem->up())) { Q_ASSERT(V_ITEM(prev->down()) == i.value()->m_pTreeItem); - Q_ASSERT(prev->m_pParent != i.value()); + Q_ASSERT(prev->m_pTTI != i.value()); } 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(!i.value()->m_pPrevious); Q_ASSERT(!i.value()->m_pParent->m_pTreeItem); } _test_validateTree(i.value()); } // Traverse as a list if (p == m_pRoot) { VisualTreeItem* oldVI(nullptr); int count(0), count2(0); for (auto i = m_pRoot->m_pFirstChild->m_pTreeItem; i; i = V_ITEM(i->down())) { Q_ASSERT((!oldVI) || i->up()); Q_ASSERT(i->up() == oldVI); oldVI = i; count++; } // Backward too oldVI = nullptr; auto last = m_pRoot->m_pLastChild; while (last && last->m_pLastChild) last = last->m_pLastChild; for (auto i = last->m_pTreeItem; i; i = V_ITEM(i->up())) { Q_ASSERT((!oldVI) || i->down()); Q_ASSERT(i->down() == oldVI); oldVI = i; count2++; } Q_ASSERT(count == count2); } // Test that the list edges are valid Q_ASSERT(!(!!p->m_pLastChild ^ !!p->m_pFirstChild)); Q_ASSERT(p->m_pLastChild == old); Q_ASSERT(p->m_pFirstChild == newest); Q_ASSERT((!old) || !old->m_pNext); Q_ASSERT((!newest) || !newest->m_pPrevious); } void TreeView2Private::slotRowsInserted(const QModelIndex& parent, int first, int last) { Q_ASSERT((!parent.isValid()) || parent.model() == q_ptr->model()); // qDebug() << "\n\nADD" << first << last; if (!isActive(parent, first, last)) return; // qDebug() << "\n\nADD2" << q_ptr->width() << q_ptr->height(); auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; TreeTraversalItems *prev(nullptr); //FIXME use up() if (first && pitem) prev = pitem->m_hLookup.value(q_ptr->model()->index(first-1, 0, parent)); //FIXME support smaller ranges for (int i = first; i <= last; i++) { auto idx = q_ptr->model()->index(i, 0, parent); Q_ASSERT(idx.isValid()); Q_ASSERT(idx.parent() != idx); Q_ASSERT(idx.model() == q_ptr->model()); auto e = addChildren(pitem, idx); // Keep a dual chained linked list between the visual elements e->m_pPrevious = prev ? prev : nullptr; //FIXME incorrect //FIXME It can happen if the previous is out of the visible range Q_ASSERT( e->m_pPrevious || e->m_Index.row() == 0); //TODO merge with bridgeGap if (prev) bridgeGap(prev, e, true); // This is required before ::ATTACH because otherwise ::down() won't work if ((!pitem->m_pFirstChild) || e->m_Index.row() <= pitem->m_pFirstChild->m_Index.row()) { e->m_pNext = pitem->m_pFirstChild; pitem->m_pFirstChild = e; } e->performAction(TreeTraversalItems::Action::ATTACH); if ((!pitem->m_pFirstChild) || e->m_Index.row() <= pitem->m_pFirstChild->m_Index.row()) { Q_ASSERT(pitem != e); if (auto pe = V_ITEM(e->m_pTreeItem->up())) - pe->m_pParent->performAction(TreeTraversalItems::Action::MOVE); + pe->m_pTTI->performAction(TreeTraversalItems::Action::MOVE); } if ((!pitem->m_pLastChild) || e->m_Index.row() > pitem->m_pLastChild->m_Index.row()) { Q_ASSERT(pitem != e); pitem->m_pLastChild = e; if (auto ne = V_ITEM(e->m_pTreeItem->down())) - ne->m_pParent->performAction(TreeTraversalItems::Action::MOVE); + ne->m_pTTI->performAction(TreeTraversalItems::Action::MOVE); } if (auto rc = q_ptr->model()->rowCount(idx)) slotRowsInserted(idx, 0, rc-1); prev = e; } if ((!pitem->m_pLastChild) || last > pitem->m_pLastChild->m_Index.row()) pitem->m_pLastChild = prev; Q_ASSERT(pitem->m_pLastChild); //FIXME use down() if (q_ptr->model()->rowCount(parent) > last) { if (auto i = pitem->m_hLookup.value(q_ptr->model()->index(last+1, 0, parent))) { i->m_pPrevious = prev; prev->m_pNext = i; } // else //FIXME it happens // Q_ASSERT(false); } //FIXME EVIL and useless m_pRoot->performAction(TreeTraversalItems::Action::MOVE); _test_validateTree(m_pRoot); Q_EMIT q_ptr->contentChanged(); if (!parent.isValid()) Q_EMIT q_ptr->countChanged(); } void TreeView2Private::slotRowsRemoved(const QModelIndex& parent, int first, int last) { Q_ASSERT((!parent.isValid()) || parent.model() == q_ptr->model()); Q_EMIT q_ptr->contentChanged(); if (!isActive(parent, first, last)) return; auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; //TODO make sure the state machine support them //TreeTraversalItems *prev(nullptr), *next(nullptr); //FIXME use up() //if (first && pitem) // prev = pitem->m_hLookup.value(q_ptr->model()->index(first-1, 0, parent)); //next = pitem->m_hLookup.value(q_ptr->model()->index(last+1, 0, parent)); //FIXME support smaller ranges for (int i = first; i <= last; i++) { auto idx = q_ptr->model()->index(i, 0, parent); auto elem = pitem->m_hLookup.value(idx); Q_ASSERT(elem); elem->performAction(TreeTraversalItems::Action::DETACH); } if (!parent.isValid()) Q_EMIT q_ptr->countChanged(); } void TreeView2Private::slotLayoutChanged() { if (auto rc = q_ptr->model()->rowCount()) slotRowsInserted({}, 0, rc - 1); Q_EMIT q_ptr->contentChanged(); Q_EMIT q_ptr->countChanged(); } void TreeView2Private::createGap(VisualTreeItem* first, VisualTreeItem* last) { - Q_ASSERT(first->m_pParent->m_pParent == last->m_pParent->m_pParent); + Q_ASSERT(first->m_pTTI->m_pParent == last->m_pTTI->m_pParent); - if (first->m_pParent->m_pPrevious) { - first->m_pParent->m_pPrevious->m_pNext = last->m_pParent->m_pNext; + if (first->m_pTTI->m_pPrevious) { + first->m_pTTI->m_pPrevious->m_pNext = last->m_pTTI->m_pNext; } - if (last->m_pParent->m_pNext) { - last->m_pParent->m_pNext->m_pPrevious = first->m_pParent->m_pPrevious; + if (last->m_pTTI->m_pNext) { + last->m_pTTI->m_pNext->m_pPrevious = first->m_pTTI->m_pPrevious; } - if (first->m_pParent->m_pParent->m_pFirstChild == first->m_pParent) - first->m_pParent->m_pParent->m_pFirstChild = last->m_pParent->m_pNext; + if (first->m_pTTI->m_pParent->m_pFirstChild == first->m_pTTI) + first->m_pTTI->m_pParent->m_pFirstChild = last->m_pTTI->m_pNext; - if (last->m_pParent->m_pParent->m_pLastChild == last->m_pParent) - last->m_pParent->m_pParent->m_pLastChild = first->m_pParent->m_pPrevious; + if (last->m_pTTI->m_pParent->m_pLastChild == last->m_pTTI) + last->m_pTTI->m_pParent->m_pLastChild = first->m_pTTI->m_pPrevious; - Q_ASSERT((!first->m_pParent->m_pPrevious) || - first->m_pParent->m_pPrevious->m_pTreeItem->down() != first); - Q_ASSERT((!last->m_pParent->m_pNext) || - last->m_pParent->m_pNext->m_pTreeItem->up() != last); + Q_ASSERT((!first->m_pTTI->m_pPrevious) || + first->m_pTTI->m_pPrevious->m_pTreeItem->down() != first); + Q_ASSERT((!last->m_pTTI->m_pNext) || + last->m_pTTI->m_pNext->m_pTreeItem->up() != last); - Q_ASSERT((!first) || first->m_pParent->m_pFirstChild || first->m_pParent->m_hLookup.isEmpty()); - Q_ASSERT((!last) || last->m_pParent->m_pFirstChild || last->m_pParent->m_hLookup.isEmpty()); + Q_ASSERT((!first) || first->m_pTTI->m_pFirstChild || first->m_pTTI->m_hLookup.isEmpty()); + Q_ASSERT((!last) || last->m_pTTI->m_pFirstChild || last->m_pTTI->m_hLookup.isEmpty()); // Do not leave invalid pointers for easier debugging - last->m_pParent->m_pNext = nullptr; - first->m_pParent->m_pPrevious = nullptr; + last->m_pTTI->m_pNext = nullptr; + first->m_pTTI->m_pPrevious = nullptr; } // Convenience wrapper void TreeView2Private::bridgeGap(VisualTreeItem* first, VisualTreeItem* second, bool insert) { bridgeGap( - first ? first->m_pParent : nullptr, - second ? second->m_pParent : nullptr, + first ? first->m_pTTI : nullptr, + second ? second->m_pTTI : nullptr, insert ); } /// Fix the issues introduced by createGap (does not update m_pParent and m_hLookup) void TreeView2Private::bridgeGap(TreeTraversalItems* first, TreeTraversalItems* second, bool insert) { // 3 possible case: siblings, first child or last child if (first && second && first->m_pParent == second->m_pParent) { // first and second are siblings // Assume the second item is new if (insert && first->m_pNext) { second->m_pNext = first->m_pNext; first->m_pNext->m_pPrevious = second; } first->m_pNext = second; second->m_pPrevious = first; } else if (second && ((!first) || first == second->m_pParent)) { // The `second` is `first` first child or it's the new root second->m_pPrevious = nullptr; if (!second->m_pParent->m_pLastChild) second->m_pParent->m_pLastChild = second; second->m_pNext = second->m_pParent->m_pFirstChild; if (second->m_pParent->m_pFirstChild) { second->m_pParent->m_pFirstChild->m_pPrevious = second; } second->m_pParent->m_pFirstChild = second; //BEGIN test /*int count =0; for (auto c = second->m_pParent->m_pFirstChild; c; c = c->m_pNext) count++; Q_ASSERT(count == second->m_pParent->m_hLookup.size());*/ //END test } else if (first) { // It's the last element or the second is a last leaf and first is unrelated first->m_pNext = nullptr; if (!first->m_pParent->m_pFirstChild) first->m_pParent->m_pFirstChild = first; if (first->m_pParent->m_pLastChild && first->m_pParent->m_pLastChild != first) { first->m_pParent->m_pLastChild->m_pNext = first; first->m_pPrevious = first->m_pParent->m_pLastChild; } first->m_pParent->m_pLastChild = first; //BEGIN test int count =0; for (auto c = first->m_pParent->m_pLastChild; c; c = c->m_pPrevious) count++; Q_ASSERT(first->m_pParent->m_pFirstChild); Q_ASSERT(count == first->m_pParent->m_hLookup.size()); //END test } else { Q_ASSERT(false); //Something went really wrong elsewhere } if (first) Q_ASSERT(first->m_pParent->m_pFirstChild); if (second) Q_ASSERT(second->m_pParent->m_pFirstChild); if (first) Q_ASSERT(first->m_pParent->m_pLastChild); if (second) Q_ASSERT(second->m_pParent->m_pLastChild); // if (first && second) { //Need to disable other asserts in down() // Q_ASSERT(first->down() == second); // Q_ASSERT(second->up() == first); // } } void TreeView2Private::setTemporaryIndices(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { //FIXME list only // Before moving them, set a temporary now/col value because it won't be set // on the index until before slotRowsMoved2 is called (but after this // method returns //TODO do not use the hashmap, it is already known if (parent == destination) { const auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; for (int i = start; i <= end; i++) { auto idx = q_ptr->model()->index(i, 0, parent); auto elem = pitem->m_hLookup.value(idx); Q_ASSERT(elem); elem->m_pTreeItem->m_MoveToRow = row + (i - start); } for (int i = row; i <= row + (end - start); i++) { auto idx = q_ptr->model()->index(i, 0, parent); auto elem = pitem->m_hLookup.value(idx); Q_ASSERT(elem); elem->m_pTreeItem->m_MoveToRow = row + (end - start) + 1; } } } void TreeView2Private::resetTemporaryIndices(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { //FIXME list only // Before moving them, set a temporary now/col value because it won't be set // on the index until before slotRowsMoved2 is called (but after this // method returns //TODO do not use the hashmap, it is already known if (parent == destination) { const auto pitem = parent.isValid() ? m_hMapper.value(parent) : m_pRoot; for (int i = start; i <= end; i++) { auto idx = q_ptr->model()->index(i, 0, parent); auto elem = pitem->m_hLookup.value(idx); Q_ASSERT(elem); elem->m_pTreeItem->m_MoveToRow = -1; } for (int i = row; i <= row + (end - start); i++) { auto idx = q_ptr->model()->index(i, 0, parent); auto elem = pitem->m_hLookup.value(idx); Q_ASSERT(elem); elem->m_pTreeItem->m_MoveToRow = -1; } } } void TreeView2Private::slotRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { Q_ASSERT((!parent.isValid()) || parent.model() == q_ptr->model()); Q_ASSERT((!destination.isValid()) || destination.model() == q_ptr->model()); // 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 ((!isActive(parent, start, end)) && !isActive(destination, row, row+(end-start))) return; setTemporaryIndices(parent, start, end, destination, row); //TODO also support trees // As the actual view is implemented as a daisy chained list, only moving // the edges is necessary for the TreeTraversalItems. Each VisualTreeItem // need to be moved. const auto idxStart = q_ptr->model()->index(start, 0, parent); const auto idxEnd = q_ptr->model()->index(end , 0, parent); Q_ASSERT(idxStart.isValid() && idxEnd.isValid()); //FIXME once partial ranges are supported, this is no longer always valid auto startVI = V_ITEM(q_ptr->itemForIndex(idxStart)); auto endVI = V_ITEM(q_ptr->itemForIndex(idxEnd)); if (end - start == 1) - Q_ASSERT(startVI->m_pParent->m_pNext == endVI->m_pParent); + Q_ASSERT(startVI->m_pTTI->m_pNext == endVI->m_pTTI); //FIXME so I don't forget, it will mess things up if silently ignored Q_ASSERT(startVI && endVI); - Q_ASSERT(startVI->m_pParent->m_pParent == endVI->m_pParent->m_pParent); + Q_ASSERT(startVI->m_pTTI->m_pParent == endVI->m_pTTI->m_pParent); auto oldPreviousVI = V_ITEM(startVI->up()); auto oldNextVI = V_ITEM(endVI->down()); Q_ASSERT((!oldPreviousVI) || oldPreviousVI->down() == startVI); Q_ASSERT((!oldNextVI) || oldNextVI->up() == endVI); auto newNextIdx = q_ptr->model()->index(row, 0, destination); // You cannot move things into an empty model Q_ASSERT((!row) || newNextIdx.isValid()); VisualTreeItem *newNextVI(nullptr), *newPrevVI(nullptr); // Rewind until a next element is found, this happens when destination is empty if (!newNextIdx.isValid() && destination.parent().isValid()) { Q_ASSERT(q_ptr->model()->rowCount(destination) == row); auto par = destination.parent(); do { if (q_ptr->model()->rowCount(par.parent()) > par.row()) { newNextIdx = q_ptr->model()->index(par.row(), 0, par.parent()); break; } par = par.parent(); } while (par.isValid()); newNextVI = V_ITEM(q_ptr->itemForIndex(newNextIdx)); } else { newNextVI = V_ITEM(q_ptr->itemForIndex(newNextIdx)); newPrevVI = newNextVI ? V_ITEM(newNextVI->up()) : nullptr; } if (!row) { auto otherI = V_ITEM(q_ptr->itemForIndex(destination)); Q_ASSERT((!newPrevVI) || otherI == newPrevVI); } // When there is no next element, then the parent has to be extracted manually if (!(newNextVI || newPrevVI)) { if (!row) newPrevVI = V_ITEM(q_ptr->itemForIndex(destination)); else { newPrevVI = V_ITEM(q_ptr->itemForIndex( destination.model()->index(row-1, 0, destination) )); } } Q_ASSERT((newPrevVI || startVI) && newPrevVI != startVI); Q_ASSERT((newNextVI || endVI ) && newNextVI != endVI ); TreeTraversalItems* newParentTTI = nullptr; - auto oldParentTTI = startVI->m_pParent->m_pParent; + auto oldParentTTI = startVI->m_pTTI->m_pParent; if (auto parentVI = V_ITEM(q_ptr->itemForIndex(destination))) - newParentTTI = parentVI->m_pParent; + newParentTTI = parentVI->m_pTTI; else newParentTTI = m_pRoot; // Make sure not to leave invalid pointers while the steps below are being performed createGap(startVI, endVI); // Update the tree parent (if necessary) if (oldParentTTI != newParentTTI) { - for (auto i = startVI; i; i = i->m_pParent->m_pNext ? i->m_pParent->m_pNext->m_pTreeItem : nullptr) { + for (auto i = startVI; i; i = i->m_pTTI->m_pNext ? i->m_pTTI->m_pNext->m_pTreeItem : nullptr) { auto idx = i->index(); const int size = oldParentTTI->m_hLookup.size(); oldParentTTI->m_hLookup.remove(idx); Q_ASSERT(oldParentTTI->m_hLookup.size() == size-1); - newParentTTI->m_hLookup[idx] = i->m_pParent; - i->m_pParent->m_pParent = newParentTTI; + newParentTTI->m_hLookup[idx] = i->m_pTTI; + i->m_pTTI->m_pParent = newParentTTI; if (i == endVI) break; } } - Q_ASSERT(startVI->m_pParent->m_pParent == newParentTTI); - Q_ASSERT(endVI->m_pParent->m_pParent == newParentTTI); + Q_ASSERT(startVI->m_pTTI->m_pParent == newParentTTI); + Q_ASSERT(endVI->m_pTTI->m_pParent == newParentTTI); bridgeGap(newPrevVI, startVI ); bridgeGap(endVI , newNextVI); // Close the gap between the old previous and next elements - Q_ASSERT(startVI->m_pParent->m_pNext != startVI->m_pParent); - Q_ASSERT(startVI->m_pParent->m_pPrevious != startVI->m_pParent); - Q_ASSERT(endVI->m_pParent->m_pNext != endVI->m_pParent ); - Q_ASSERT(endVI->m_pParent->m_pPrevious != endVI->m_pParent ); + Q_ASSERT(startVI->m_pTTI->m_pNext != startVI->m_pTTI); + Q_ASSERT(startVI->m_pTTI->m_pPrevious != startVI->m_pTTI); + Q_ASSERT(endVI->m_pTTI->m_pNext != endVI->m_pTTI ); + Q_ASSERT(endVI->m_pTTI->m_pPrevious != endVI->m_pTTI ); //BEGIN debug if (newPrevVI) { int count = 0; - for (auto c = newPrevVI->m_pParent->m_pParent->m_pFirstChild; c; c = c->m_pNext) + for (auto c = newPrevVI->m_pTTI->m_pParent->m_pFirstChild; c; c = c->m_pNext) count++; - Q_ASSERT(count == newPrevVI->m_pParent->m_pParent->m_hLookup.size()); + Q_ASSERT(count == newPrevVI->m_pTTI->m_pParent->m_hLookup.size()); count = 0; - for (auto c = newPrevVI->m_pParent->m_pParent->m_pLastChild; c; c = c->m_pPrevious) + for (auto c = newPrevVI->m_pTTI->m_pParent->m_pLastChild; c; c = c->m_pPrevious) count++; - Q_ASSERT(count == newPrevVI->m_pParent->m_pParent->m_hLookup.size()); + Q_ASSERT(count == newPrevVI->m_pTTI->m_pParent->m_hLookup.size()); } //END bridgeGap(oldPreviousVI, oldNextVI); - if (endVI->m_pParent->m_pNext) { - Q_ASSERT(endVI->m_pParent->m_pNext->m_pPrevious == endVI->m_pParent); + if (endVI->m_pTTI->m_pNext) { + Q_ASSERT(endVI->m_pTTI->m_pNext->m_pPrevious == endVI->m_pTTI); } - if (startVI->m_pParent->m_pPrevious) { - Q_ASSERT(startVI->m_pParent->m_pPrevious->m_pParent == startVI->m_pParent->m_pParent); - Q_ASSERT(startVI->m_pParent->m_pPrevious->m_pNext == startVI->m_pParent); + if (startVI->m_pTTI->m_pPrevious) { + Q_ASSERT(startVI->m_pTTI->m_pPrevious->m_pParent == startVI->m_pTTI->m_pParent); + Q_ASSERT(startVI->m_pTTI->m_pPrevious->m_pNext == startVI->m_pTTI); } // Q_ASSERT((!newNextVI) || newNextVI->m_pParent->m_pPrevious == endVI->m_pParent); // Q_ASSERT((!newPrevVI) || // // newPrevVI->m_pParent->m_pNext == startVI->m_pParent || // (newPrevVI->m_pParent->m_pFirstChild == startVI->m_pParent && !row) // ); // Q_ASSERT((!oldPreviousVI) || (!oldPreviousVI->m_pParent->m_pNext) || // oldPreviousVI->m_pParent->m_pNext == (oldNextVI ? oldNextVI->m_pParent : nullptr)); // Move everything //TODO move it more efficient m_pRoot->performAction(TreeTraversalItems::Action::MOVE); resetTemporaryIndices(parent, start, end, destination, row); } void TreeView2Private::slotRowsMoved2(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { Q_UNUSED(parent) Q_UNUSED(start) Q_UNUSED(end) Q_UNUSED(destination) Q_UNUSED(row) // The test would fail if it was in aboutToBeMoved _test_validateTree(m_pRoot); } void TreeView2Private::slotDataChanged(const QModelIndex& tl, const QModelIndex& br) { if (tl.model() && tl.model() != q_ptr->model()) { Q_ASSERT(false); return; } if (br.model() && br.model() != q_ptr->model()) { Q_ASSERT(false); return; } if ((!tl.isValid()) || (!br.isValid())) return; if (!isActive(tl.parent(), tl.row(), br.row())) return; //FIXME tolerate other cases Q_ASSERT(q_ptr->model()); Q_ASSERT(tl.model() == q_ptr->model() && br.model() == q_ptr->model()); Q_ASSERT(tl.parent() == br.parent()); //TODO Use a smaller range when possible //itemForIndex(const QModelIndex& idx) const final override; for (int i = tl.row(); i <= br.row(); i++) { const auto idx = q_ptr->model()->index(i, tl.column(), tl.parent()); if (auto item = V_ITEM(q_ptr->itemForIndex(idx))) item->performAction(VisualTreeItem::Action::UPDATE); } } bool VisualTreeItem::performAction(Action a) { if (m_State == VisualTreeItem::State::FAILED) - m_pParent->d_ptr->m_FailedCount--; + m_pTTI->d_ptr->m_FailedCount--; const int s = (int)m_State; m_State = m_fStateMap [s][(int)a]; Q_ASSERT(m_State != VisualTreeItem::State::ERROR); const bool ret = (this->*m_fStateMachine[s][(int)a])(); if (m_State == VisualTreeItem::State::FAILED || !ret) { m_State = VisualTreeItem::State::FAILED; - m_pParent->d_ptr->m_FailedCount++; + m_pTTI->d_ptr->m_FailedCount++; } return ret; } QWeakPointer VisualTreeItem::reference() const { if (!m_pSelf) m_pSelf = QSharedPointer( const_cast(this) ); return m_pSelf; } int VisualTreeItem::depth() const { - return m_pParent->m_Depth; + return m_pTTI->m_Depth; +} + +bool VisualTreeItem::isVisible() const +{ + return true;//m_pTTI->TODO } QPersistentModelIndex VisualTreeItem::index() const { - return m_pParent->m_Index; + return m_pTTI->m_Index; } /** * Flatten the tree as a linked list. * * Returns the previous non-failed item. */ FlickableView::ModelIndexItem* VisualTreeItem::up() const { Q_ASSERT(m_State == State::ACTIVE || m_State == State::BUFFER || m_State == State::FAILED || m_State == State::POOLING || m_State == State::DANGLING //FIXME add a new state for // "deletion in progress" or swap the f call and set state ); TreeTraversalItems* ret = nullptr; // Another simple case, there is no parent - if (!m_pParent->m_pParent) { + if (!m_pTTI->m_pParent) { Q_ASSERT(!index().parent().isValid()); //TODO remove, no longer true when partial loading is implemented return nullptr; } // The parent has no previous siblings, therefor it is directly above the item - if (!m_pParent->m_pPrevious) { + if (!m_pTTI->m_pPrevious) { // if (m_pParent->m_pParent && m_pParent->m_pParent->m_pFirstChild) // Q_ASSERT(m_pParent->m_pParent->m_pFirstChild == m_pParent); //FIXME Q_ASSERT(index().row() == 0); // This is the root, there is no previous element - if (!m_pParent->m_pParent->m_pTreeItem) { + if (!m_pTTI->m_pParent->m_pTreeItem) { //Q_ASSERT(!index().parent().isValid()); //Happens when reseting models return nullptr; } - ret = m_pParent->m_pParent; + ret = m_pTTI->m_pParent; // Avoids useless unreadable indentation goto sanitize; } - ret = m_pParent->m_pPrevious; + ret = m_pTTI->m_pPrevious; while (ret->m_pLastChild) ret = ret->m_pLastChild; sanitize: Q_ASSERT((!ret) || ret->m_pTreeItem); // Recursively look for a valid element. Doing this here allows the views // that implement this (abstract) class to work without having to always // check if some of their item failed to load. This is non-fatal in the // other Qt views, so it isn't fatal here either. if (ret && ret->m_pTreeItem->m_State == VisualTreeItem::State::FAILED) return ret->m_pTreeItem->up(); return ret ? ret->m_pTreeItem : nullptr; } /** * Flatten the tree as a linked list. * * Returns the next non-failed item. */ FlickableView::ModelIndexItem* VisualTreeItem::down() const { Q_ASSERT(m_State == State::ACTIVE || m_State == State::BUFFER || m_State == State::FAILED || m_State == State::POOLING || m_State == State::DANGLING //FIXME add a new state for // "deletion in progress" or swap the f call and set state ); VisualTreeItem* ret = nullptr; - auto i = m_pParent; + auto i = m_pTTI; - if (m_pParent->m_pFirstChild) { + if (m_pTTI->m_pFirstChild) { //Q_ASSERT(m_pParent->m_pFirstChild->m_Index.row() == 0); //racy - ret = m_pParent->m_pFirstChild->m_pTreeItem; + ret = m_pTTI->m_pFirstChild->m_pTreeItem; goto sanitizeNext; } // Recursively unwrap the tree until an element is found while(i) { if (i->m_pNext) { ret = i->m_pNext->m_pTreeItem; goto sanitizeNext; } i = i->m_pParent; } // Can't happen, exists to detect corrupted code - if (m_pParent->m_Index.parent().isValid()) { - Q_ASSERT(m_pParent->m_pParent); + if (m_pTTI->m_Index.parent().isValid()) { + Q_ASSERT(m_pTTI->m_pParent); // Q_ASSERT(m_pParent->m_pParent->m_pParent->m_hLookup.size() // == m_pParent->m_Index.parent().row()+1); } sanitizeNext: // Recursively look for a valid element. Doing this here allows the views // that implement this (abstract) class to work without having to always // check if some of their item failed to load. This is non-fatal in the // other Qt views, so it isn't fatal here either. if (ret && ret->m_State == VisualTreeItem::State::FAILED) return ret->down(); return ret; } int VisualTreeItem::row() const { return m_MoveToRow == -1 ? index().row() : m_MoveToRow; } int VisualTreeItem::column() const { return m_MoveToColumn == -1 ? index().column() : m_MoveToColumn; } bool VisualTreeItem::nothing() { return true; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" bool VisualTreeItem::error() { Q_ASSERT(false); return true; } #pragma GCC diagnostic pop bool VisualTreeItem::destroy() { auto ptrCopy = m_pSelf; QTimer::singleShot(0,[this, ptrCopy]() { if (!ptrCopy) delete this; // else the reference will be dropped and the destructor called }); m_pSelf.clear(); return true; } bool TreeTraversalItems::performAction(Action a) { const int s = (int)m_State; m_State = m_fStateMap [s][(int)a]; bool ret = (this->*m_fStateMachine[s][(int)a])(); return ret; } bool TreeTraversalItems::nothing() { return true; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" bool TreeTraversalItems::error() { Q_ASSERT(false); return false; } #pragma GCC diagnostic pop bool TreeTraversalItems::show() { // qDebug() << "SHOW"; if (!m_pTreeItem) { m_pTreeItem = V_ITEM(d_ptr->q_ptr->createItem()); - m_pTreeItem->m_pParent = this; + m_pTreeItem->m_pTTI = this; } m_pTreeItem->performAction(VisualTreeItem::Action::ENTER_BUFFER); m_pTreeItem->performAction(VisualTreeItem::Action::ENTER_VIEW ); return true; } bool TreeTraversalItems::hide() { // qDebug() << "HIDE"; return true; } bool TreeTraversalItems::attach() { if (m_pTreeItem) m_pTreeItem->performAction(VisualTreeItem::Action::ATTACH); // qDebug() << "ATTACH" << (int)m_State; performAction(Action::MOVE); //FIXME don't return performAction(Action::SHOW); //FIXME don't } // This methodwrap the removal of the element from the view bool VisualTreeItem::detach() { remove(); m_State = VisualTreeItem::State::POOLED; return true; } bool TreeTraversalItems::detach() { // First, detach any remaining children auto i = m_hLookup.begin(); while ((i = m_hLookup.begin()) != m_hLookup.end()) i.value()->performAction(TreeTraversalItems::Action::DETACH); Q_ASSERT(m_hLookup.isEmpty()); if (m_pTreeItem) { // If the item was active (due, for example, of a full reset), then // it has to be removed from view then deleted. if (m_pTreeItem->m_State == VisualTreeItem::State::ACTIVE) { m_pTreeItem->performAction(VisualTreeItem::Action::DETACH); // It should still exists, it may crash otherwise, so make sure early Q_ASSERT(m_pTreeItem->m_State == VisualTreeItem::State::POOLED); m_pTreeItem->m_State = VisualTreeItem::State::POOLED; //FIXME ^^ add a new action for finish pooling or call // VisualTreeItem::detach from a new action method (instead of directly) } m_pTreeItem->performAction(VisualTreeItem::Action::DETACH); m_pTreeItem = nullptr; } if (m_pParent) { const int size = m_pParent->m_hLookup.size(); m_pParent->m_hLookup.remove(m_Index); Q_ASSERT(size == m_pParent->m_hLookup.size()+1); } if (m_pPrevious || m_pNext) { if (m_pPrevious) m_pPrevious->m_pNext = m_pNext; if (m_pNext) m_pNext->m_pPrevious = m_pPrevious; } else if (m_pParent) { //FIXME very wrong Q_ASSERT(m_pParent->m_hLookup.isEmpty()); m_pParent->m_pFirstChild = nullptr; m_pParent->m_pLastChild = nullptr; } //FIXME set the parent m_pFirstChild correctly and add an insert()/move() method // then drop bridgeGap return true; } bool TreeTraversalItems::refresh() { // // qDebug() << "REFRESH"; for (auto i = m_pFirstChild; i; i = i->m_pNext) { Q_ASSERT(i); Q_ASSERT(i != this); i->performAction(TreeTraversalItems::Action::UPDATE); if (i == m_pLastChild) break; } return true; } bool TreeTraversalItems::index() { //TODO remove this implementation and use the one below // Propagate for (auto i = m_pFirstChild; i; i = i->m_pNext) { Q_ASSERT(i); Q_ASSERT(i != this); i->performAction(TreeTraversalItems::Action::MOVE); if (i == m_pLastChild) break; } if (m_pTreeItem) m_pTreeItem->performAction(VisualTreeItem::Action::MOVE); //FIXME don't return true; // if (!m_pTreeItem) // return true; //FIXME this is better, but require having createItem() called earlier // if (m_pTreeItem) // m_pTreeItem->performAction(VisualTreeItem::Action::MOVE); //FIXME don't // // if (oldGeo != m_pTreeItem->geometry()) // if (auto n = m_pTreeItem->down()) // n->m_pParent->performAction(TreeTraversalItems::Action::MOVE); // return true; } bool TreeTraversalItems::destroy() { detach(); m_pTreeItem = nullptr; Q_ASSERT(m_hLookup.isEmpty()); delete this; return true; } bool TreeView2Private::nothing() { return true; } bool TreeView2Private::resetScoll() { return true; } bool TreeView2Private::refresh() { // Propagate for (auto i = m_pRoot->m_hLookup.constBegin(); i != m_pRoot->m_hLookup.constEnd(); i++) i.value()->performAction(TreeTraversalItems::Action::UPDATE); return true; } bool TreeView2Private::refreshFront() { return true; } bool TreeView2Private::refreshBack() { return true; } bool TreeView2Private::error() { Q_ASSERT(false); return true; } #include #undef V_ITEM diff --git a/src/qmlwidgets/treeview2.h b/src/qmlwidgets/treeview2.h index 5fb3de1b..2e23344f 100644 --- a/src/qmlwidgets/treeview2.h +++ b/src/qmlwidgets/treeview2.h @@ -1,221 +1,221 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * 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 . * **************************************************************************/ #pragma once #include // Qt #include class QQmlComponent; class TreeView2Private; class TreeView2; struct TreeTraversalItems; //FIXME remove /** * Polymorphic tree item for the TreeView2. * * Classes implementing TreeView2 need to provide an implementation of the pure * virtual functions. It is useful, for example, to manage both a raster and * QQuickItem based version of a view. * * The state is managed by the TreeView2 and it's own protected virtual methods. */ class VisualTreeItem : public FlickableView::ModelIndexItem { friend class TreeView2; friend struct TreeTraversalItems; friend class TreeView2Private; public: explicit VisualTreeItem(FlickableView* p) : ModelIndexItem(p) {} virtual ~VisualTreeItem() {} enum class State { POOLING , /*!< Being currently removed from view */ POOLED , /*!< Not currently in use, either new or waiting for re-use */ BUFFER , /*!< Not currently on screen, pre-loaded for performance */ ACTIVE , /*!< Visible */ FAILED , /*!< Loading the item was attempted, but failed */ DANGLING, /*!< Pending deletion, invalid pointers */ ERROR , /*!< Something went wrong */ }; // Helpers virtual ModelIndexItem* up () const override final; virtual ModelIndexItem* down() const override final; virtual int row () const override final; virtual int column() const override final; // Getters virtual QPersistentModelIndex index () const override final; virtual int depth() const final override; /// Allows to keep a reference while still being tracked by the state machine virtual QWeakPointer reference() const final override; /// Visibility relative to the displayed window of the FlickableView::view() - virtual bool isVisible() const override {return true;} //FIXME implement + virtual bool isVisible() const override; // Actions virtual bool attach () = 0; virtual bool refresh() = 0; virtual bool move () = 0; virtual bool flush () = 0; virtual bool remove () = 0; bool detach(); private: enum class Action { //TODO make this private to TreeView2Private ATTACH = 0, /*!< Activate the element (do not sync it) */ ENTER_BUFFER = 1, /*!< Sync all roles */ ENTER_VIEW = 2, /*!< NOP (todo) */ UPDATE = 3, /*!< Reload the roles */ MOVE = 4, /*!< Move to a new position */ LEAVE_BUFFER = 5, /*!< Stop keeping track of data changes */ DETACH = 6, /*!< Delete */ }; typedef bool(VisualTreeItem::*StateF)(); State m_State {State::POOLED}; - TreeTraversalItems* m_pParent {nullptr}; + TreeTraversalItems* m_pTTI {nullptr}; mutable QSharedPointer m_pSelf; int m_MoveToRow {-1}; int m_MoveToColumn {-1}; static const State m_fStateMap [7][7]; static const StateF m_fStateMachine[7][7]; bool performAction(Action); bool nothing(); bool error (); bool destroy(); }; /** * Second generation of QtQuick treeview. * * The first one was designed for the chat view. It had a limited number of * requirement when it came to QtModel. However it required total control of * the layout. * * This is the opposite use case. The layout is classic, but the model support * has to be complete. Performance and lazy loading is also more important. * * It require less work to write a new treeview than refector the first one to * support the additional requirements. In the long run, the first generation * could be folded into this widget (if it ever makes sense, otherwise they will * keep diverging). */ class TreeView2 : public FlickableView { Q_OBJECT friend struct TreeTraversalItems; friend class VisualTreeItem; public: /// Assume each hierarchy level have the same height (for performance) Q_PROPERTY(bool uniformRowHeight READ hasUniformRowHeight WRITE setUniformRowHeight) /// Assume each column has the same width (for performance) Q_PROPERTY(bool uniformColumnWidth READ hasUniformColumnWidth WRITE setUniformColumnColumnWidth) /// The view can be collapsed Q_PROPERTY(bool collapsible READ isCollapsible WRITE setCollapsible) /// Expand all elements by default Q_PROPERTY(bool autoExpand READ isAutoExpand WRITE setAutoExpand) /// The maximum depth of a tree (for performance) Q_PROPERTY(int maxDepth READ maxDepth WRITE setMaxDepth) /// Recycle existing QQuickItem delegates for new QModelIndex (for performance) Q_PROPERTY(RecyclingMode recyclingMode READ recyclingMode WRITE setRecyclingMode) /// The number of elements to be preloaded outside of the visible area (for latency) Q_PROPERTY(int cacheBuffer READ cacheBuffer WRITE setCacheBuffer) /// The number of delegates to be kept in a recycling pool (for performance) Q_PROPERTY(int poolSize READ poolSize WRITE setPoolSize) enum RecyclingMode { NoRecycling , /*!< Destroy and create new QQuickItems all the time */ RecyclePerDepth, /*!< Keep different pools buffer for each levels of children */ AlwaysRecycle , /*!< Assume all depth level use the same delegate */ }; Q_ENUM(RecyclingMode) explicit TreeView2(QQuickItem* parent = nullptr); virtual ~TreeView2(); virtual void setModel(QSharedPointer model) override; bool hasUniformRowHeight() const; void setUniformRowHeight(bool value); bool hasUniformColumnWidth() const; void setUniformColumnColumnWidth(bool value); bool isCollapsible() const; void setCollapsible(bool value); bool isAutoExpand() const; void setAutoExpand(bool value); int maxDepth() const; void setMaxDepth(int depth); int cacheBuffer() const; void setCacheBuffer(int value); int poolSize() const; void setPoolSize(int value); RecyclingMode recyclingMode() const; void setRecyclingMode(RecyclingMode mode); /** * If some changes on the QML side require moving elements. It's the * equivalent of calling dataChanged on every direct children index of * `index`. */ Q_INVOKABLE void reloadChildren(const QModelIndex& index) const; /** * Get the parent tree QQuickItem for a given index */ Q_INVOKABLE QQuickItem* parentTreeItem(const QModelIndex& index) const; protected: virtual void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; /** * Get the VisualTreeItem associated with a model index. * * Note that if the index is not currently visible or buferred, it will * return nullptr. */ ModelIndexItem* itemForIndex(const QModelIndex& idx) const final override; /// To be used with moderation. Necessary when the delegate is replaced. void reload(); Q_SIGNALS: void modelChanged(QSharedPointer model); virtual void contentChanged() = 0; private: TreeView2Private* d_ptr; Q_DECLARE_PRIVATE(TreeView2) };