flickable tabbars

This commit is contained in:
Uwe Rathmann 2020-03-13 07:39:31 +01:00
parent 3c7308e23f
commit 53e924a999
14 changed files with 333 additions and 88 deletions

View File

@ -39,7 +39,7 @@ class TabView : public QskTabView
TabView( QQuickItem* parent = nullptr )
: QskTabView( parent )
{
for ( int i = 0; i < 6; i++ )
for ( int i = 0; i < 10; i++ )
{
QString text;
if ( i == 4 )

View File

@ -569,6 +569,9 @@ void QskMaterialSkin::initTabBarHints()
setBoxShape( Q::Panel, 0 );
setBoxBorderMetrics( Q::Panel, 0 );
setGradient( Q::Panel, QskGradient() );
// when flicking
setAnimation( Q::Panel | Metric, QskAnimationHint( 200, QEasingCurve::InCubic ) );
}
void QskMaterialSkin::initTabViewHints()

View File

@ -641,6 +641,9 @@ void QskSquiekSkin::initTabBarHints()
setMargins( Q::Panel | Padding, 0 );
setMargins( Q::Panel | Margin, 0 );
setPanel( Q::Panel, NoPanel );
// when flicking
setAnimation( Q::Panel | Metric, QskAnimationHint( 200, QEasingCurve::OutCubic ) );
}
void QskSquiekSkin::initTabViewHints()

View File

@ -959,6 +959,15 @@ QRectF QskControl::focusIndicatorRect() const
return contentsRect();
}
bool QskControl::hasFocusIndicatorClip() const
{
/*
Often we want to clip the focus indicator,
when the control is clipped.
*/
return clip();
}
void QskControl::updateLayout()
{
}

View File

@ -91,7 +91,9 @@ class QSK_EXPORT QskControl : public QskQuickItem, public QskSkinnable
virtual QRectF layoutRectForSize( const QSizeF& ) const;
virtual QRectF gestureRect() const;
virtual QRectF focusIndicatorRect() const;
virtual bool hasFocusIndicatorClip() const;
QRectF subControlRect( QskAspect::Subcontrol ) const;
QRectF subControlRect( const QSizeF&, QskAspect::Subcontrol ) const;

View File

