qskinny/src/controls/QskListView.cpp

511 lines
12 KiB
C++
Raw Normal View History

2017-07-21 18:21:34 +02:00
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
2023-04-06 09:23:37 +02:00
* SPDX-License-Identifier: BSD-3-Clause
2017-07-21 18:21:34 +02:00
*****************************************************************************/
#include "QskListView.h"
#include "QskAspect.h"
2018-08-03 08:15:28 +02:00
#include "QskColorFilter.h"
2022-01-12 13:29:42 +01:00
#include "QskEvent.h"
2023-07-18 16:18:36 +02:00
#include "QskSkinlet.h"
2022-01-12 13:29:42 +01:00
#include <qguiapplication.h>
#include <qstylehints.h>
#include <qmath.h>
2017-07-21 18:21:34 +02:00
QSK_SUBCONTROL( QskListView, Cell )
QSK_SUBCONTROL( QskListView, Text )
QSK_SUBCONTROL( QskListView, Graphic )
2021-12-22 15:08:27 +01:00
QSK_STATE( QskListView, Selected, QskAspect::FirstUserState )
2017-07-21 18:21:34 +02:00
#define FOCUS_ON_CURRENT 1
static inline int qskRowAt( const QskListView* listView, const QPointF& pos )
{
const auto rect = listView->viewContentsRect();
if ( rect.contains( pos ) )
{
const auto y = pos.y() - rect.top() + listView->scrollPos().y();
const int row = y / listView->rowHeight();
if ( row >= 0 && row < listView->rowCount() )
return row;
}
return -1;
}
2017-07-21 18:21:34 +02:00
class QskListView::PrivateData
{
2018-08-03 08:15:28 +02:00
public:
PrivateData()
: preferredWidthFromColumns( false )
, selectionMode( QskListView::SingleSelection )
2017-07-21 18:21:34 +02:00
{
}
void setRowState( QskListView* listView, int row, QskAspect::State state )
{
using Q = QskListView;
auto& storedRow = ( state == Q::Hovered )
? hoveredRow : ( ( state == Q::Pressed ) ? pressedRow : selectedRow );
if ( row == storedRow )
return;
if ( storedRow >= 0 )
{
const auto states = listView->rowStates( storedRow );
startTransitions( listView, storedRow, states, states & ~state );
}
if ( row >= 0 )
{
const auto states = listView->rowStates( row );
startTransitions( listView, row, states, states | state );
}
storedRow = row;
listView->update();
}
private:
inline void startTransitions( QskListView* listView, int row,
QskAspect::States oldStates, QskAspect::States newStates )
{
/*
working implementation can be found in
https://github.com/uwerat/qskinny/tree/features/listview
*/
2023-08-09 10:40:01 +02:00
Q_UNUSED( listView );
Q_UNUSED( row );
Q_UNUSED( oldStates );
Q_UNUSED( newStates );
}
public:
2023-07-18 16:18:36 +02:00
/*
Currently we only support single selection. We can't navigate
the current item ( = focus ) without changing the selection.
So for the moment the selected row is always the currentRow.
*/
2017-07-21 18:21:34 +02:00
bool preferredWidthFromColumns : 1;
SelectionMode selectionMode : 4;
int hoveredRow = -1;
int pressedRow = -1;
2023-07-18 16:18:36 +02:00
int selectedRow = -1;
2017-07-21 18:21:34 +02:00
};
2018-08-03 08:15:28 +02:00
QskListView::QskListView( QQuickItem* parent )
: QskScrollView( parent )
, m_data( new PrivateData() )
2017-07-21 18:21:34 +02:00
{
#if FOCUS_ON_CURRENT
2023-07-18 16:18:36 +02:00
connect( this, &QskScrollView::scrollPosChanged, &QskControl::focusIndicatorRectChanged );
#endif
2017-07-21 18:21:34 +02:00
}
QskListView::~QskListView()
{
}
void QskListView::setPreferredWidthFromColumns( bool on )
{
if ( on != m_data->preferredWidthFromColumns )
{
m_data->preferredWidthFromColumns = on;
resetImplicitSize();
2017-10-30 08:33:43 +01:00
Q_EMIT preferredWidthFromColumnsChanged();
2017-07-21 18:21:34 +02:00
}
}
bool QskListView::preferredWidthFromColumns() const
{
return m_data->preferredWidthFromColumns;
}
2017-10-20 13:31:55 +02:00
void QskListView::setTextOptions( const QskTextOptions& textOptions )
2017-07-21 18:21:34 +02:00
{
2022-08-25 09:39:33 +02:00
if ( setTextOptionsHint( Text, textOptions ) )
2017-07-21 18:21:34 +02:00
{
updateScrollableSize();
Q_EMIT textOptionsChanged();
}
}
QskTextOptions QskListView::textOptions() const
{
2022-08-25 09:39:33 +02:00
return textOptionsHint( Text );
2017-07-21 18:21:34 +02:00
}
2023-06-30 14:38:48 +02:00
void QskListView::resetTextOptions()
{
if ( resetTextOptionsHint( Text ) )
{
updateScrollableSize();
Q_EMIT textOptionsChanged();
}
}
2017-07-21 18:21:34 +02:00
void QskListView::setSelectedRow( int row )
{
if ( row < 0 )
row = -1;
if ( row >= rowCount() )
{
if ( !isComponentComplete() )
{
// when being called from Qml we delay the checks until
// componentComplete
}
else
{
if ( row >= rowCount() )
row = -1;
}
}
if ( row != m_data->selectedRow )
{
m_data->setRowState( this, row, Selected );
2017-07-21 18:21:34 +02:00
Q_EMIT selectedRowChanged( row );
2023-07-18 16:18:36 +02:00
Q_EMIT focusIndicatorRectChanged();
2017-07-21 18:21:34 +02:00
update();
}
}
int QskListView::selectedRow() const
{
return m_data->selectedRow;
}
void QskListView::setSelectionMode( SelectionMode mode )
{
if ( mode != m_data->selectionMode )
{
m_data->selectionMode = mode;
if ( m_data->selectionMode == NoSelection )
setSelectedRow( -1 );
Q_EMIT selectionModeChanged();
}
}
QskListView::SelectionMode QskListView::selectionMode() const
{
return m_data->selectionMode;
}
2023-07-18 16:18:36 +02:00
QRectF QskListView::focusIndicatorRect() const
{
#if FOCUS_ON_CURRENT
2023-07-18 16:18:36 +02:00
if( m_data->selectedRow >= 0 )
{
auto rect = effectiveSkinlet()->sampleRect(
2023-07-18 16:18:36 +02:00
this, contentsRect(), Cell, m_data->selectedRow );
rect = rect.translated( -scrollPos() );
if ( rect.intersects( viewContentsRect() ) )
return rect;
2023-07-18 16:18:36 +02:00
}
#endif
2023-07-18 16:18:36 +02:00
return Inherited::focusIndicatorRect();
}
2017-07-21 18:21:34 +02:00
void QskListView::keyPressEvent( QKeyEvent* event )
{
if ( m_data->selectionMode == NoSelection )
2017-11-21 09:05:09 +01:00
{
Inherited::keyPressEvent( event );
2017-07-21 18:21:34 +02:00
return;
2017-11-21 09:05:09 +01:00
}
2017-07-21 18:21:34 +02:00
int row = selectedRow();
2017-10-18 20:00:06 +02:00
2017-07-21 18:21:34 +02:00
switch ( event->key() )
{
case Qt::Key_Down:
{
if ( row < rowCount() - 1 )
row++;
break;
}
case Qt::Key_Up:
{
if ( row == -1 )
row = rowCount() - 1;
if ( row != 0 )
row--;
break;
}
case Qt::Key_Home:
{
row = 0;
break;
}
case Qt::Key_End:
{
row = rowCount() - 1;
break;
}
case Qt::Key_PageUp:
case Qt::Key_PageDown:
{
// TODO ...
2022-03-24 17:10:11 +01:00
Inherited::keyPressEvent( event );
return;
2017-07-21 18:21:34 +02:00
}
default:
{
Inherited::keyPressEvent( event );
return;
2017-07-21 18:21:34 +02:00
}
}
const int r = selectedRow();
setSelectedRow( row );
row = selectedRow();
if ( row != r )
{
2018-01-16 12:13:38 +01:00
auto pos = scrollPos();
2017-12-07 17:12:52 +01:00
const qreal rowPos = row * rowHeight();
2017-07-21 18:21:34 +02:00
if ( rowPos < scrollPos().y() )
{
2018-01-16 12:13:38 +01:00
pos.setY( rowPos );
2017-07-21 18:21:34 +02:00
}
else
{
const QRectF vr = viewContentsRect();
const double scrolledBottom = scrollPos().y() + vr.height();
if ( rowPos + rowHeight() > scrolledBottom )
{
const double y = rowPos + rowHeight() - vr.height();
2018-01-16 12:13:38 +01:00
pos.setY( y );
2017-07-21 18:21:34 +02:00
}
}
2018-01-16 12:13:38 +01:00
if ( pos != scrollPos() )
{
if ( event->isAutoRepeat() )
setScrollPos( pos );
else
scrollTo( pos );
}
2017-07-21 18:21:34 +02:00
}
}
void QskListView::keyReleaseEvent( QKeyEvent* event )
{
Inherited::keyReleaseEvent( event );
}
void QskListView::mousePressEvent( QMouseEvent* event )
{
if ( m_data->selectionMode != NoSelection )
{
const int row = qskRowAt( this, qskMousePosition( event ) );
if ( row >= 0 )
2017-07-21 18:21:34 +02:00
{
m_data->setRowState( this, row, Pressed );
setSelectedRow( row );
2017-10-25 17:10:50 +02:00
return;
2017-07-21 18:21:34 +02:00
}
}
2017-10-25 17:10:50 +02:00
Inherited::mousePressEvent( event );
2017-07-21 18:21:34 +02:00
}
void QskListView::mouseReleaseEvent( QMouseEvent* event )
{
m_data->setRowState( this, -1, Pressed );
2017-07-21 18:21:34 +02:00
Inherited::mouseReleaseEvent( event );
}
void QskListView::mouseUngrabEvent()
{
m_data->setRowState( this, -1, Pressed );
Inherited::mouseUngrabEvent();
}
void QskListView::hoverEnterEvent( QHoverEvent* event )
{
if ( m_data->selectionMode != NoSelection )
{
const int row = qskRowAt( this, qskHoverPosition( event ) );
m_data->setRowState( this, row, Hovered );
}
Inherited::hoverEnterEvent( event );
}
void QskListView::hoverMoveEvent( QHoverEvent* event )
{
if ( m_data->selectionMode != NoSelection )
{
const int row = qskRowAt( this, qskHoverPosition( event ) );
m_data->setRowState( this, row, Hovered );
}
Inherited::hoverMoveEvent( event );
}
void QskListView::hoverLeaveEvent( QHoverEvent* event )
{
m_data->setRowState( this, -1, Hovered );
Inherited::hoverLeaveEvent( event );
}
void QskListView::changeEvent( QEvent* event )
{
if ( event->type() == QEvent::StyleChange )
updateScrollableSize();
Inherited::changeEvent( event );
}
QskAspect::States QskListView::rowStates( int row ) const
{
auto states = skinStates();
if ( row >= 0 )
{
if ( row == m_data->selectedRow )
states |= Selected;
if ( row == m_data->hoveredRow )
states |= Hovered;
if ( row == m_data->pressedRow )
states |= Pressed;
}
return states;
}
2022-01-12 13:29:42 +01:00
#ifndef QT_NO_WHEELEVENT
2022-01-17 09:46:27 +01:00
static qreal qskAlignedToRows( const qreal y0, qreal dy,
qreal rowHeight, qreal viewHeight )
{
qreal y = y0 - dy;
if ( dy > 0 )
{
y = qFloor( y / rowHeight ) * rowHeight;
}
else
{
y += viewHeight;
y = qCeil( y / rowHeight ) * rowHeight;
y -= viewHeight;
}
return y;
}
2022-01-12 13:29:42 +01:00
QPointF QskListView::scrollOffset( const QWheelEvent* event ) const
{
QPointF offset;
const auto pos = qskWheelPosition( event );
if ( subControlRect( VerticalScrollBar ).contains( pos ) )
{
const auto steps = qskWheelSteps( event );
offset.setY( steps );
}
else if ( subControlRect( HorizontalScrollBar ).contains( pos ) )
{
const auto steps = qskWheelSteps( event );
offset.setX( steps );
}
else if ( viewContentsRect().contains( pos ) )
{
offset = event->angleDelta() / QWheelEvent::DefaultDeltasPerStep;
}
if ( offset.x() != 0.0 )
{
offset.rx() *= viewContentsRect().width(); // pagewise
}
else if ( offset.y() != 0.0 )
{
2022-01-17 09:46:27 +01:00
const qreal y0 = scrollPos().y();
2022-01-12 13:29:42 +01:00
const auto viewHeight = viewContentsRect().height();
const qreal rowHeight = this->rowHeight();
2022-03-08 11:53:46 +01:00
const int numLines = QGuiApplication::styleHints()->wheelScrollLines();
2022-01-12 13:29:42 +01:00
qreal dy = numLines * rowHeight;
if ( event->modifiers() & ( Qt::ControlModifier | Qt::ShiftModifier ) )
dy = qMax( dy, viewHeight );
2022-01-17 09:46:27 +01:00
dy *= offset.y(); // multiplied by the wheelsteps
// aligning rows that enter the view
dy = qskAlignedToRows( y0, dy, rowHeight, viewHeight );
offset.setY( y0 - dy );
2022-01-12 13:29:42 +01:00
}
2022-01-17 09:46:27 +01:00
// TODO using the animated scrollTo instead ?
2022-01-12 13:29:42 +01:00
return offset;
}
#endif
2017-07-21 18:21:34 +02:00
void QskListView::updateScrollableSize()
{
const double h = rowCount() * rowHeight();
2017-12-07 17:12:52 +01:00
qreal w = 0.0;
2017-07-21 18:21:34 +02:00
for ( int col = 0; col < columnCount(); col++ )
w += columnWidth( col );
const QSizeF sz = scrollableSize();
2017-12-07 17:12:52 +01:00
setScrollableSize( QSizeF( w, h ) );
2017-07-21 18:21:34 +02:00
if ( m_data->preferredWidthFromColumns &&
sz.width() != scrollableSize().width() )
{
resetImplicitSize();
}
}
void QskListView::componentComplete()
{
Inherited::componentComplete();
if ( m_data->selectedRow >= 0 )
{
// during Qml instantiation we might have set an invalid
// row selection
if ( m_data->selectedRow >= rowCount() )
setSelectedRow( -1 );
}
}
#include "moc_QskListView.cpp"