QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskScrollBox.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskScrollBox.h"
7#include "QskAnimationHint.h"
8#include "QskEvent.h"
9#include "QskFlickAnimator.h"
10#include "QskGesture.h"
11#include "QskPanGestureRecognizer.h"
12#include "QskQuick.h"
13
14QSK_QT_PRIVATE_BEGIN
15#include <private/qquickwindow_p.h>
16QSK_QT_PRIVATE_END
17
18static inline constexpr qreal qskViewportPadding()
19{
20 return 10.0; // should be from the skin, TODO ...
21}
22
23static inline bool qskIsScrollable(
24 const QskScrollBox* scrollBox, Qt::Orientations orientations )
25{
26 if ( orientations )
27 {
28 const QSizeF viewSize = scrollBox->viewContentsRect().size();
29 const QSizeF& scrollableSize = scrollBox->scrollableSize();
30
31 if ( orientations & Qt::Vertical )
32 {
33 if ( viewSize.height() < scrollableSize.height() )
34 return true;
35 }
36
37 if ( orientations & Qt::Horizontal )
38 {
39 if ( viewSize.width() < scrollableSize.width() )
40 return true;
41 }
42 }
43
44 return false;
45}
46
47namespace
48{
49 class FlickAnimator final : public QskFlickAnimator
50 {
51 public:
52 FlickAnimator()
53 {
54 // skin hints: TODO
55 setDuration( 1000 );
56 setEasingCurve( QEasingCurve::OutCubic );
57 }
58
59 void setScrollBox( QskScrollBox* scrollBox )
60 {
61 m_scrollBox = scrollBox;
62 }
63
64 void translate( qreal dx, qreal dy ) override
65 {
66 const QPointF pos = m_scrollBox->scrollPos();
67 m_scrollBox->setScrollPos( pos - QPointF( dx, -dy ) );
68 }
69
70 private:
71 QskScrollBox* m_scrollBox;
72 };
73
74 class ScrollAnimator final : public QskAnimator
75 {
76 public:
77 ScrollAnimator()
78 : m_scrollBox( nullptr )
79 {
80 }
81
82 void setScrollBox( QskScrollBox* scrollBox )
83 {
84 m_scrollBox = scrollBox;
85 }
86
87 void scroll( const QPointF& from, const QPointF& to )
88 {
89 if ( isRunning() )
90 {
91 m_to = to;
92 return;
93 }
94
95 if ( from == to || m_scrollBox == nullptr )
96 {
97 return;
98 }
99
100 m_from = from;
101 m_to = to;
102
103 const auto hint = m_scrollBox->flickHint();
104
105 setDuration( hint.duration );
106 setEasingCurve( hint.type );
107 setWindow( m_scrollBox->window() );
108
109 start();
110 }
111
112 protected:
113 void advance( qreal value ) override
114 {
115 qreal x = m_from.x() + ( m_to.x() - m_from.x() ) * value;
116 qreal y = m_from.y() + ( m_to.y() - m_from.y() ) * value;
117
118 m_scrollBox->setScrollPos( QPointF( x, y ) );
119 }
120
121 private:
122 QskScrollBox* m_scrollBox;
123
124 QPointF m_from;
125 QPointF m_to;
126 };
127
128 class PanRecognizer final : public QskPanGestureRecognizer
129 {
130 using Inherited = QskPanGestureRecognizer;
131
132 public:
133 PanRecognizer( QObject* parent = nullptr )
134 : QskPanGestureRecognizer( parent )
135 {
136 setOrientations( Qt::Horizontal | Qt::Vertical );
137 }
138
139 bool isAcceptedPos( const QPointF& pos ) const override
140 {
141 if ( auto scrollBox = qobject_cast< const QskScrollBox* >( watchedItem() ) )
142 {
143 if ( qskIsScrollable( scrollBox, orientations() ) )
144 return scrollBox->viewContentsRect().contains( pos );
145 }
146
147 return false;
148 }
149 };
150}
151
152class QskScrollBox::PrivateData
153{
154 public:
155 QPointF scrollPos;
156 QSizeF scrollableSize = QSize( 0.0, 0.0 );
157
158 PanRecognizer panRecognizer;
159
160 FlickAnimator flicker;
161 ScrollAnimator scroller;
162
163 bool autoScrollFocusItem = true;
164};
165
166QskScrollBox::QskScrollBox( QQuickItem* parent )
167 : Inherited( parent )
168 , m_data( new PrivateData() )
169{
170 m_data->flicker.setScrollBox( this );
171 m_data->scroller.setScrollBox( this );
172
173 m_data->panRecognizer.setWatchedItem( this );
174
175 setAcceptedMouseButtons( Qt::LeftButton );
176 setFiltersChildMouseEvents( true );
177
178 setWheelEnabled( true );
179 setFocusPolicy( Qt::StrongFocus );
180
181 connectWindow( window(), true );
182}
183
184QskScrollBox::~QskScrollBox()
185{
186}
187
188void QskScrollBox::setAutoScrollFocusedItem( bool on )
189{
190 if ( m_data->autoScrollFocusItem != on )
191 {
192 m_data->autoScrollFocusItem = on;
193 connectWindow( window(), true );
194 Q_EMIT autoScrollFocusedItemChanged( on );
195 }
196}
197
198bool QskScrollBox::autoScrollFocusItem() const
199{
200 return m_data->autoScrollFocusItem;
201}
202
203void QskScrollBox::onFocusItemChanged()
204{
205 if ( window() )
206 {
207#if QT_VERSION >= QT_VERSION_CHECK( 6, 1, 0 )
208 auto wd = QQuickWindowPrivate::get( window() )->deliveryAgentPrivate();
209#else
210 auto wd = QQuickWindowPrivate::get( window() );
211#endif
212 auto reason = wd->lastFocusReason;
213 if ( reason == Qt::TabFocusReason || reason == Qt::BacktabFocusReason )
214 ensureFocusItemVisible();
215 }
216}
217
218void QskScrollBox::ensureFocusItemVisible()
219{
220 if ( window() == nullptr )
221 return;
222
223 if ( const auto focusItem = window()->activeFocusItem() )
224 ensureItemVisible( focusItem );
225}
226
227void QskScrollBox::setFlickRecognizerTimeout( int timeout )
228{
229 if ( timeout < 0 )
230 timeout = -1;
231
232 m_data->panRecognizer.setTimeout( timeout );
233}
234
235int QskScrollBox::flickRecognizerTimeout() const
236{
237 return m_data->panRecognizer.timeout();
238}
239
240void QskScrollBox::setFlickableOrientations( Qt::Orientations orientations )
241{
242 if ( m_data->panRecognizer.orientations() != orientations )
243 {
244 m_data->panRecognizer.setOrientations( orientations );
245 Q_EMIT flickableOrientationsChanged();
246 }
247}
248
249Qt::Orientations QskScrollBox::flickableOrientations() const
250{
251 return m_data->panRecognizer.orientations();
252}
253
254void QskScrollBox::setScrollPos( const QPointF& pos )
255{
256 const QPointF boundedPos = boundedScrollPos( pos );
257 if ( boundedPos != m_data->scrollPos )
258 {
259 m_data->scrollPos = boundedPos;
260 update();
261
262 Q_EMIT scrollPosChanged();
263 Q_EMIT scrolledTo( boundedPos );
264 }
265}
266
267QPointF QskScrollBox::scrollPos() const
268{
269 return m_data->scrollPos;
270}
271
272void QskScrollBox::scrollTo( const QPointF& pos )
273{
274 m_data->scroller.scroll( scrollPos(), pos );
275}
276
277void QskScrollBox::setScrollableSize( const QSizeF& size )
278{
279 const QSizeF boundedSize = size.expandedTo( QSizeF( 0, 0 ) );
280
281 if ( boundedSize != m_data->scrollableSize )
282 {
283 m_data->scrollableSize = boundedSize;
284 Q_EMIT scrollableSizeChanged( m_data->scrollableSize );
285
286 setScrollPos( m_data->scrollPos ); // scroll pos might need to be re-bounded
287
288 update();
289 }
290}
291
292QSizeF QskScrollBox::scrollableSize() const
293{
294 return m_data->scrollableSize;
295}
296
297void QskScrollBox::ensureItemVisible( const QQuickItem* item )
298{
299 if ( qskIsAncestorOf( this, item ) )
300 {
301 auto pos = scrollPos() - viewContentsRect().topLeft();
302 pos += mapFromItem( item, QPointF() );
303
304 ensureVisible( QRectF( pos.x(), pos.y(), item->width(), item->height() ) );
305 }
306}
307
308void QskScrollBox::ensureVisible( const QPointF& pos )
309{
310 const qreal margin = qskViewportPadding();
311
312 QRectF r( scrollPos(), viewContentsRect().size() );
313 r.adjust( margin, margin, -margin, -margin );
314
315 qreal x = r.x();
316 qreal y = r.y();
317
318 if ( pos.x() < r.left() )
319 {
320 x = pos.x();
321 }
322 else if ( pos.x() > r.right() )
323 {
324 x = pos.x() - r.width();
325 }
326
327 if ( pos.y() < r.top() )
328 {
329 y = pos.y();
330 }
331 else if ( y > r.right() )
332 {
333 y = pos.y() - r.height();
334 }
335
336 const QPoint newPos( x - margin, y - margin );
337
338 if( isInitiallyPainted() && window() )
339 scrollTo( newPos );
340 else
341 setScrollPos( newPos );
342}
343
344void QskScrollBox::ensureVisible( const QRectF& itemRect )
345{
346 const qreal margin = qskViewportPadding();
347
348 QRectF r( scrollPos(), viewContentsRect().size() );
349 r.adjust( margin, margin, -margin, -margin );
350
351 qreal x = r.x();
352 qreal y = r.y();
353
354 if ( itemRect.width() > r.width() )
355 {
356 x = itemRect.left() + 0.5 * ( itemRect.width() - r.width() );
357 }
358 else if ( itemRect.right() > r.right() )
359 {
360 x = itemRect.right() - r.width();
361 }
362 else if ( itemRect.left() < r.left() )
363 {
364 x = itemRect.left();
365 }
366
367 if ( itemRect.height() > r.height() )
368 {
369 y = itemRect.top() + 0.5 * ( itemRect.height() - r.height() );
370 }
371 else if ( itemRect.bottom() > r.bottom() )
372 {
373 y = itemRect.bottom() - r.height();
374 }
375 else if ( itemRect.top() < r.top() )
376 {
377 y = itemRect.top();
378 }
379
380 const QPoint newPos( x - margin, y - margin );
381
382 if( isInitiallyPainted() && window() )
383 scrollTo( newPos );
384 else
385 setScrollPos( newPos );
386}
387
389{
391
392 connectWindow( event->oldWindow(), false );
393 connectWindow( event->window(), true );
394}
395
397{
398 if ( event->isResized() )
399 setScrollPos( scrollPos() );
400
402}
403
404void QskScrollBox::gestureEvent( QskGestureEvent* event )
405{
406 if ( event->gesture()->type() == QskGesture::Pan )
407 {
408 const auto gesture = static_cast< const QskPanGesture* >( event->gesture().get() );
409
410 switch ( gesture->state() )
411 {
412 case QskGesture::Updated:
413 {
414 setScrollPos( scrollPos() - gesture->delta() );
415 break;
416 }
417 case QskGesture::Finished:
418 {
419 m_data->flicker.setWindow( window() );
420 m_data->flicker.accelerate( gesture->angle(), gesture->velocity() );
421 break;
422 }
423 case QskGesture::Canceled:
424 {
425 // what to do here: maybe going back to the origin of the gesture ??
426 break;
427 }
428 default:
429 break;
430 }
431
432 return;
433 }
434
435 Inherited::gestureEvent( event );
436}
437
438#ifndef QT_NO_WHEELEVENT
439
440QPointF QskScrollBox::scrollOffset( const QWheelEvent* event ) const
441{
442 QPointF offset;
443
444 const auto pos = qskWheelPosition( event );
445 const auto viewRect = viewContentsRect();
446
447 if ( viewRect.contains( pos ) )
448 {
449 offset = event->pixelDelta();
450 if ( offset.isNull() )
451 offset = event->angleDelta() / QWheelEvent::DefaultDeltasPerStep;
452
453 offset.rx() *= viewRect.width();
454 offset.ry() *= viewRect.height();
455 }
456
457 return offset;
458}
459
460void QskScrollBox::wheelEvent( QWheelEvent* event )
461{
462 const auto offset = scrollOffset( event );
463 if ( !offset.isNull() )
464 setScrollPos( m_data->scrollPos - offset );
465}
466
467#endif
468
469QPointF QskScrollBox::boundedScrollPos( const QPointF& pos ) const
470{
471 const QRectF vr = viewContentsRect();
472
473 const qreal maxX = qMax( 0.0, scrollableSize().width() - vr.width() );
474 const qreal maxY = qMax( 0.0, scrollableSize().height() - vr.height() );
475
476 return QPointF( qBound( 0.0, pos.x(), maxX ), qBound( 0.0, pos.y(), maxY ) );
477}
478
479void QskScrollBox::connectWindow( const QQuickWindow* window, bool on )
480{
481 if ( ( window == nullptr ) || ( on && !autoScrollFocusItem() ) )
482 return;
483
484 if ( on )
485 {
486 QObject::connect( window, &QQuickWindow::activeFocusItemChanged,
487 this, &QskScrollBox::onFocusItemChanged, Qt::UniqueConnection );
488 }
489 else
490 {
491 QObject::disconnect( window, &QQuickWindow::activeFocusItemChanged,
492 this, &QskScrollBox::onFocusItemChanged );
493 }
494}
495
496#include "moc_QskScrollBox.cpp"
virtual void geometryChangeEvent(QskGeometryChangeEvent *)
Definition QskItem.cpp:855
bool isInitiallyPainted() const
Definition QskItem.cpp:578
virtual void windowChangeEvent(QskWindowChangeEvent *)
Definition QskItem.cpp:851
void windowChangeEvent(QskWindowChangeEvent *) override
void geometryChangeEvent(QskGeometryChangeEvent *) override