diff --git a/src/flickablescrollbar.cpp b/src/flickablescrollbar.cpp index 3a3a66a..9c5f4b7 100644 --- a/src/flickablescrollbar.cpp +++ b/src/flickablescrollbar.cpp @@ -1,135 +1,135 @@ /*************************************************************************** * 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 "flickablescrollbar.h" // KQuickItemViews #include "views/flickable.h" class FlickableScrollBarPrivate : public QObject { Q_OBJECT public: Flickable *m_pView {nullptr}; qreal m_HandleHeight { 0 }; qreal m_Position { 0 }; bool m_Visible { false }; FlickableScrollBar* q_ptr; public Q_SLOTS: void recomputeGeometry(); }; FlickableScrollBar::FlickableScrollBar(QQuickItem* parent) : QQuickItem(parent), d_ptr(new FlickableScrollBarPrivate) { d_ptr->q_ptr = this; } FlickableScrollBar::~FlickableScrollBar() { delete d_ptr; } QObject* FlickableScrollBar::view() const { return d_ptr->m_pView; } void FlickableScrollBar::setView(QObject* v) { if (d_ptr->m_pView) { disconnect(d_ptr->m_pView, &Flickable::contentHeightChanged, d_ptr, &FlickableScrollBarPrivate::recomputeGeometry); - disconnect(d_ptr->m_pView, &Flickable::currentYChanged, + disconnect(d_ptr->m_pView, &Flickable::contentYChanged, d_ptr, &FlickableScrollBarPrivate::recomputeGeometry); disconnect(d_ptr->m_pView, &Flickable::heightChanged, d_ptr, &FlickableScrollBarPrivate::recomputeGeometry); } d_ptr->m_pView = qobject_cast(v); Q_ASSERT((!v) || d_ptr->m_pView); connect(d_ptr->m_pView, &Flickable::contentHeightChanged, d_ptr, &FlickableScrollBarPrivate::recomputeGeometry); - connect(d_ptr->m_pView, &Flickable::currentYChanged, + connect(d_ptr->m_pView, &Flickable::contentYChanged, d_ptr, &FlickableScrollBarPrivate::recomputeGeometry); connect(d_ptr->m_pView, &Flickable::heightChanged, d_ptr, &FlickableScrollBarPrivate::recomputeGeometry); d_ptr->recomputeGeometry(); } qreal FlickableScrollBar::position() const { return d_ptr->m_Position; } void FlickableScrollBar::setPosition(qreal p) { if (!d_ptr->m_pView) return; // Simple rule of 3 - d_ptr->m_pView->setCurrentY( + d_ptr->m_pView->setContentY( (p * d_ptr->m_pView->contentHeight()) / d_ptr->m_pView->height() ); } qreal FlickableScrollBar::handleHeight() const { return d_ptr->m_HandleHeight; } bool FlickableScrollBar::isHandleVisible() const { return d_ptr->m_Visible; } /** * The idea behind the scrollhandle is that the height represent the height of a * page until it gets too small. In mobile mode, the height is always the same * regardless of the content height to hide less space on the smaller screen. */ void FlickableScrollBarPrivate::recomputeGeometry() { if (!m_pView) return; const qreal oldP = m_Position; const qreal oldH = m_HandleHeight; const qreal totalHeight = m_pView->contentHeight(); const qreal pageHeight = m_pView->height(); const qreal pageCount = totalHeight/pageHeight; const qreal handleHeight = std::max(pageHeight/pageCount, 50.0); - const qreal handleBegin = (m_pView->currentY()*pageHeight)/totalHeight; + const qreal handleBegin = (m_pView->contentY()*pageHeight)/totalHeight; m_HandleHeight = handleHeight; m_Position = handleBegin; m_Visible = totalHeight > pageHeight; if (oldH != m_HandleHeight) emit q_ptr->handleHeightChanged(); if (oldP != m_Position) emit q_ptr->positionChanged (); } #include diff --git a/src/singlemodelviewbase.cpp b/src/singlemodelviewbase.cpp index e8794bb..af66872 100644 --- a/src/singlemodelviewbase.cpp +++ b/src/singlemodelviewbase.cpp @@ -1,236 +1,236 @@ /*************************************************************************** * Copyright (C) 2017 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 "singlemodelviewbase.h" // Qt #include // KQuickItemViews #include #include #include #include "viewport.h" #include "private/viewport_p.h" #include "private/geostrategyselector_p.h" class SingleModelViewBasePrivate { public: bool m_IsSortingEnabled { false }; ModelAdapter *m_pModelAdapter { nullptr }; SingleModelViewBase* q_ptr; }; SingleModelViewBase::SingleModelViewBase(ItemFactoryBase *factory, QQuickItem* parent) : ViewBase(parent), d_ptr(new SingleModelViewBasePrivate()) { d_ptr->q_ptr = this; d_ptr->m_pModelAdapter = new ModelAdapter(this); addModelAdapter(d_ptr->m_pModelAdapter); auto vp = d_ptr->m_pModelAdapter->viewports().first(); vp->setItemFactory(factory); auto sm = d_ptr->m_pModelAdapter->selectionAdapter(); // Ok, connecting signals to signals is not a very good idea, I am lazy connect(sm, &SelectionAdapter::currentIndexChanged, this, &SingleModelViewBase::currentIndexChanged); connect(sm, &SelectionAdapter::selectionModelChanged, this, &SingleModelViewBase::selectionModelChanged); connect(d_ptr->m_pModelAdapter, &ModelAdapter::modelAboutToChange, this, &SingleModelViewBase::applyModelChanges); connect(vp, &Viewport::cornerChanged, this, &SingleModelViewBase::cornerChanged); } SingleModelViewBase::~SingleModelViewBase() { delete d_ptr; } QQmlComponent* SingleModelViewBase::highlight() const { return d_ptr->m_pModelAdapter->selectionAdapter()->highlight(); } void SingleModelViewBase::setHighlight(QQmlComponent* h) { d_ptr->m_pModelAdapter->selectionAdapter()->setHighlight(h); } void SingleModelViewBase::setDelegate(QQmlComponent* delegate) { d_ptr->m_pModelAdapter->setDelegate(delegate); emit delegateChanged(delegate); refresh(); } QQmlComponent* SingleModelViewBase::delegate() const { return d_ptr->m_pModelAdapter->delegate(); } QSharedPointer SingleModelViewBase::selectionModel() const { return d_ptr->m_pModelAdapter->selectionAdapter()->selectionModel(); } QVariant SingleModelViewBase::model() const { return d_ptr->m_pModelAdapter->model(); } void SingleModelViewBase::setModel(const QVariant& m) { d_ptr->m_pModelAdapter->setModel(m); emit modelChanged(); } void SingleModelViewBase::setSelectionModel(QSharedPointer m) { d_ptr->m_pModelAdapter->selectionAdapter()->setSelectionModel(m); emit selectionModelChanged(); } QAbstractItemModel *SingleModelViewBase::rawModel() const { return d_ptr->m_pModelAdapter->rawModel(); } void SingleModelViewBase::applyModelChanges(QAbstractItemModel* m) { if (d_ptr->m_IsSortingEnabled && m) { m->sort(0); } } bool SingleModelViewBase::isDelegateSizeForced() const { return d_ptr->m_pModelAdapter->viewports().constFirst()->s_ptr-> m_pGeoAdapter->isSizeForced(); } void SingleModelViewBase::setDelegateSizeForced(bool f) { d_ptr->m_pModelAdapter->viewports().constFirst()->s_ptr-> m_pGeoAdapter->setSizeForced(f); } bool SingleModelViewBase::isSortingEnabled() const { return d_ptr->m_IsSortingEnabled; } void SingleModelViewBase::setSortingEnabled(bool val) { d_ptr->m_IsSortingEnabled = val; if (d_ptr->m_IsSortingEnabled && rawModel()) { rawModel()->sort(0); } } QModelIndex SingleModelViewBase::currentIndex() const { return selectionModel()->currentIndex(); } void SingleModelViewBase::setCurrentIndex(const QModelIndex& index, QItemSelectionModel::SelectionFlags f) { selectionModel()->setCurrentIndex(index, f); } bool SingleModelViewBase::hasUniformRowHeight() const { return d_ptr->m_pModelAdapter->viewports().constFirst()->s_ptr-> m_pGeoAdapter->capabilities() & GeometryAdapter::Capabilities::HAS_UNIFORM_HEIGHT; } void SingleModelViewBase::setUniformRowHeight(bool value) { Q_UNUSED(value) //d_ptr->m_pModelAdapter->setUniformRowHeight(value); } bool SingleModelViewBase::hasUniformColumnWidth() const { return d_ptr->m_pModelAdapter->viewports().constFirst()->s_ptr-> m_pGeoAdapter->capabilities() & GeometryAdapter::Capabilities::HAS_UNIFORM_WIDTH; } void SingleModelViewBase::setUniformColumnColumnWidth(bool value) { Q_UNUSED(value) //d_ptr->m_pModelAdapter->setUniformColumnColumnWidth(value); } void SingleModelViewBase::moveTo(Qt::Edge e) { QTimer::singleShot(0, [this, e]() { //HACK This need the viewportAdapter to be optimized switch(e) { case Qt::TopEdge: - setCurrentY(0); + setContentY(0); break; case Qt::BottomEdge: { - int y = currentY(); + int y = contentY(); // Keep loading until it doesn't load anything else do { - setCurrentY(999999); - } while (currentY() > y && (y = currentY())); + setContentY(999999); + } while (contentY() > y && (y = contentY())); } case Qt::LeftEdge: case Qt::RightEdge: break; //TODO } }); } QModelIndex SingleModelViewBase::indexAt(const QPoint & point) const { return d_ptr->m_pModelAdapter->viewports().first()->indexAt(point); } QModelIndex SingleModelViewBase::topLeft() const { return d_ptr->m_pModelAdapter->viewports().first()->indexAt(Qt::TopLeftCorner); } QModelIndex SingleModelViewBase::topRight() const { return d_ptr->m_pModelAdapter->viewports().first()->indexAt(Qt::TopRightCorner); } QModelIndex SingleModelViewBase::bottomLeft() const { return d_ptr->m_pModelAdapter->viewports().first()->indexAt(Qt::BottomLeftCorner); } QModelIndex SingleModelViewBase::bottomRight() const { return d_ptr->m_pModelAdapter->viewports().first()->indexAt(Qt::BottomRightCorner); } QRectF SingleModelViewBase::itemRect(const QModelIndex& i) const { return d_ptr->m_pModelAdapter->viewports().first()->itemRect(i); } diff --git a/src/viewport.cpp b/src/viewport.cpp index 227b73b..1ffd990 100644 --- a/src/viewport.cpp +++ b/src/viewport.cpp @@ -1,593 +1,593 @@ /*************************************************************************** * 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 "viewport.h" // Qt #include #include #include // KQuickItemViews #include "private/viewport_p.h" #include "proxies/sizehintproxymodel.h" #include "private/statetracker/content_p.h" #include "adapters/modeladapter.h" #include "adapters/viewportadapter.h" #include "contextadapterfactory.h" #include "adapters/contextadapter.h" #include "private/statetracker/viewitem_p.h" #include "private/statetracker/model_p.h" #include "adapters/abstractitemadapter.h" #include "viewbase.h" #include "private/indexmetadata_p.h" #include "private/geostrategyselector_p.h" class ViewportPrivate : public QObject { Q_OBJECT public: QQmlEngine *m_pEngine { nullptr }; ModelAdapter *m_pModelAdapter { nullptr }; ViewportAdapter *m_pViewAdapter { nullptr }; // The viewport rectangle QRectF m_ViewRect; QRectF m_UsedRect; void updateAvailableEdges(); Viewport *q_ptr; public Q_SLOTS: void slotModelChanged(QAbstractItemModel* m, QAbstractItemModel* o); void slotModelAboutToChange(QAbstractItemModel* m, QAbstractItemModel* o); void slotViewportChanged(const QRectF &viewport); }; Viewport::Viewport(ModelAdapter* ma) : QObject(), s_ptr(new ViewportSync()), d_ptr(new ViewportPrivate()) { d_ptr->q_ptr = this; s_ptr->q_ptr = this; d_ptr->m_pModelAdapter = ma; s_ptr->m_pReflector = new StateTracker::Content(this); s_ptr->m_pGeoAdapter = new GeoStrategySelector(this); resize(QRectF { 0.0, 0.0, ma->view()->width(), ma->view()->height() }); connect(ma, &ModelAdapter::modelChanged, d_ptr, &ViewportPrivate::slotModelChanged); connect(ma->view(), &Flickable::viewportChanged, d_ptr, &ViewportPrivate::slotViewportChanged); connect(ma, &ModelAdapter::delegateChanged, s_ptr->m_pReflector, [this]() { s_ptr->m_pReflector->modelTracker()->performAction( StateTracker::Model::Action::RESET ); }); d_ptr->slotModelChanged(ma->rawModel(), nullptr); connect(s_ptr->m_pReflector, &StateTracker::Content::contentChanged, this, &Viewport::contentChanged); } Viewport::~Viewport() { delete s_ptr; delete d_ptr; } QRectF Viewport::currentRect() const { return d_ptr->m_UsedRect; } void ViewportPrivate::slotModelAboutToChange(QAbstractItemModel* m, QAbstractItemModel* o) { Q_UNUSED(m) } void ViewportPrivate::slotModelChanged(QAbstractItemModel* m, QAbstractItemModel* o) { Q_UNUSED(o) - m_pModelAdapter->view()->setCurrentY(0); + m_pModelAdapter->view()->setContentY(0); q_ptr->s_ptr->m_pReflector->modelTracker()->setModel(m); Q_ASSERT(m_pModelAdapter->rawModel() == m); q_ptr->s_ptr->m_pGeoAdapter->setModel(m); if (m && m_ViewRect.size().isValid() && m_pModelAdapter->delegate()) { q_ptr->s_ptr->m_pReflector->modelTracker() << StateTracker::Model::Action::POPULATE << StateTracker::Model::Action::ENABLE; } } void ViewportPrivate::slotViewportChanged(const QRectF &viewport) { // Q_ASSERT(viewport.y() == 0); //FIXME I broke it m_ViewRect = viewport; m_UsedRect = viewport; //FIXME remove wrong updateAvailableEdges(); q_ptr->s_ptr->m_pReflector->modelTracker() << StateTracker::Model::Action::MOVE; } ModelAdapter *Viewport::modelAdapter() const { return d_ptr->m_pModelAdapter; } QSizeF Viewport::size() const { return d_ptr->m_UsedRect.size(); } QPointF Viewport::position() const { return d_ptr->m_UsedRect.topLeft(); } QSizeF Viewport::totalSize() const { if ((!d_ptr->m_pModelAdapter->delegate()) || !d_ptr->m_pModelAdapter->rawModel()) return {0.0, 0.0}; return {}; //TODO } void Viewport::setItemFactory(ViewBase::ItemFactoryBase *factory) { s_ptr->m_fFactory = ([this, factory]() -> AbstractItemAdapter* { return factory->create(this); }); } Qt::Edges Viewport::availableEdges() const { return s_ptr->m_pReflector->availableEdges( IndexMetadata::EdgeType::FREE ); } void ViewportPrivate::updateAvailableEdges() { if (q_ptr->s_ptr->m_pReflector->modelTracker()->state() == StateTracker::Model::State::RESETING) return; //TODO it needs another state machine to get rid of the `if` if (!q_ptr->s_ptr->m_pReflector->modelTracker()->modelCandidate()) return; Qt::Edges available; auto v = m_pModelAdapter->view(); auto tve = q_ptr->s_ptr->m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::TopEdge ); Q_ASSERT((!tve) || (!tve->up()) || (!tve->up()->isVisible())); auto bve = q_ptr->s_ptr->m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::BottomEdge ); Q_ASSERT((!bve) || (!bve->down()) || (!bve->down()->isVisible())); // If they don't have a valid size, then there is a bug elsewhere Q_ASSERT((!tve) || tve->isValid()); Q_ASSERT((!bve) || tve->isValid()); // Do not attempt to load the geometry yet, let the loading code do it later const bool tveValid = tve && tve->isValid(); const bool bveValid = bve && bve->isValid(); // Given 42x0 sized item are possible. However just "fixing" this by adding // a minimum size wont help because it will trigger the out of sync view // correction in an infinite loop. QRectF tvg(tve?tve->decoratedGeometry():QRectF()), bvg(bve?bve->decoratedGeometry():QRectF()); const auto fixedIntersect = [](bool valid, QRectF& vp, QRectF& geo) -> bool { Q_UNUSED(valid) //TODO return vp.intersects(geo) || ( geo.y() >= vp.y() && geo.width() == 0 && geo.y() <= vp.y() + vp.height() ) || ( geo.height() == 0 && geo.width() > 0 && geo.x() <= vp.x() + vp.width() && geo.x() >= vp.x() ); }; QRectF vp = m_ViewRect; // Add an extra pixel to the height to prevent off-by-one where the view is // perfectly full and can't scroll any more (and thus load the next item) vp.setHeight(vp.height()+1.0); if ((!tve) || (fixedIntersect(tveValid, vp, tvg) && tvg.y() > 0)) available |= Qt::TopEdge; if ((!bve) || fixedIntersect(bveValid, vp, bvg)) available |= Qt::BottomEdge; q_ptr->s_ptr->m_pReflector->setAvailableEdges( available, IndexMetadata::EdgeType::FREE ); q_ptr->s_ptr->m_pReflector->setAvailableEdges( available, IndexMetadata::EdgeType::VISIBLE ); // Q_ASSERT((~hasInvisible)&available == available || !available); // m_pReflector->setAvailableEdges( // (~hasInvisible)&15, IndexMetadata::EdgeType::VISIBLE // ); q_ptr->s_ptr->m_pReflector->modelTracker() << StateTracker::Model::Action::TRIM; const auto oldBve(bve), oldTve(tve); bve = q_ptr->s_ptr->m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::BottomEdge ); //BEGIN test tve = q_ptr->s_ptr->m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::TopEdge ); Q_ASSERT((!tve) || (!tve->up()) || (!tve->up()->isVisible())); Q_ASSERT((!bve) || (!bve->down()) || (!bve->down()->isVisible())); //END test // Size is normal as the state as not converged yet Q_ASSERT((!bve) || ( bve->geometryTracker()->state() == StateTracker::Geometry::State::VALID || bve->geometryTracker()->state() == StateTracker::Geometry::State::SIZE) ); // Resize the contend height, it has to be done after the geometry has been // updated. if (bve && q_ptr->s_ptr->m_pGeoAdapter->capabilities() & GeometryAdapter::Capabilities::TRACKS_QQUICKITEM_GEOMETRY) { const auto geo = bve->decoratedGeometry(); v->contentItem()->setHeight(std::max( geo.y()+geo.height(), v->height() )); emit v->contentHeightChanged( v->contentItem()->height() ); } if (oldTve != tve || oldBve != bve) emit q_ptr->cornerChanged(); } void ViewportSync::geometryUpdated(IndexMetadata *item) { if (m_pReflector->modelTracker()->state() == StateTracker::Model::State::RESETING) return; //TODO it needs another state machine to get rid of the `if` //TODO assert if the size hints don't match reality // This will recompute the geometry item->decoratedGeometry(); if (m_pGeoAdapter->capabilities() & GeometryAdapter::Capabilities::TRACKS_QQUICKITEM_GEOMETRY) q_ptr->d_ptr->updateAvailableEdges(); } void ViewportSync::updateGeometry(IndexMetadata* item) { if (m_pGeoAdapter->capabilities() & GeometryAdapter::Capabilities::TRACKS_QQUICKITEM_GEOMETRY) item->sizeHint(); if (auto i = item->down()) notifyInsert(i); q_ptr->d_ptr->updateAvailableEdges(); refreshVisible(); //notifyInsert(item->down()); } // When the QModelIndex role change void ViewportSync::notifyChange(IndexMetadata* item) { item << IndexMetadata::GeometryAction::MODIFY; } void ViewportSync::notifyRemoval(IndexMetadata* item) { Q_UNUSED(item) if (m_pReflector->modelTracker()->state() == StateTracker::Model::State::RESETING) return; //TODO it needs another state machine to get rid of the `if` // auto tve = m_pReflector->getEdge( // IndexMetadata::EdgeType::VISIBLE, Qt::TopEdge // ); // auto bve = q_ptr->d_ptr->m_pReflector->getEdge( // IndexMetadata::EdgeType::VISIBLE, Qt::BottomEdge // ); // Q_ASSERT(item != bve); //TODO // //FIXME this is horrible // while(auto next = item->down()) { //FIXME dead code // if (next->m_State.state() != StateTracker::Geometry::State::VALID || // next->m_State.state() != StateTracker::Geometry::State::POSITION) // break; // // next->m_State.performAction( // GeometryAction::MOVE, nullptr, nullptr // ); // // Q_ASSERT(next != bve); //TODO // // Q_ASSERT(next->m_State.state() != StateTracker::Geometry::State::VALID); // } // // refreshVisible(); // // q_ptr->d_ptr->updateAvailableEdges(); } //TODO temporary hack to avoid having to implement a complex way to move the // limited number of loaded elements. Given once trimming is enabled, the // number of visible items should be fairely low/constant, it is a good enough // temporary solution. void ViewportSync::refreshVisible() { if (m_pReflector->modelTracker()->state() == StateTracker::Model::State::RESETING) return; //TODO it needs another state machine to get rid of the `if` //TODO eventually move to a relative origin so moving an item to the top // doesn't need to move everything IndexMetadata *item = m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::TopEdge ); if (!item) return; Q_ASSERT((!item->up()) || !item->up()->isVisible()); auto bve = m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::BottomEdge ); auto prev = item->up(); // First, make sure the previous elem has a valid size. If, for example, // rowsMoved moves a previously unloaded item to the front, this information // will be lost. if (prev && !prev->isValid()) { // This is the slow path, it can be /very/ slow. Possible mitigation // include adding more lower level methods to never lose track of the // list state, but this makes everything more (too) complex. Another // better solution is more aggressive unloading to the list becomes // smaller. if (!prev->isTopItem()) { qDebug() << "Slow path"; //FIXME this is slow auto i = prev; while((i = i->up()) && i && !i->isValid()); Q_ASSERT(i); while ((i = i->down()) != item) i->decoratedGeometry(); } else prev->setPosition({0.0, 0.0}); } const bool hasSingleItem = item == bve; do { item->sizeHint(); Q_ASSERT(item->geometryTracker()->state() != StateTracker::Geometry::State::INIT); Q_ASSERT(item->geometryTracker()->state() != StateTracker::Geometry::State::POSITION); if (prev) { //FIXME this isn't ok, it needs to take into account the point size //it could by multiple pixels item->setPosition(prev->decoratedGeometry().bottomLeft()); } item->sizeHint(); Q_ASSERT(item->isVisible()); Q_ASSERT(item->isValid()); //FIXME As of 0.1, this still fails from time to time //Q_ASSERT(item->isInSync());//TODO THIS_COMMIT // This `performAction` exists to recover from runtime failures if (!item->isInSync()) item << IndexMetadata::LoadAction::MOVE; } while((!hasSingleItem) && item->up() != bve && (item = item->down())); } void ViewportSync::notifyInsert(IndexMetadata* item) { using GeoState = StateTracker::Geometry::State; Q_ASSERT(item); if (m_pReflector->modelTracker()->state() == StateTracker::Model::State::RESETING) return; //TODO it needs another state machine to get rid of the `if` if (!item) return; const bool needsPosition = item->geometryTracker()->state() == GeoState::INIT || item->geometryTracker()->state() == GeoState::SIZE; // If the item is new and is inserted near valid items, skip some back and // forth and set the position now. if (needsPosition && item->up() && item->up()->geometryTracker()->state() == GeoState::VALID) { item->setPosition(item->up()->decoratedGeometry().bottomLeft()); } // If the item is inserted in front, set the position if (item->isTopItem()) { item->setPosition({0.0, 0.0}); } auto bve = m_pReflector->getEdge( IndexMetadata::EdgeType::VISIBLE, Qt::BottomEdge ); //FIXME this is also horrible do { if (item == bve) { item << IndexMetadata::GeometryAction::MOVE; break; } if (!item->isValid() && item->geometryTracker()->state() != GeoState::POSITION) { break; } item << IndexMetadata::GeometryAction::MOVE; } while((item = item->down())); refreshVisible(); q_ptr->d_ptr->updateAvailableEdges(); } void Viewport::resize(const QRectF& rect) { if (s_ptr->m_pReflector->modelTracker()->state() == StateTracker::Model::State::RESETING) return; //TODO it needs another state machine to get rid of the `if` const bool wasValid = d_ptr->m_ViewRect.size().isValid(); Q_ASSERT(rect.x() == 0); // The {x, y} may not be at {0, 0}, but given it is a relative viewport, // then the content doesn't care about where it is on the screen. d_ptr->m_ViewRect = rect; Q_ASSERT(rect.y() == 0); // For now ignore the case where the content is smaller, it doesn't change // anything. This could eventually change d_ptr->m_UsedRect.setSize(rect.size()); //FIXME remove, wrong s_ptr->refreshVisible(); d_ptr->updateAvailableEdges(); if ((!d_ptr->m_pModelAdapter) || (!d_ptr->m_pModelAdapter->rawModel())) return; if (!d_ptr->m_pModelAdapter->delegate()) return; if (!rect.isValid()) return; // Refresh the content if (!wasValid) s_ptr->m_pReflector->modelTracker() << StateTracker::Model::Action::POPULATE << StateTracker::Model::Action::ENABLE; else { //FIXME make sure the state machine handle the lack of delegate properly s_ptr->m_pReflector->modelTracker() << StateTracker::Model::Action::MOVE; } } IndexMetadata *ViewportSync::metadataForIndex(const QModelIndex& idx) const { return m_pReflector->metadataForIndex(idx); } QQmlEngine *ViewportSync::engine() { if (!m_pEngine) m_pEngine = q_ptr->modelAdapter()->view()->rootContext()->engine(); return m_pEngine; } QQmlComponent *ViewportSync::component() { engine(); m_pComponent = new QQmlComponent(m_pEngine); m_pComponent->setData("import QtQuick 2.4; Item {property QtObject content: null;}", {}); return m_pComponent; } GeometryAdapter *Viewport::geometryAdapter() const { return s_ptr->m_pGeoAdapter; } void Viewport::setGeometryAdapter(GeometryAdapter *a) { s_ptr->m_pGeoAdapter->setCurrentAdapter(a); } QModelIndex Viewport::indexAt(const QPoint &point) const { return {}; //TODO } QModelIndex Viewport::indexAt(Qt::Corner corner) const { // multi column isn't supported yet, so it is close enough for now switch (corner) { case Qt::TopLeftCorner: case Qt::TopRightCorner: return indexAt(Qt::TopEdge); case Qt::BottomLeftCorner: case Qt::BottomRightCorner: return indexAt(Qt::BottomEdge); } return {}; } QModelIndex Viewport::indexAt(Qt::Edge edge) const { if (auto tve = s_ptr->m_pReflector->getEdge(IndexMetadata::EdgeType::VISIBLE, edge)) return tve->index(); return {}; } QRectF Viewport::itemRect(const QModelIndex& i) const { if (auto md = s_ptr->m_pReflector->metadataForIndex(i)) return md->decoratedGeometry(); //TODO load it return {}; } #include diff --git a/src/views/flickable.cpp b/src/views/flickable.cpp index ad00dd6..1149f5d 100644 --- a/src/views/flickable.cpp +++ b/src/views/flickable.cpp @@ -1,528 +1,528 @@ /*************************************************************************** * Copyright (C) 2017 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 "flickable.h" // Qt #include #include #include #include #include #include #include // LibStdC++ #include class FlickablePrivate final : public QObject { Q_OBJECT public: typedef bool(FlickablePrivate::*StateF)(QMouseEvent*); /// The current status of the inertial viewport state machine enum class DragState { IDLE , /*!< Nothing is happening */ PRESSED , /*!< A potential drag */ EVAL , /*!< Drag without lock */ DRAGGED , /*!< An in progress grag */ INERTIA , /*!< A leftover drag */ }; /// Events affecting the behavior of the inertial viewport state machine enum class DragEvent { TIMEOUT , /*!< When inertia is exhausted */ PRESS , /*!< When a mouse button is pressed */ RELEASE , /*!< When a mouse button is released */ MOVE , /*!< When the mouse moves */ TIMER , /*!< 30 times per seconds */ OTHER , /*!< Doesn't affect the state */ ACCEPT , /*!< Accept the drag ownership */ REJECT , /*!< Reject the drag ownership */ }; // Actions bool nothing (QMouseEvent* e); /*!< No operations */ bool error (QMouseEvent* e); /*!< Warn something went wrong */ bool start (QMouseEvent* e); /*!< From idle to pre-drag */ bool stop (QMouseEvent* e); /*!< Stop inertia */ bool drag (QMouseEvent* e); /*!< Move the viewport */ bool cancel (QMouseEvent* e); /*!< Cancel potential drag */ bool inertia (QMouseEvent* e); /*!< Iterate on the inertia curve */ bool release (QMouseEvent* e); /*!< Trigger the inertia */ bool eval (QMouseEvent* e); /*!< Check for potential drag ops */ bool lock (QMouseEvent* e); /*!< Lock the input grabber */ // Attributes QQuickItem* m_pContainer {nullptr}; QPointF m_StartPoint { }; QPointF m_DragPoint { }; QTimer* m_pTimer {nullptr}; qint64 m_StartTime { 0 }; int m_LastDelta { 0 }; qreal m_Velocity { 0 }; qreal m_DecelRate { 0.9 }; bool m_Interactive{ true }; mutable QQmlContext *m_pRootContext {nullptr}; qreal m_MaxVelocity {std::numeric_limits::max()}; DragState m_State {DragState::IDLE}; // Helpers void loadVisibleElements(); bool applyEvent(DragEvent event, QMouseEvent* e); bool updateVelocity(); DragEvent eventMapper(QEvent* e) const; // State machine static const StateF m_fStateMachine[5][8]; static const DragState m_fStateMap [5][8]; Flickable* q_ptr; public Q_SLOTS: void tick(); }; #define A &FlickablePrivate:: // Actions #define S FlickablePrivate::DragState:: // Next state /** * This is a Mealy machine, states callbacks are allowed to throw more events */ const FlickablePrivate::DragState FlickablePrivate::m_fStateMap[5][8] { /* TIMEOUT PRESS RELEASE MOVE TIMER OTHER ACCEPT REJECT */ /* IDLE */ {S IDLE , S PRESSED, S IDLE , S IDLE , S IDLE , S IDLE , S IDLE , S IDLE }, /* PRESSED */ {S PRESSED, S PRESSED, S IDLE , S EVAL , S PRESSED, S PRESSED, S PRESSED, S PRESSED}, /* EVAL */ {S IDLE , S EVAL , S IDLE , S EVAL , S EVAL , S EVAL , S DRAGGED, S IDLE }, /* DRAGGED */ {S DRAGGED, S DRAGGED, S INERTIA , S DRAGGED, S DRAGGED, S DRAGGED, S DRAGGED, S DRAGGED}, /* INERTIA */ {S IDLE , S IDLE , S IDLE , S DRAGGED, S INERTIA, S INERTIA, S INERTIA, S INERTIA}}; const FlickablePrivate::StateF FlickablePrivate::m_fStateMachine[5][8] { /* TIMEOUT PRESS RELEASE MOVE TIMER OTHER ACCEPT REJECT */ /* IDLE */ {A error , A start , A nothing , A nothing, A error , A nothing, A error, A error }, /* PRESSED */ {A error , A nothing, A cancel , A eval , A error , A nothing, A error, A error }, /* EVAL */ {A error , A nothing, A cancel , A eval , A error , A nothing, A lock , A cancel }, /* DRAGGED */ {A error , A drag , A release , A drag , A error , A nothing, A error, A error }, /* INERTIA */ {A stop , A stop , A stop , A error , A inertia, A nothing, A error, A error }}; #undef S #undef A Flickable::Flickable(QQuickItem* parent) : QQuickItem(parent), d_ptr(new FlickablePrivate) { d_ptr->q_ptr = this; setClip(true); setAcceptedMouseButtons(Qt::LeftButton); setFiltersChildMouseEvents(true); d_ptr->m_pTimer = new QTimer(this); d_ptr->m_pTimer->setInterval(1000/30); connect(d_ptr->m_pTimer, &QTimer::timeout, d_ptr, &FlickablePrivate::tick); } Flickable::~Flickable() { if (d_ptr->m_pContainer) delete d_ptr->m_pContainer; delete d_ptr; } QQuickItem* Flickable::contentItem() { if (!d_ptr->m_pContainer) { QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine(); // Can't be called too early as the engine wont be ready. Q_ASSERT(engine); QQmlComponent rect1(engine, this); rect1.setData("import QtQuick 2.4; Item {}", {}); d_ptr->m_pContainer = qobject_cast(rect1.create()); d_ptr->m_pContainer->setHeight(height()); d_ptr->m_pContainer->setWidth(width ()); engine->setObjectOwnership(d_ptr->m_pContainer, QQmlEngine::CppOwnership); d_ptr->m_pContainer->setParentItem(this); emit contentHeightChanged(height()); } return d_ptr->m_pContainer; } QRectF Flickable::viewport() const { return { 0.0, - currentY(), + contentY(), width(), height() }; } -qreal Flickable::currentY() const +qreal Flickable::contentY() const { if (!d_ptr->m_pContainer) return 0; return -d_ptr->m_pContainer->y(); } -void Flickable::setCurrentY(qreal y) +void Flickable::setContentY(qreal y) { if (!d_ptr->m_pContainer) return; // Do not allow out of bound scroll y = std::fmax(y, 0); if (d_ptr->m_pContainer->height() >= height()) y = std::fmin(y, d_ptr->m_pContainer->height() - height()); if (d_ptr->m_pContainer->y() == -y) return; d_ptr->m_pContainer->setY(-y); - emit currentYChanged(y); + emit contentYChanged(y); emit viewportChanged(viewport()); emit percentageChanged( ((-d_ptr->m_pContainer->y()))/(d_ptr->m_pContainer->height()-height()) ); } qreal Flickable::contentHeight() const { if (!d_ptr->m_pContainer) return 0; return d_ptr->m_pContainer->height(); } /// Timer events void FlickablePrivate::tick() { applyEvent(DragEvent::TIMER, nullptr); } /** * Use the linear velocity. This class currently mostly ignore horizontal * movements, but nevertheless the intention is to keep the inertia factor * from its vector. * * @return If there is inertia */ bool FlickablePrivate::updateVelocity() { const qreal dy = m_DragPoint.y() - m_StartPoint.y(); const qreal dt = (QDateTime::currentMSecsSinceEpoch() - m_StartTime)/(1000.0/30.0); // Points per frame m_Velocity = (dy/dt); // Do not start for low velocity mouse release if (std::fabs(m_Velocity) < 40) //TODO C++17 use std::clamp m_Velocity = 0; if (std::fabs(m_Velocity) > std::fabs(m_MaxVelocity)) m_Velocity = m_Velocity > 0 ? m_MaxVelocity : -m_MaxVelocity; return m_Velocity; } /** * Map qevent to DragEvent */ FlickablePrivate::DragEvent FlickablePrivate::eventMapper(QEvent* event) const { auto e = FlickablePrivate::DragEvent::OTHER; #pragma GCC diagnostic ignored "-Wswitch-enum" switch(event->type()) { case QEvent::MouseMove: e = FlickablePrivate::DragEvent::MOVE; break; case QEvent::MouseButtonPress: e = FlickablePrivate::DragEvent::PRESS; break; case QEvent::MouseButtonRelease: e = FlickablePrivate::DragEvent::RELEASE; break; default: break; } #pragma GCC diagnostic pop return e; } /** * The tabs eat some mousePress events at random. * * Mitigate the issue by allowing the event series to begin later. */ bool Flickable::childMouseEventFilter(QQuickItem* item, QEvent* event) { if (!d_ptr->m_Interactive) return false; const auto e = d_ptr->eventMapper(event); return e == FlickablePrivate::DragEvent::OTHER ? QQuickItem::childMouseEventFilter(item, event) : d_ptr->applyEvent(e, static_cast(event) ); } bool Flickable::event(QEvent *event) { if (!d_ptr->m_Interactive) return false; const auto e = d_ptr->eventMapper(event); if (event->type() == QEvent::Wheel) { - setCurrentY(currentY() - static_cast(event)->angleDelta().y()); + setContentY(contentY() - static_cast(event)->angleDelta().y()); event->accept(); return true; } return e == FlickablePrivate::DragEvent::OTHER ? QQuickItem::event(event) : d_ptr->applyEvent(e, static_cast(event) ); } void Flickable::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) { if (d_ptr->m_pContainer) { d_ptr->m_pContainer->setWidth(std::max(newGeometry.width(), d_ptr->m_pContainer->width())); d_ptr->m_pContainer->setHeight(std::max(newGeometry.height(), d_ptr->m_pContainer->height())); emit contentHeightChanged(d_ptr->m_pContainer->height()); } //TODO prevent out of scope QQuickItem::geometryChanged(newGeometry, oldGeometry); emit viewportChanged(viewport()); } /// State functions /// /** * Make the Mealy machine move between the states */ bool FlickablePrivate::applyEvent(DragEvent event, QMouseEvent* e) { if (!m_pContainer) return false; const bool wasDragging(q_ptr->isDragging()), wasMoving(q_ptr->isMoving()); // Set the state before the callback so recursive events work const int s = (int)m_State; m_State = m_fStateMap [s][(int)event]; bool ret = (this->*m_fStateMachine[s][(int)event])(e); if (ret && e) e->accept(); if (wasDragging != q_ptr->isDragging()) emit q_ptr->draggingChanged(q_ptr->isDragging()); if (wasMoving != q_ptr->isMoving()) emit q_ptr->movingChanged(q_ptr->isMoving()); return ret && e; } bool FlickablePrivate::nothing(QMouseEvent*) { return false; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" bool FlickablePrivate::error(QMouseEvent*) { qWarning() << "simpleFlickable: Invalid state change"; Q_ASSERT(false); return false; } #pragma GCC diagnostic pop bool FlickablePrivate::stop(QMouseEvent* event) { m_pTimer->stop(); m_Velocity = 0; m_StartPoint = m_DragPoint = {}; // Resend for further processing if (event) applyEvent(FlickablePrivate::DragEvent::PRESS, event); return false; } bool FlickablePrivate::drag(QMouseEvent* e) { if (!m_pContainer) return false; const int dy(e->pos().y() - m_DragPoint.y()); m_DragPoint = e->pos(); - q_ptr->setCurrentY(q_ptr->currentY() - dy); + q_ptr->setContentY(q_ptr->contentY() - dy); // Reset the inertia on the differential inflexion points if ((m_LastDelta >= 0) ^ (dy >= 0)) { m_StartPoint = e->pos(); m_StartTime = QDateTime::currentMSecsSinceEpoch(); } m_LastDelta = dy; return true; } bool FlickablePrivate::start(QMouseEvent* e) { m_StartPoint = m_DragPoint = e->pos(); m_StartTime = QDateTime::currentMSecsSinceEpoch(); q_ptr->setFocus(true, Qt::MouseFocusReason); // The event itself may be a normal click, let the children handle it too return false; } bool FlickablePrivate::cancel(QMouseEvent*) { m_StartPoint = m_DragPoint = {}; q_ptr->setKeepMouseGrab(false); // Reject the event, let the click pass though return false; } bool FlickablePrivate::release(QMouseEvent*) { q_ptr->setKeepMouseGrab(false); q_ptr->ungrabMouse(); if (updateVelocity()) m_pTimer->start(); else applyEvent(DragEvent::TIMEOUT, nullptr); m_DragPoint = {}; return false; } bool FlickablePrivate::lock(QMouseEvent*) { q_ptr->setKeepMouseGrab(true); q_ptr->grabMouse(); return true; } bool FlickablePrivate::eval(QMouseEvent* e) { // It might look like an oversimplification, but the math here is correct. // Think of the rectangle being at the origin of a radiant wheel. The // hypotenuse of the rectangle will point at an angle. This code is // equivalent to the range PI/2 <-> 3*(PI/2) U 5*(PI/2) <-> 7*(PI/2) static const constexpr uchar EVENT_THRESHOLD = 10; // Reject large horizontal swipe and allow large vertical ones if (std::fabs(m_StartPoint.x() - e->pos().x()) > EVENT_THRESHOLD) { applyEvent(DragEvent::REJECT, e); return false; } else if (std::fabs(m_StartPoint.y() - e->pos().y()) > EVENT_THRESHOLD) applyEvent(DragEvent::ACCEPT, e); return drag(e); } bool FlickablePrivate::inertia(QMouseEvent*) { m_Velocity *= m_DecelRate; - q_ptr->setCurrentY(q_ptr->currentY() - m_Velocity); + q_ptr->setContentY(q_ptr->contentY() - m_Velocity); // Clamp the asymptotes to avoid an infinite loop, I chose a random value if (std::fabs(m_Velocity) < 0.05) applyEvent(DragEvent::TIMEOUT, nullptr); return true; } bool Flickable::isDragging() const { return d_ptr->m_State == FlickablePrivate::DragState::DRAGGED || d_ptr->m_State == FlickablePrivate::DragState::EVAL; } bool Flickable::isMoving() const { return isDragging() || d_ptr->m_State == FlickablePrivate::DragState::INERTIA; } qreal Flickable::flickDeceleration() const { return d_ptr->m_DecelRate; } void Flickable::setFlickDeceleration(qreal v) { d_ptr->m_DecelRate = v; } bool Flickable::isInteractive() const { return d_ptr->m_Interactive; } void Flickable::setInteractive(bool v) { d_ptr->m_Interactive = v; } qreal Flickable::maximumFlickVelocity() const { return d_ptr->m_MaxVelocity; } void Flickable::setMaximumFlickVelocity(qreal v) { d_ptr->m_MaxVelocity = v; } QQmlContext* Flickable::rootContext() const { if (!d_ptr->m_pRootContext) d_ptr->m_pRootContext = QQmlEngine::contextForObject(this); return d_ptr->m_pRootContext; } #include diff --git a/src/views/flickable.h b/src/views/flickable.h index 3275a9f..6ee39ec 100644 --- a/src/views/flickable.h +++ b/src/views/flickable.h @@ -1,107 +1,107 @@ /*************************************************************************** * Copyright (C) 2017 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 . * **************************************************************************/ #ifndef FLICKABLE_H #define FLICKABLE_H #include class FlickablePrivate; /** * This file re-implements the flickable view. * * It is necessary to avoid a dependency on Qt private APIs in order to * re-implement higher level views such as the tree view. The upstream code * could also hardly have been copy/pasted in this project as it depends on * yet more hidden APIs and the code (along with dependencies) is over an order * or magnitude larger than this implementation. It would have been a * maintainability nightmare. * * This implementation is API compatible with a small subset of the Flickable * properties and uses a 200 lines of code inertial state machine instead of * 1.5k line of vomit code to do the exact same job. */ class Q_DECL_EXPORT Flickable : public QQuickItem { Q_OBJECT public: // Implement some of the QtQuick2.Flickable API - Q_PROPERTY(qreal contentY READ currentY WRITE setCurrentY NOTIFY currentYChanged) + Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged ) Q_PROPERTY(bool dragging READ isDragging NOTIFY draggingChanged) Q_PROPERTY(bool flicking READ isDragging NOTIFY movingChanged) Q_PROPERTY(bool moving READ isDragging NOTIFY movingChanged) Q_PROPERTY(bool movingHorizontally READ isDragging NOTIFY movingChanged) Q_PROPERTY(bool draggingHorizontally READ isDragging NOTIFY draggingChanged) Q_PROPERTY(bool flickingHorizontally READ isDragging NOTIFY movingChanged) Q_PROPERTY(qreal flickDeceleration READ flickDeceleration WRITE setFlickDeceleration) Q_PROPERTY(bool interactive READ isInteractive WRITE setInteractive) Q_PROPERTY(qreal maximumFlickVelocity READ maximumFlickVelocity WRITE setMaximumFlickVelocity) /** * The geometry of the content subset currently displayed be the Flickable. * - * It is usually {0, currentY, height, width}. + * It is usually {0, contentY, height, width}. */ Q_PROPERTY(QRectF viewport READ viewport NOTIFY viewportChanged) explicit Flickable(QQuickItem* parent = nullptr); virtual ~Flickable(); - qreal currentY() const; - virtual void setCurrentY(qreal y); + qreal contentY() const; + virtual void setContentY(qreal y); QRectF viewport() const; qreal contentHeight() const; QQuickItem* contentItem(); bool isDragging() const; bool isMoving() const; qreal flickDeceleration() const; void setFlickDeceleration(qreal v); bool isInteractive() const; void setInteractive(bool v); qreal maximumFlickVelocity() const; void setMaximumFlickVelocity(qreal v); QQmlContext* rootContext() const; Q_SIGNALS: void contentHeightChanged(qreal height); - void currentYChanged(qreal y); + void contentYChanged(qreal y); void percentageChanged(qreal percent); void draggingChanged(bool dragging); void movingChanged(bool dragging); void viewportChanged(const QRectF &view); protected: bool event(QEvent *ev) override; bool childMouseEventFilter(QQuickItem *, QEvent *) override; void geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) override; private: FlickablePrivate* d_ptr; Q_DECLARE_PRIVATE(Flickable) }; #endif