QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskListView.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskListView.h"
7#include "QskAspect.h"
8#include "QskColorFilter.h"
9#include "QskEvent.h"
10#include "QskSkinlet.h"
11
12#include <qguiapplication.h>
13#include <qstylehints.h>
14
15#include <qmath.h>
16
17QSK_SUBCONTROL( QskListView, Cell )
18QSK_SUBCONTROL( QskListView, Text )
19QSK_SUBCONTROL( QskListView, Graphic )
20
21QSK_STATE( QskListView, Selected, QskAspect::FirstUserState )
22
23#define FOCUS_ON_CURRENT 1
24
25static inline int qskRowAt( const QskListView* listView, const QPointF& pos )
26{
27 const auto rect = listView->viewContentsRect();
28 if ( rect.contains( pos ) )
29 {
30 const auto y = pos.y() - rect.top() + listView->scrollPos().y();
31
32 const int row = y / listView->rowHeight();
33 if ( row >= 0 && row < listView->rowCount() )
34 return row;
35 }
36
37 return -1;
38}
39
40class QskListView::PrivateData
41{
42 public:
43 PrivateData()
44 : preferredWidthFromColumns( false )
45 , selectionMode( QskListView::SingleSelection )
46 {
47 }
48
49 void setRowState( QskListView* listView, int row, QskAspect::State state )
50 {
51 using Q = QskListView;
52
53 auto& storedRow = ( state == Q::Hovered )
54 ? hoveredRow : ( ( state == Q::Pressed ) ? pressedRow : selectedRow );
55
56 if ( row == storedRow )
57 return;
58
59 if ( storedRow >= 0 )
60 {
61 const auto states = listView->rowStates( storedRow );
62 startTransitions( listView, storedRow, states, states & ~state );
63 }
64
65 if ( row >= 0 )
66 {
67 const auto states = listView->rowStates( row );
68 startTransitions( listView, row, states, states | state );
69 }
70
71 storedRow = row;
72 listView->update();
73 }
74
75 private:
76 inline void startTransitions( QskListView* listView, int row,
77 QskAspect::States oldStates, QskAspect::States newStates )
78 {
79 using Q = QskListView;
80
81 listView->startHintTransitions(
82 { Q::Cell, Q::Text }, oldStates, newStates, row );
83 }
84
85 public:
86 /*
87 Currently we only support single selection. We can't navigate
88 the current item ( = focus ) without changing the selection.
89 So for the moment the selected row is always the currentRow.
90 */
91
92 bool preferredWidthFromColumns : 1;
93 SelectionMode selectionMode : 4;
94
95 int hoveredRow = -1;
96 int pressedRow = -1;
97 int selectedRow = -1;
98};
99
100QskListView::QskListView( QQuickItem* parent )
101 : QskScrollView( parent )
102 , m_data( new PrivateData() )
103{
104#if FOCUS_ON_CURRENT
105 connect( this, &QskScrollView::scrollPosChanged, &QskControl::focusIndicatorRectChanged );
106#endif
107}
108
109QskListView::~QskListView()
110{
111}
112
113void QskListView::setPreferredWidthFromColumns( bool on )
114{
115 if ( on != m_data->preferredWidthFromColumns )
116 {
117 m_data->preferredWidthFromColumns = on;
119
120 Q_EMIT preferredWidthFromColumnsChanged();
121 }
122}
123
124bool QskListView::preferredWidthFromColumns() const
125{
126 return m_data->preferredWidthFromColumns;
127}
128
129void QskListView::setTextOptions( const QskTextOptions& textOptions )
130{
131 if ( setTextOptionsHint( Text, textOptions ) )
132 {
133 updateScrollableSize();
134 Q_EMIT textOptionsChanged();
135 }
136}
137
138QskTextOptions QskListView::textOptions() const
139{
140 return textOptionsHint( Text );
141}
142
143void QskListView::resetTextOptions()
144{
145 if ( resetTextOptionsHint( Text ) )
146 {
147 updateScrollableSize();
148 Q_EMIT textOptionsChanged();
149 }
150}
151
152void QskListView::setSelectedRow( int row )
153{
154 if ( row < 0 )
155 row = -1;
156
157 if ( row >= rowCount() )
158 {
159 if ( !isComponentComplete() )
160 {
161 // when being called from Qml we delay the checks until
162 // componentComplete
163 }
164 else
165 {
166 if ( row >= rowCount() )
167 row = -1;
168 }
169 }
170
171 if ( row != m_data->selectedRow )
172 {
173 m_data->setRowState( this, row, Selected );
174
175 Q_EMIT selectedRowChanged( row );
177
178 update();
179 }
180}
181
182int QskListView::selectedRow() const
183{
184 return m_data->selectedRow;
185}
186
187void QskListView::setSelectionMode( SelectionMode mode )
188{
189 if ( mode != m_data->selectionMode )
190 {
191 m_data->selectionMode = mode;
192
193 if ( m_data->selectionMode == NoSelection )
194 setSelectedRow( -1 );
195
196 Q_EMIT selectionModeChanged();
197 }
198}
199
200QskListView::SelectionMode QskListView::selectionMode() const
201{
202 return m_data->selectionMode;
203}
204
206{
207#if FOCUS_ON_CURRENT
208 if( m_data->selectedRow >= 0 )
209 {
210 auto rect = effectiveSkinlet()->sampleRect(
211 this, contentsRect(), Cell, m_data->selectedRow );
212
213 rect = rect.translated( -scrollPos() );
214 if ( rect.intersects( viewContentsRect() ) )
215 return rect;
216 }
217#endif
218
220}
221
222void QskListView::keyPressEvent( QKeyEvent* event )
223{
224 if ( m_data->selectionMode == NoSelection )
225 {
226 Inherited::keyPressEvent( event );
227 return;
228 }
229
230 int row = selectedRow();
231
232 switch ( event->key() )
233 {
234 case Qt::Key_Down:
235 {
236 if ( row < rowCount() - 1 )
237 row++;
238 break;
239 }
240 case Qt::Key_Up:
241 {
242 if ( row == -1 )
243 row = rowCount() - 1;
244
245 if ( row != 0 )
246 row--;
247
248 break;
249 }
250 case Qt::Key_Home:
251 {
252 row = 0;
253 break;
254 }
255 case Qt::Key_End:
256 {
257 row = rowCount() - 1;
258 break;
259 }
260 case Qt::Key_PageUp:
261 case Qt::Key_PageDown:
262 {
263 // TODO ...
264 Inherited::keyPressEvent( event );
265 return;
266 }
267 default:
268 {
269 Inherited::keyPressEvent( event );
270 return;
271 }
272 }
273
274 const int r = selectedRow();
275
276 setSelectedRow( row );
277
278 row = selectedRow();
279
280 if ( row != r )
281 {
282 auto pos = scrollPos();
283
284 const qreal rowPos = row * rowHeight();
285 if ( rowPos < scrollPos().y() )
286 {
287 pos.setY( rowPos );
288 }
289 else
290 {
291 const QRectF vr = viewContentsRect();
292
293 const double scrolledBottom = scrollPos().y() + vr.height();
294 if ( rowPos + rowHeight() > scrolledBottom )
295 {
296 const double y = rowPos + rowHeight() - vr.height();
297 pos.setY( y );
298 }
299 }
300
301 if ( pos != scrollPos() )
302 {
303 if ( event->isAutoRepeat() )
304 setScrollPos( pos );
305 else
306 scrollTo( pos );
307 }
308 }
309}
310
311void QskListView::keyReleaseEvent( QKeyEvent* event )
312{
313 Inherited::keyReleaseEvent( event );
314}
315
316void QskListView::mousePressEvent( QMouseEvent* event )
317{
318 if ( m_data->selectionMode != NoSelection )
319 {
320 const int row = qskRowAt( this, qskMousePosition( event ) );
321 if ( row >= 0 )
322 {
323 m_data->setRowState( this, row, Pressed );
324 setSelectedRow( row );
325 return;
326 }
327 }
328
329 Inherited::mousePressEvent( event );
330}
331
332void QskListView::mouseReleaseEvent( QMouseEvent* event )
333{
334 m_data->setRowState( this, -1, Pressed );
335 Inherited::mouseReleaseEvent( event );
336}
337
338void QskListView::mouseUngrabEvent()
339{
340 m_data->setRowState( this, -1, Pressed );
341 Inherited::mouseUngrabEvent();
342}
343
344void QskListView::hoverEnterEvent( QHoverEvent* event )
345{
346 if ( m_data->selectionMode != NoSelection )
347 {
348 const int row = qskRowAt( this, qskHoverPosition( event ) );
349 m_data->setRowState( this, row, Hovered );
350 }
351
352 Inherited::hoverEnterEvent( event );
353}
354
355void QskListView::hoverMoveEvent( QHoverEvent* event )
356{
357 if ( m_data->selectionMode != NoSelection )
358 {
359 const int row = qskRowAt( this, qskHoverPosition( event ) );
360 m_data->setRowState( this, row, Hovered );
361 }
362
363 Inherited::hoverMoveEvent( event );
364}
365
366void QskListView::hoverLeaveEvent( QHoverEvent* event )
367{
368 m_data->setRowState( this, -1, Hovered );
369 Inherited::hoverLeaveEvent( event );
370}
371
372void QskListView::changeEvent( QEvent* event )
373{
374 if ( event->type() == QEvent::StyleChange )
375 updateScrollableSize();
376
377 Inherited::changeEvent( event );
378}
379
380QskAspect::States QskListView::rowStates( int row ) const
381{
382 auto states = skinStates();
383
384 if ( row >= 0 )
385 {
386 if ( row == m_data->selectedRow )
387 states |= Selected;
388
389 if ( row == m_data->hoveredRow )
390 states |= Hovered;
391
392 if ( row == m_data->pressedRow )
393 states |= Pressed;
394 }
395
396 return states;
397}
398
399#ifndef QT_NO_WHEELEVENT
400
401static qreal qskAlignedToRows( const qreal y0, qreal dy,
402 qreal rowHeight, qreal viewHeight )
403{
404 qreal y = y0 - dy;
405
406 if ( dy > 0 )
407 {
408 y = qFloor( y / rowHeight ) * rowHeight;
409 }
410 else
411 {
412 y += viewHeight;
413 y = qCeil( y / rowHeight ) * rowHeight;
414 y -= viewHeight;
415 }
416
417 return y;
418}
419
420QPointF QskListView::scrollOffset( const QWheelEvent* event ) const
421{
422 QPointF offset;
423
424 const auto pos = qskWheelPosition( event );
425
426 if ( subControlRect( VerticalScrollBar ).contains( pos ) )
427 {
428 const auto steps = qskWheelSteps( event );
429 offset.setY( steps );
430 }
431 else if ( subControlRect( HorizontalScrollBar ).contains( pos ) )
432 {
433 const auto steps = qskWheelSteps( event );
434 offset.setX( steps );
435 }
436 else if ( viewContentsRect().contains( pos ) )
437 {
438 offset = event->angleDelta() / QWheelEvent::DefaultDeltasPerStep;
439 }
440
441 if ( offset.x() != 0.0 )
442 {
443 offset.rx() *= viewContentsRect().width(); // pagewise
444 }
445 else if ( offset.y() != 0.0 )
446 {
447 const qreal y0 = scrollPos().y();
448 const auto viewHeight = viewContentsRect().height();
449 const qreal rowHeight = this->rowHeight();
450
451 const int numLines = QGuiApplication::styleHints()->wheelScrollLines();
452
453 qreal dy = numLines * rowHeight;
454 if ( event->modifiers() & ( Qt::ControlModifier | Qt::ShiftModifier ) )
455 dy = qMax( dy, viewHeight );
456
457 dy *= offset.y(); // multiplied by the wheelsteps
458
459 // aligning rows that enter the view
460 dy = qskAlignedToRows( y0, dy, rowHeight, viewHeight );
461
462 offset.setY( y0 - dy );
463 }
464
465 // TODO using the animated scrollTo instead ?
466
467 return offset;
468}
469
470#endif
471
472void QskListView::updateScrollableSize()
473{
474 const double h = rowCount() * rowHeight();
475
476 qreal w = 0.0;
477 for ( int col = 0; col < columnCount(); col++ )
478 w += columnWidth( col );
479
480 const QSizeF sz = scrollableSize();
481
482 setScrollableSize( QSizeF( w, h ) );
483
484 if ( m_data->preferredWidthFromColumns &&
485 sz.width() != scrollableSize().width() )
486 {
488 }
489}
490
491void QskListView::componentComplete()
492{
494
495 if ( m_data->selectedRow >= 0 )
496 {
497 // during Qml instantiation we might have set an invalid
498 // row selection
499
500 if ( m_data->selectedRow >= rowCount() )
501 setSelectedRow( -1 );
502 }
503}
504
505#include "moc_QskListView.cpp"
@ FirstUserState
Definition QskAspect.h:116
QRectF subControlRect(QskAspect::Subcontrol) const
void focusIndicatorRectChanged()
static const QskAspect::State Hovered
Definition QskControl.h:56
virtual QRectF focusIndicatorRect() const
QRectF contentsRect() const
QRectF rect
Definition QskItem.h:21
void resetImplicitSize()
Definition QskItem.cpp:721
virtual void changeEvent(QEvent *)
Definition QskItem.cpp:859
void componentComplete() override
Definition QskItem.cpp:272
QRectF focusIndicatorRect() const override
void changeEvent(QEvent *) override
const QskSkinlet * effectiveSkinlet() const