qskinny/src/controls/QskScrollBox.cpp
2023-10-05 08:59:30 +02:00

497 lines
12 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskScrollBox.h"
#include "QskAnimationHint.h"
#include "QskEvent.h"
#include "QskFlickAnimator.h"
#include "QskGesture.h"
#include "QskPanGestureRecognizer.h"
#include "QskQuick.h"
QSK_QT_PRIVATE_BEGIN
#include <private/qquickwindow_p.h>
QSK_QT_PRIVATE_END
static inline constexpr qreal qskViewportPadding()
{
return 10.0; // should be from the skin, TODO ...
}
static inline bool qskIsScrollable(
const QskScrollBox* scrollBox, Qt::Orientations orientations )
{
if ( orientations )
{
const QSizeF viewSize = scrollBox->viewContentsRect().size();
const QSizeF& scrollableSize = scrollBox->scrollableSize();
if ( orientations & Qt::Vertical )
{
if ( viewSize.height() < scrollableSize.height() )
return true;
}
if ( orientations & Qt::Horizontal )
{
if ( viewSize.width() < scrollableSize.width() )
return true;
}
}
return false;
}
namespace
{
class FlickAnimator final : public QskFlickAnimator
{
public:
FlickAnimator()
{
// skin hints: TODO
setDuration( 1000 );
setEasingCurve( QEasingCurve::OutCubic );
}
void setScrollBox( QskScrollBox* scrollBox )
{
m_scrollBox = scrollBox;
}
void translate( qreal dx, qreal dy ) override
{
const QPointF pos = m_scrollBox->scrollPos();
m_scrollBox->setScrollPos( pos - QPointF( dx, -dy ) );
}
private:
QskScrollBox* m_scrollBox;
};
class ScrollAnimator final : public QskAnimator
{
public:
ScrollAnimator()
: m_scrollBox( nullptr )
{
}
void setScrollBox( QskScrollBox* scrollBox )
{
m_scrollBox = scrollBox;
}
void scroll( const QPointF& from, const QPointF& to )
{
if ( isRunning() )
{
m_to = to;
return;
}
if ( from == to || m_scrollBox == nullptr )
{
return;
}
m_from = from;
m_to = to;
const auto hint = m_scrollBox->flickHint();
setDuration( hint.duration );
setEasingCurve( hint.type );
setWindow( m_scrollBox->window() );
start();
}
protected:
void advance( qreal value ) override
{
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_scrollBox->setScrollPos( QPointF( x, y ) );
}
private:
QskScrollBox* m_scrollBox;
QPointF m_from;
QPointF m_to;
};
class PanRecognizer final : public QskPanGestureRecognizer
{
using Inherited = QskPanGestureRecognizer;
public:
PanRecognizer( QObject* parent = nullptr )
: QskPanGestureRecognizer( parent )
{
setOrientations( Qt::Horizontal | Qt::Vertical );
}
QRectF gestureRect() const override
{
if ( auto scrollBox = qobject_cast< const QskScrollBox* >( watchedItem() ) )
{
if ( qskIsScrollable( scrollBox, orientations() ) )
return scrollBox->viewContentsRect();
}
return QRectF( 0.0, 0.0, -1.0, -1.0 ); // empty
}
};
}
class QskScrollBox::PrivateData
{
public:
QPointF scrollPos;
QSizeF scrollableSize = QSize( 0.0, 0.0 );
PanRecognizer panRecognizer;
FlickAnimator flicker;
ScrollAnimator scroller;
bool autoScrollFocusItem = true;
};
QskScrollBox::QskScrollBox( QQuickItem* parent )
: Inherited( parent )
, m_data( new PrivateData() )
{
m_data->flicker.setScrollBox( this );
m_data->scroller.setScrollBox( this );
m_data->panRecognizer.setWatchedItem( this );
setAcceptedMouseButtons( Qt::LeftButton );
setFiltersChildMouseEvents( true );
setWheelEnabled( true );
setFocusPolicy( Qt::StrongFocus );
connectWindow( window(), true );
}
QskScrollBox::~QskScrollBox()
{
}
void QskScrollBox::setAutoScrollFocusedItem( bool on )
{
if ( m_data->autoScrollFocusItem != on )
{
m_data->autoScrollFocusItem = on;
connectWindow( window(), true );
Q_EMIT autoScrollFocusedItemChanged( on );
}
}
bool QskScrollBox::autoScrollFocusItem() const
{
return m_data->autoScrollFocusItem;
}
void QskScrollBox::onFocusItemChanged()
{
if ( window() )
{
#if QT_VERSION >= QT_VERSION_CHECK( 6, 1, 0 )
auto wd = QQuickWindowPrivate::get( window() )->deliveryAgentPrivate();
#else
auto wd = QQuickWindowPrivate::get( window() );
#endif
auto reason = wd->lastFocusReason;
if ( reason == Qt::TabFocusReason || reason == Qt::BacktabFocusReason )
ensureFocusItemVisible();
}
}
void QskScrollBox::ensureFocusItemVisible()
{
if ( window() == nullptr )
return;
if ( const auto focusItem = window()->activeFocusItem() )
ensureItemVisible( focusItem );
}
void QskScrollBox::setFlickRecognizerTimeout( int timeout )
{
if ( timeout < 0 )
timeout = -1;
m_data->panRecognizer.setTimeout( timeout );
}
int QskScrollBox::flickRecognizerTimeout() const
{
return m_data->panRecognizer.timeout();
}
void QskScrollBox::setFlickableOrientations( Qt::Orientations orientations )
{
if ( m_data->panRecognizer.orientations() != orientations )
{
m_data->panRecognizer.setOrientations( orientations );
Q_EMIT flickableOrientationsChanged();
}
}
Qt::Orientations QskScrollBox::flickableOrientations() const
{
return m_data->panRecognizer.orientations();
}
void QskScrollBox::setScrollPos( const QPointF& pos )
{
const QPointF boundedPos = boundedScrollPos( pos );
if ( boundedPos != m_data->scrollPos )
{
m_data->scrollPos = boundedPos;
update();
Q_EMIT scrollPosChanged();
Q_EMIT scrolledTo( boundedPos );
}
}
QPointF QskScrollBox::scrollPos() const
{
return m_data->scrollPos;
}
void QskScrollBox::scrollTo( const QPointF& pos )
{
m_data->scroller.scroll( scrollPos(), pos );
}
void QskScrollBox::setScrollableSize( const QSizeF& size )
{
const QSizeF boundedSize = size.expandedTo( QSizeF( 0, 0 ) );
if ( boundedSize != m_data->scrollableSize )
{
m_data->scrollableSize = boundedSize;
Q_EMIT scrollableSizeChanged( m_data->scrollableSize );
setScrollPos( m_data->scrollPos ); // scroll pos might need to be re-bounded
update();
}
}
QSizeF QskScrollBox::scrollableSize() const
{
return m_data->scrollableSize;
}
void QskScrollBox::ensureItemVisible( const QQuickItem* item )
{
if ( qskIsAncestorOf( this, item ) )
{
auto pos = scrollPos() - viewContentsRect().topLeft();
pos += mapFromItem( item, QPointF() );
ensureVisible( QRectF( pos.x(), pos.y(), item->width(), item->height() ) );
}
}
void QskScrollBox::ensureVisible( const QPointF& pos )
{
const qreal margin = qskViewportPadding();
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() && window() )
scrollTo( newPos );
else
setScrollPos( newPos );
}
void QskScrollBox::ensureVisible( const QRectF& itemRect )
{
const qreal margin = qskViewportPadding();
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() && window() )
scrollTo( newPos );
else
setScrollPos( newPos );
}
void QskScrollBox::windowChangeEvent( QskWindowChangeEvent* event )
{
Inherited::windowChangeEvent( event );
connectWindow( event->oldWindow(), false );
connectWindow( event->window(), true );
}
void QskScrollBox::geometryChangeEvent( QskGeometryChangeEvent* event )
{
if ( event->isResized() )
setScrollPos( scrollPos() );
Inherited::geometryChangeEvent( event );
}
void QskScrollBox::gestureEvent( QskGestureEvent* event )
{
if ( event->gesture()->type() == QskGesture::Pan )
{
const auto gesture = static_cast< const QskPanGesture* >( event->gesture().get() );
switch ( gesture->state() )
{
case QskGesture::Updated:
{
setScrollPos( scrollPos() - gesture->delta() );
break;
}
case QskGesture::Finished:
{
m_data->flicker.setWindow( window() );
m_data->flicker.accelerate( gesture->angle(), gesture->velocity() );
break;
}
case QskGesture::Canceled:
{
// what to do here: maybe going back to the origin of the gesture ??
break;
}
default:
break;
}
return;
}
Inherited::gestureEvent( event );
}
#ifndef QT_NO_WHEELEVENT
QPointF QskScrollBox::scrollOffset( const QWheelEvent* event ) const
{
QPointF offset;
const auto pos = qskWheelPosition( event );
const auto viewRect = viewContentsRect();
if ( viewRect.contains( pos ) )
{
offset = event->pixelDelta();
if ( offset.isNull() )
offset = event->angleDelta() / QWheelEvent::DefaultDeltasPerStep;
offset.rx() *= viewRect.width();
offset.ry() *= viewRect.height();
}
return offset;
}
void QskScrollBox::wheelEvent( QWheelEvent* event )
{
const auto offset = scrollOffset( event );
if ( !offset.isNull() )
setScrollPos( m_data->scrollPos - offset );
}
#endif
QPointF QskScrollBox::boundedScrollPos( const QPointF& pos ) const
{
const QRectF vr = viewContentsRect();
const qreal maxX = qMax( 0.0, scrollableSize().width() - vr.width() );
const qreal maxY = qMax( 0.0, scrollableSize().height() - vr.height() );
return QPointF( qBound( 0.0, pos.x(), maxX ), qBound( 0.0, pos.y(), maxY ) );
}
void QskScrollBox::connectWindow( const QQuickWindow* window, bool on )
{
if ( ( window == nullptr ) || ( on && !autoScrollFocusItem() ) )
return;
if ( on )
{
QObject::connect( window, &QQuickWindow::activeFocusItemChanged,
this, &QskScrollBox::onFocusItemChanged, Qt::UniqueConnection );
}
else
{
QObject::disconnect( window, &QQuickWindow::activeFocusItemChanged,
this, &QskScrollBox::onFocusItemChanged );
}
}
#include "moc_QskScrollBox.cpp"