diff --git a/skins/material/QskMaterialSkin.cpp b/skins/material/QskMaterialSkin.cpp index ae50589d..37a4b8b0 100644 --- a/skins/material/QskMaterialSkin.cpp +++ b/skins/material/QskMaterialSkin.cpp @@ -594,6 +594,9 @@ void QskMaterialSkin::initScrollViewHints() setGradient( subControl, pal.accentColor ); setBoxBorderColors( subControl, pal.accentColor ); } + + // when changing the position by QskScrollView::scrollTo + setAnimation( Q::Viewport | Metric, QskAnimationHint( 200, QEasingCurve::InCubic ) ); } void QskMaterialSkin::initListViewHints() diff --git a/skins/squiek/QskSquiekSkin.cpp b/skins/squiek/QskSquiekSkin.cpp index 7e0f7153..4d199662 100644 --- a/skins/squiek/QskSquiekSkin.cpp +++ b/skins/squiek/QskSquiekSkin.cpp @@ -634,6 +634,9 @@ void QskSquiekSkin::initScrollViewHints() setAnimation( subControl | Color, qskDuration ); } + + // when changing the position by QskScrollView::scrollTo + setAnimation( Q::Viewport | Metric, QskAnimationHint( 200, QEasingCurve::OutCubic ) ); } void QskSquiekSkin::initListViewHints() diff --git a/src/controls/QskListView.cpp b/src/controls/QskListView.cpp index da92d755..e590670e 100644 --- a/src/controls/QskListView.cpp +++ b/src/controls/QskListView.cpp @@ -223,10 +223,12 @@ void QskListView::keyPressEvent( QKeyEvent* event ) if ( row != r ) { + auto pos = scrollPos(); + const qreal rowPos = row * rowHeight(); if ( rowPos < scrollPos().y() ) { - setScrollPos( QPointF( scrollPos().x(), rowPos ) ); + pos.setY( rowPos ); } else { @@ -236,9 +238,17 @@ void QskListView::keyPressEvent( QKeyEvent* event ) if ( rowPos + rowHeight() > scrolledBottom ) { const double y = rowPos + rowHeight() - vr.height(); - setScrollPos( QPointF( scrollPos().x(), y ) ); + pos.setY( y ); } } + + if ( pos != scrollPos() ) + { + if ( event->isAutoRepeat() ) + setScrollPos( pos ); + else + scrollTo( pos ); + } } } diff --git a/src/controls/QskPopup.cpp b/src/controls/QskPopup.cpp index 9e5d1bdc..70854192 100644 --- a/src/controls/QskPopup.cpp +++ b/src/controls/QskPopup.cpp @@ -215,7 +215,7 @@ QskPopup::QskPopup( QQuickItem* parent ): // we don't want to be resized by layout code setTransparentForPositioner( true ); - setFlag( ItemIsFocusScope ); + setFlag( ItemIsFocusScope, true ); setTabFence( true ); setFocusPolicy( Qt::ClickFocus ); } diff --git a/src/controls/QskScrollArea.cpp b/src/controls/QskScrollArea.cpp index 4896e80d..4ac59dca 100644 --- a/src/controls/QskScrollArea.cpp +++ b/src/controls/QskScrollArea.cpp @@ -7,13 +7,28 @@ #include "QskScrollViewSkinlet.h" #include "QskLayoutConstraint.h" #include "QskBoxClipNode.h" +#include "QskEvent.h" QSK_QT_PRIVATE_BEGIN #include +#include #include #include QSK_QT_PRIVATE_END +static inline bool qskIsAncestorOf( const QQuickItem* item, const QQuickItem* child ) +{ + while ( child ) + { + if ( child == item ) + return true; + + child = child->parentItem(); + } + + return false; +} + static QSizeF qskAdjustedSize( const QQuickItem* item, const QSizeF& targetSize ) { using namespace QskLayoutConstraint; @@ -171,6 +186,7 @@ public: protected: virtual bool event( QEvent* event ) override final; + virtual void windowChangeEvent( QskWindowChangeEvent* ) override final; virtual void itemChange( ItemChange, const ItemChangeData& ) override final; virtual void geometryChanged( const QRectF&, const QRectF& ) override final; @@ -195,6 +211,9 @@ protected: virtual void updateNode( QSGNode* ) override final; private: + void connectWindow( const QQuickWindow*, bool on ); + void onFocusItemChanged(); + inline QskScrollArea* scrollArea() { return static_cast< QskScrollArea* >( parentItem() ); @@ -213,6 +232,8 @@ QskScrollAreaClipItem::QskScrollAreaClipItem( QskScrollArea* scrollArea ): { setObjectName( QStringLiteral( "QskScrollAreaClipItem" ) ); setClip( true ); + + connectWindow( window(), true ); } QskScrollAreaClipItem::~QskScrollAreaClipItem() @@ -220,6 +241,23 @@ QskScrollAreaClipItem::~QskScrollAreaClipItem() enableGeometryListener( false ); } +void QskScrollAreaClipItem::connectWindow( const QQuickWindow* window, bool on ) +{ + if ( window == nullptr ) + return; + + if ( on ) + { + connect( window, &QQuickWindow::activeFocusItemChanged, + this, &QskScrollAreaClipItem::onFocusItemChanged ); + } + else + { + disconnect( window, &QQuickWindow::activeFocusItemChanged, + this, &QskScrollAreaClipItem::onFocusItemChanged ); + } +} + void QskScrollAreaClipItem::updateNode( QSGNode* ) { auto* d = QQuickItemPrivate::get( this ); @@ -317,7 +355,7 @@ void QskScrollAreaClipItem::itemChange( { enableGeometryListener( false ); } - + Inherited::itemChange( change, value ); } @@ -337,6 +375,20 @@ void QskScrollAreaClipItem::enableGeometryListener( bool on ) } } +void QskScrollAreaClipItem::onFocusItemChanged() +{ + if ( window() == nullptr || !scrollArea()->autoScrollFocusItem() ) + return; + + const auto focusItem = window()->activeFocusItem(); + if ( focusItem ) + { + auto reason = QQuickWindowPrivate::get( window() )->lastFocusReason; + if ( reason == Qt::TabFocusReason || reason == Qt::BacktabFocusReason ) + scrollArea()->ensureItemVisible( focusItem ); + } +} + bool QskScrollAreaClipItem::event( QEvent* event ) { if( event->type() == QEvent::LayoutRequest ) @@ -348,11 +400,20 @@ bool QskScrollAreaClipItem::event( QEvent* event ) return Inherited::event( event ); } +void QskScrollAreaClipItem::windowChangeEvent( QskWindowChangeEvent* event ) +{ + Inherited::windowChangeEvent( event ); + + connectWindow( event->oldWindow(), false ); + connectWindow( event->window(), true ); +} + class QskScrollArea::PrivateData { public: PrivateData(): - isItemResizable( true ) + isItemResizable( true ), + autoScrollFocusItem( true ) { } @@ -371,7 +432,9 @@ public: } QskScrollAreaClipItem* clipItem; + bool isItemResizable : 1; + bool autoScrollFocusItem : 1; }; /* @@ -424,6 +487,16 @@ QskScrollArea::~QskScrollArea() delete m_data->clipItem; } +void QskScrollArea::ensureItemVisible( const QQuickItem* item ) +{ + const QQuickItem* scrolledItem = this->scrolledItem(); + if ( scrolledItem && qskIsAncestorOf( scrolledItem, item ) ) + { + const auto pos = scrolledItem->mapFromItem( item, QPointF() ); + ensureVisible( QRectF( pos.x(), pos.y(), item->width(), item->height() ) ); + } +} + void QskScrollArea::updateLayout() { Inherited::updateLayout(); @@ -469,6 +542,20 @@ void QskScrollArea::adjustItem() } } +void QskScrollArea::setAutoScrollFocusedItem( bool on ) +{ + if ( m_data->autoScrollFocusItem != on ) + { + m_data->autoScrollFocusItem = on; + Q_EMIT autoScrollFocusedItemChanged(); + } +} + +bool QskScrollArea::autoScrollFocusItem() const +{ + return m_data->autoScrollFocusItem; +} + void QskScrollArea::setItemResizable( bool on ) { if ( on != m_data->isItemResizable ) diff --git a/src/controls/QskScrollArea.h b/src/controls/QskScrollArea.h index aa428202..654106a8 100644 --- a/src/controls/QskScrollArea.h +++ b/src/controls/QskScrollArea.h @@ -18,6 +18,9 @@ class QSK_EXPORT QskScrollArea : public QskScrollView Q_PROPERTY( bool itemResizable READ isItemResizable WRITE setItemResizable NOTIFY itemResizableChanged FINAL ) + Q_PROPERTY( bool autoScrollFocusedItem READ autoScrollFocusItem + WRITE setAutoScrollFocusedItem NOTIFY autoScrollFocusedItemChanged FINAL ) + using Inherited = QskScrollView; public: @@ -30,9 +33,15 @@ public: void setItemResizable( bool on ); bool isItemResizable() const; + void setAutoScrollFocusedItem( bool on ); + bool autoScrollFocusItem() const; + + void ensureItemVisible( const QQuickItem * ); + Q_SIGNALS: void itemResizableChanged(); void scrolledItemChanged(); + void autoScrollFocusedItemChanged(); protected: virtual void updateLayout() override; diff --git a/src/controls/QskScrollView.cpp b/src/controls/QskScrollView.cpp index 1dad282d..6f04724e 100644 --- a/src/controls/QskScrollView.cpp +++ b/src/controls/QskScrollView.cpp @@ -7,6 +7,7 @@ #include "QskPanGestureRecognizer.h" #include "QskFlickAnimator.h" #include "QskBoxBorderMetrics.h" +#include "QskAnimationHint.h" #include "QskGesture.h" #include "QskAspect.h" #include "QskEvent.h" @@ -25,11 +26,12 @@ QSK_STATE( QskScrollView, HorizontalHandlePressed, QskAspect::FirstSystemState < namespace { - class FlickAnimator : public QskFlickAnimator + class FlickAnimator final : public QskFlickAnimator { public: FlickAnimator() { + // skin hints: TODO setDuration( 1000 ); setEasingCurve( QEasingCurve::OutCubic ); } @@ -48,6 +50,61 @@ namespace private: QskScrollView* m_scrollView; }; + + class ScrollAnimator final : public QskAnimator + { + public: + ScrollAnimator(): + m_scrollView( nullptr ) + { + } + + void setScrollView( QskScrollView* scrollView ) + { + m_scrollView = scrollView; + } + + void scroll( const QPointF& from, const QPointF& to ) + { + if ( isRunning() ) + { + m_to = to; + return; + } + + if ( from == to || m_scrollView == nullptr ) + { + return; + } + + m_from = from; + m_to = to; + + const auto hint = m_scrollView->effectiveAnimation( + QskAspect::Metric, QskScrollView::Viewport ); + + setDuration( hint.duration ); + setEasingCurve( hint.type ); + setWindow( m_scrollView->window() ); + + start(); + } + + protected: + virtual void advance( qreal value ) override final + { + qreal x = m_from.x() + ( m_to.x() - m_from.x() ) * value; + qreal y = m_from.y() + ( m_to.y() - m_from.y() ) * value; + + m_scrollView->setScrollPos( QPointF( x, y ) ); + } + + private: + QskScrollView* m_scrollView; + + QPointF m_from; + QPointF m_to; + }; } class QskScrollView::PrivateData @@ -58,6 +115,7 @@ public: verticalScrollBarPolicy( Qt::ScrollBarAsNeeded ), scrollableSize( 0.0, 0.0 ), panRecognizerTimeout( 100 ), // value coming from the platform ??? + viewportPadding( 10 ), isScrolling( 0 ) { } @@ -72,6 +130,9 @@ public: int panRecognizerTimeout; FlickAnimator flicker; + ScrollAnimator scroller; + + qreal viewportPadding; qreal scrollPressPos; int isScrolling; @@ -82,6 +143,7 @@ QskScrollView::QskScrollView( QQuickItem* parent ): m_data( new PrivateData() ) { m_data->flicker.setScrollView( this ); + m_data->scroller.setScrollView( this ); m_data->panRecognizer.setWatchedItem( this ); m_data->panRecognizer.setOrientations( Qt::Horizontal | Qt::Vertical ); @@ -174,6 +236,11 @@ QPointF QskScrollView::scrollPos() const return m_data->scrollPos; } +void QskScrollView::scrollTo( const QPointF& pos ) +{ + m_data->scroller.scroll( scrollPos(), pos ); +} + bool QskScrollView::isScrolling( Qt::Orientation orientation ) const { return m_data->isScrolling == orientation; @@ -198,6 +265,86 @@ QSizeF QskScrollView::scrollableSize() const return m_data->scrollableSize; } +void QskScrollView::ensureVisible( const QPointF& pos ) +{ + const qreal margin = m_data->viewportPadding; + + QRectF r( scrollPos(), viewContentsRect().size() ); + r.adjust( margin, margin, -margin, -margin ); + + qreal x = r.x(); + qreal y = r.y(); + + if ( pos.x() < r.left() ) + { + x = pos.x(); + } + else if ( pos.x() > r.right() ) + { + x = pos.x() - r.width(); + } + + if ( pos.y() < r.top() ) + { + y = pos.y(); + } + else if ( y > r.right() ) + { + y = pos.y() - r.height(); + } + + const QPoint newPos( x - margin, y - margin ); + + if ( isInitiallyPainted() ) + scrollTo( newPos ); + else + setScrollPos( newPos ); +} + +void QskScrollView::ensureVisible( const QRectF& itemRect ) +{ + const qreal margin = m_data->viewportPadding; + + QRectF r( scrollPos(), viewContentsRect().size() ); + r.adjust( margin, margin, -margin, -margin ); + + qreal x = r.x(); + qreal y = r.y(); + + if ( itemRect.width() > r.width() ) + { + x = itemRect.left() + 0.5 * ( itemRect.width() - r.width() ); + } + else if ( itemRect.right() > r.right() ) + { + x = itemRect.right() - r.width(); + } + else if ( itemRect.left() < r.left() ) + { + x = itemRect.left(); + } + + if ( itemRect.height() > r.height() ) + { + y = itemRect.top() + 0.5 * ( itemRect.height() - r.height() ); + } + else if ( itemRect.bottom() > r.bottom() ) + { + y = itemRect.bottom() - r.height(); + } + else if ( itemRect.top() < r.top() ) + { + y = itemRect.top(); + } + + const QPoint newPos( x - margin, y - margin ); + + if ( isInitiallyPainted() ) + scrollTo( newPos ); + else + setScrollPos( newPos ); +} + QRectF QskScrollView::viewContentsRect() const { // This code should be done in the skinlet. TODO ... diff --git a/src/controls/QskScrollView.h b/src/controls/QskScrollView.h index 666ce2b9..c712c90b 100644 --- a/src/controls/QskScrollView.h +++ b/src/controls/QskScrollView.h @@ -67,6 +67,10 @@ Q_SIGNALS: public Q_SLOTS: void setScrollPos( const QPointF& ); + void scrollTo( const QPointF& ); + + void ensureVisible( const QPointF& ); + void ensureVisible( const QRectF& ); protected: virtual void mouseMoveEvent( QMouseEvent* ) override;