@ -103,17 +103,26 @@ void QskFocusIndicator::onFocusItemChanged()
{
auto itemParent = item->parentItem();
if ( itemParent == window()->contentItem() || itemParent->clip() )
bool doReparent = ( itemParent == window()->contentItem() );
if ( !doReparent )
{
/*
When the focus item is clipped - maybe because of being at the
border of a scrollarea - the focus indicator needs to be
border of a scrollarea - the focus indicator might need to be
clipped as well. The easiest way to have this is to put us
below the item having the clip.
*/
setParentItem( itemParent );
if ( auto control = qskControlCast( itemParent ) )
doReparent = control->hasFocusIndicatorClip();
else
doReparent = itemParent->clip();
}
if ( doReparent )
setParentItem( itemParent );
if ( itemParent == parentItem() )
{
// We want to be on top, but do we cover all corner cases ???

View File

@ -124,9 +124,13 @@ class QskScrollAreaClipItem final : public QskControl, public QQuickItemChangeLi
return scrollArea()->subControlRect( QskScrollView::Viewport );
}
bool hasFocusIndicatorClip() const override
{
return scrollArea()->hasFocusIndicatorClip();
}
protected:
bool event( QEvent* event ) override;
void windowChangeEvent( QskWindowChangeEvent* ) override;
void itemChange( ItemChange, const ItemChangeData& ) override;
void geometryChanged( const QRectF&, const QRectF& ) override;
@ -151,9 +155,6 @@ class QskScrollAreaClipItem final : public QskControl, public QQuickItemChangeLi
void updateNode( QSGNode* ) override;
private:
void connectWindow( const QQuickWindow*, bool on );
void onFocusItemChanged();
inline QskScrollArea* scrollArea()
{
return static_cast< QskScrollArea* >( parentItem() );
@ -172,8 +173,6 @@ QskScrollAreaClipItem::QskScrollAreaClipItem( QskScrollArea* scrollArea )
{
setObjectName( QStringLiteral( "QskScrollAreaClipItem" ) );
setClip( true );
connectWindow( window(), true );
}
QskScrollAreaClipItem::~QskScrollAreaClipItem()
@ -181,23 +180,6 @@ 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 );
@ -310,20 +292,6 @@ 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 )
@ -335,20 +303,11 @@ 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 )
, autoScrollFocusItem( true )
{
}
@ -369,7 +328,6 @@ class QskScrollArea::PrivateData
QskScrollAreaClipItem* clipItem = nullptr;
bool isItemResizable : 1;
bool autoScrollFocusItem : 1;
};
/*
@ -401,16 +359,6 @@ 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();
@ -457,18 +405,9 @@ void QskScrollArea::adjustItem()
}
}
void QskScrollArea::setAutoScrollFocusedItem( bool on )
bool QskScrollArea::hasFocusIndicatorClip() const
{
if ( m_data->autoScrollFocusItem != on )
{
m_data->autoScrollFocusItem = on;
Q_EMIT autoScrollFocusedItemChanged();
}
}
bool QskScrollArea::autoScrollFocusItem() const
{
return m_data->autoScrollFocusItem;
return true;
}
void QskScrollArea::setItemResizable( bool on )
@ -476,7 +415,7 @@ void QskScrollArea::setItemResizable( bool on )
if ( on != m_data->isItemResizable )
{
m_data->isItemResizable = on;
Q_EMIT itemResizableChanged();
Q_EMIT itemResizableChanged( on );
if ( m_data->isItemResizable )
polish();
@ -520,8 +459,7 @@ QQuickItem* QskScrollArea::scrolledItem() const
void QskScrollArea::translateItem()
{
auto item = m_data->clipItem->scrolledItem();
if ( item )
if ( auto item = m_data->clipItem->scrolledItem() )
{
const QPointF pos = viewContentsRect().topLeft() - scrollPos();
item->setPosition( pos );

View File

@ -18,9 +18,6 @@ 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:
@ -33,15 +30,11 @@ class QSK_EXPORT QskScrollArea : public QskScrollView
void setItemResizable( bool on );
bool isItemResizable() const;
void setAutoScrollFocusedItem( bool on );
bool autoScrollFocusItem() const;
void ensureItemVisible( const QQuickItem* );
bool hasFocusIndicatorClip() const override;
Q_SIGNALS:
void itemResizableChanged();
void scrolledItemChanged();
void autoScrollFocusedItemChanged();
void itemResizableChanged( bool );
protected:
void updateLayout() override;

View File

@ -9,6 +9,11 @@
#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
namespace
{
@ -95,6 +100,11 @@ namespace
class QskScrollBox::PrivateData
{
public:
PrivateData()
: autoScrollFocusItem( true )
{
}
QPointF scrollPos;
QSizeF scrollableSize = QSize( 0.0, 0.0 );
@ -105,6 +115,8 @@ class QskScrollBox::PrivateData
ScrollAnimator scroller;
const qreal viewportPadding = 10;
bool autoScrollFocusItem : 1;
};
QskScrollBox::QskScrollBox( QQuickItem* parent )
@ -119,14 +131,52 @@ QskScrollBox::QskScrollBox( QQuickItem* parent )
setFiltersChildMouseEvents( true );
setAcceptedMouseButtons( Qt::LeftButton );
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() )
{
auto reason = QQuickWindowPrivate::get( window() )->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 )
@ -202,6 +252,17 @@ QRectF QskScrollBox::gestureRect() const
return viewContentsRect();
}
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 = m_data->viewportPadding;
@ -282,6 +343,14 @@ void QskScrollBox::ensureVisible( const QRectF& itemRect )
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() )
@ -443,4 +512,21 @@ QPointF QskScrollBox::boundedScrollPos( const QPointF& pos ) const
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"

View File

@ -18,12 +18,18 @@ class QSK_EXPORT QskScrollBox : public QskControl
Q_PROPERTY( Qt::Orientations flickableOrientations READ flickableOrientations
WRITE setFlickableOrientations NOTIFY flickableOrientationsChanged FINAL )
Q_PROPERTY( bool autoScrollFocusedItem READ autoScrollFocusItem
WRITE setAutoScrollFocusedItem NOTIFY autoScrollFocusedItemChanged FINAL )
using Inherited = QskControl;
public:
QskScrollBox( QQuickItem* parent = nullptr );
~QskScrollBox() override;
void setAutoScrollFocusedItem( bool on );
bool autoScrollFocusItem() const;
void setFlickableOrientations( Qt::Orientations );
Qt::Orientations flickableOrientations() const;
@ -43,17 +49,21 @@ class QSK_EXPORT QskScrollBox : public QskControl
void scrollPosChanged();
void scrollableSizeChanged( const QSizeF& );
void autoScrollFocusedItemChanged( bool );
void flickableOrientationsChanged();
public Q_SLOTS:
void setScrollPos( const QPointF& );
void scrollTo( const QPointF& );
void ensureItemVisible( const QQuickItem* );
void ensureFocusItemVisible();
void ensureVisible( const QPointF& );
void ensureVisible( const QRectF& );
protected:
void geometryChangeEvent( QskGeometryChangeEvent* ) override;
void windowChangeEvent( QskWindowChangeEvent* ) override;
void gestureEvent( QskGestureEvent* ) override;
#ifndef QT_NO_WHEELEVENT
@ -66,6 +76,8 @@ class QSK_EXPORT QskScrollBox : public QskControl
private:
QPointF boundedScrollPos( const QPointF& ) const;
void onFocusItemChanged();
void connectWindow( const QQuickWindow*, bool on );
class PrivateData;
std::unique_ptr< PrivateData > m_data;

View File

@ -38,7 +38,6 @@ QskScrollView::QskScrollView( QQuickItem* parent )
: Inherited( parent )
, m_data( new PrivateData() )
{
setAcceptedMouseButtons( Qt::LeftButton );
}
QskScrollView::~QskScrollView()

View File

@ -5,9 +5,12 @@
#include "QskTabBar.h"
#include "QskAspect.h"
#include "QskScrollBox.h"
#include "QskLinearBox.h"
#include "QskTabButton.h"
#include "QskTextOptions.h"
#include "QskAnimationHint.h"
#include "QskQuick.h"
QSK_SUBCONTROL( QskTabBar, Panel )
@ -21,8 +24,10 @@ static inline Qt::Orientation qskOrientation( int position )
namespace
{
class ButtonBox : public QskLinearBox
class ButtonBox final : public QskLinearBox
{
using Inherited = QskLinearBox;
public:
ButtonBox( Qt::Orientation orientation, QQuickItem* parent )
: QskLinearBox( orientation, parent )
@ -48,6 +53,142 @@ namespace
}
}
};
class ScrollBox final : public QskScrollBox
{
using Inherited = QskScrollBox;
public:
ScrollBox( QQuickItem* parent )
: QskScrollBox( parent )
{
setPolishOnResize( true );
enableAutoTranslation( true );
setFocusPolicy( Qt::NoFocus );
}
bool hasFocusIndicatorClip() const override
{
return false;
}
QskAnimationHint flickHint() const override
{
if ( auto tabBar = qobject_cast< const QskTabBar* >( parentItem() ) )
{
return tabBar->effectiveAnimation( QskAspect::Metric,
QskTabBar::Panel, QskAspect::NoState );
}
return QskAnimationHint( 200, QEasingCurve::OutCubic );
}
QRectF viewContentsRect() const override
{
return layoutRect();
}
void setOrientation( Qt::Orientation orientation )
{
setFlickableOrientations( orientation );
if ( orientation == Qt::Horizontal )
setSizePolicy( QskSizePolicy::Ignored, QskSizePolicy::MinimumExpanding );
else
setSizePolicy( QskSizePolicy::MinimumExpanding, QskSizePolicy::Ignored );
}
void ensureItemVisible( const QQuickItem* item )
{
if ( qskIsAncestorOf( this, item ) )
{
const auto pos = mapFromItem( item, QPointF() );
ensureVisible( QRectF( pos.x(), pos.y(), item->width(), item->height() ) );
}
}
protected:
bool event( QEvent* event ) override
{
if ( event->type() == QEvent::LayoutRequest )
{
resetImplicitSize();
polish();
}
return Inherited::event( event );
}
void updateLayout() override
{
auto scrolledItem = this->scrolledItem();
auto itemSize = viewContentsRect().size();
itemSize = qskConstrainedItemSize( scrolledItem, itemSize );
scrolledItem->setSize( itemSize );
enableAutoTranslation( false );
setScrollableSize( itemSize );
setScrollPos( scrollPos() );
enableAutoTranslation( true );
translateItem();
setClip( size().width() < itemSize.width()
|| size().height() < itemSize.height() );
}
QSizeF layoutSizeHint( Qt::SizeHint which, const QSizeF& constraint ) const override
{
auto hint = scrolledItem()->sizeConstraint( which, constraint );
if ( which == Qt::MinimumSize )
{
if ( sizePolicy().horizontalPolicy() & QskSizePolicy::ShrinkFlag )
hint.setWidth( -1 );
if ( sizePolicy().verticalPolicy() & QskSizePolicy::ShrinkFlag )
hint.setHeight( -1 );
}
return hint;
}
private:
inline QskControl* scrolledItem() const
{
return qskControlCast( childItems().first() );
}
void enableAutoTranslation( bool on )
{
if ( on )
{
connect( this, &QskScrollBox::scrollPosChanged,
this, &ScrollBox::translateItem );
}
else
{
disconnect( this, &QskScrollBox::scrollPosChanged,
this, &ScrollBox::translateItem );
}
}
void translateItem()
{
if ( auto item = this->scrolledItem() )
{
const QPointF pos = viewContentsRect().topLeft() - scrollPos();
item->setPosition( pos );
}
}
};
}
class QskTabBar::PrivateData
@ -72,6 +213,7 @@ class QskTabBar::PrivateData
}
}
ScrollBox* scrollBox = nullptr;
ButtonBox* buttonBox = nullptr;
int currentIndex = -1;
@ -97,9 +239,17 @@ QskTabBar::QskTabBar( Qsk::Position position, QQuickItem* parent )
else
initSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Preferred );
m_data->buttonBox = new ButtonBox( orientation, this );
m_data->scrollBox = new ScrollBox( this );
m_data->scrollBox->setOrientation( orientation );
m_data->buttonBox = new ButtonBox( orientation, m_data->scrollBox );
m_data->buttonBox->setSpacing( metric( QskTabBar::Panel | QskAspect::Spacing ) );
#if 1
// We might want to have a mode, where the buttons are stretched: TODO ...
m_data->buttonBox->setSizePolicy( QskSizePolicy::Maximum, QskSizePolicy::Maximum );
#endif
connect( this, &QskTabBar::currentIndexChanged,
m_data->buttonBox, &ButtonBox::restack, Qt::QueuedConnection );
}
@ -120,6 +270,7 @@ void QskTabBar::setPosition( Qsk::Position position )
{
setSizePolicy( sizePolicy( Qt::Vertical ), sizePolicy( Qt::Horizontal ) );
m_data->buttonBox->setOrientation( orientation );
m_data->scrollBox->setOrientation( orientation );
}
resetImplicitSize();
@ -140,6 +291,20 @@ Qt::Orientation QskTabBar::orientation() const
return qskOrientation( m_data->position );
}
void QskTabBar::setAutoScrollFocusedButton( bool on )
{
if ( m_data->scrollBox->autoScrollFocusItem() != on )
{
m_data->scrollBox->setAutoScrollFocusedItem( on );
Q_EMIT autoScrollFocusedButtonChanged( on );
}
}
bool QskTabBar::autoScrollFocusButton() const
{
return m_data->scrollBox->autoScrollFocusItem();
}
void QskTabBar::setTextOptions( const QskTextOptions& options )
{
if ( options != m_data->textOptions )
@ -366,6 +531,11 @@ int QskTabBar::indexOf( QskTabButton* button ) const
return m_data->buttonBox->indexOf( button );
}
void QskTabBar::ensureButtonVisible( const QskTabButton* button )
{
m_data->scrollBox->ensureItemVisible( button );
}
void QskTabBar::componentComplete()
{
Inherited::componentComplete();

View File

@ -20,6 +20,9 @@ class QSK_EXPORT QskTabBar : public QskBox
Q_PROPERTY( Qt::Orientation orientation READ orientation )
Q_PROPERTY( bool autoScrollFocusButton READ autoScrollFocusButton
WRITE setAutoScrollFocusedButton NOTIFY autoScrollFocusedButtonChanged FINAL )
Q_PROPERTY( int count READ count NOTIFY countChanged FINAL )
Q_PROPERTY( int currentIndex READ currentIndex
@ -43,6 +46,11 @@ class QSK_EXPORT QskTabBar : public QskBox
Qt::Orientation orientation() const;
void setAutoScrollFocusedButton( bool on );
bool autoScrollFocusButton() const;
void ensureButtonVisible( const QskTabButton* );
void setTextOptions( const QskTextOptions& );
QskTextOptions textOptions() const;
@ -86,6 +94,7 @@ class QSK_EXPORT QskTabBar : public QskBox
void countChanged( int );
void textOptionsChanged( const QskTextOptions& );
void positionChanged( Qsk::Position );
void autoScrollFocusedButtonChanged( bool );
protected:
void componentComplete() override;

View File

@ -210,12 +210,22 @@ QSizeF QskTabView::layoutSizeHint(
const auto& tabBar = m_data->tabBar;
const auto& stackBox = m_data->stackBox;
const auto barHint = tabBar->sizeConstraint( which );
auto barHint = tabBar->sizeConstraint( which );
#if 1
/*
How to limit the constribution of the tabbar in a reasonable way ?
QTabWidget uses 200x200 - what is kind of random. TODO ...
*/
const qreal minBarSize = 200;
#endif
QSizeF hint;
if ( orientation() == Qt::Vertical )
{
barHint.setWidth( qMin( barHint.width(), minBarSize ) );
if ( constraint.width() >= 0.0 )
{
qreal w = qMax( constraint.width(), barHint.width() );
@ -240,6 +250,8 @@ QSizeF QskTabView::layoutSizeHint(
}
else
{
barHint.setHeight( qMin( barHint.height(), minBarSize ) );
if ( constraint.width() >= 0.0 )
{
qreal w = constraint.width() - barHint.width();