qskinny/src/controls/QskListView.cpp

506 lines
11 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskListView.h"
#include "QskAspect.h"
#include "QskColorFilter.h"
#include "QskEvent.h"
#include "QskSkinlet.h"
#include <qguiapplication.h>
#include <qstylehints.h>
#include <qmath.h>
QSK_SUBCONTROL( QskListView, Cell )
QSK_SUBCONTROL( QskListView, Text )
QSK_SUBCONTROL( QskListView, Graphic )
QSK_STATE( QskListView, Selected, QskAspect::FirstUserState )
#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;
}
class QskListView::PrivateData
{
public:
PrivateData()
: preferredWidthFromColumns( false )
, selectionMode( QskListView::SingleSelection )
{
}
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 )
{
using Q = QskListView;
listView->startHintTransitions(
{ Q::Cell, Q::Text }, oldStates, newStates, row );
}
public:
/*
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.
*/
bool preferredWidthFromColumns : 1;
SelectionMode selectionMode : 4;
int hoveredRow = -1;
int pressedRow = -1;
int selectedRow = -1;
};
QskListView::QskListView( QQuickItem* parent )
: QskScrollView( parent )
, m_data( new PrivateData() )
{
#if FOCUS_ON_CURRENT
connect( this, &QskScrollView::scrollPosChanged, &QskControl::focusIndicatorRectChanged );
#endif
}
QskListView::~QskListView()
{
}
void QskListView::setPreferredWidthFromColumns( bool on )
{
if ( on != m_data->preferredWidthFromColumns )
{
m_data->preferredWidthFromColumns = on;
resetImplicitSize();
Q_EMIT preferredWidthFromColumnsChanged();
}
}
bool QskListView::preferredWidthFromColumns() const
{
return m_data->preferredWidthFromColumns;
}
void QskListView::setTextOptions( const QskTextOptions& textOptions )
{
if ( setTextOptionsHint( Text, textOptions ) )
{
updateScrollableSize();
Q_EMIT textOptionsChanged();
}
}
QskTextOptions QskListView::textOptions() const
{
return textOptionsHint( Text );
}
void QskListView::resetTextOptions()
{
if ( resetTextOptionsHint( Text ) )
{
updateScrollableSize();
Q_EMIT textOptionsChanged();
}
}
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 );
Q_EMIT selectedRowChanged( row );
Q_EMIT focusIndicatorRectChanged();
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;
}
QRectF QskListView::focusIndicatorRect() const
{
#if FOCUS_ON_CURRENT
if( m_data->selectedRow >= 0 )
{
auto rect = effectiveSkinlet()->sampleRect(
this, contentsRect(), Cell, m_data->selectedRow );
rect = rect.translated( -scrollPos() );
if ( rect.intersects( viewContentsRect() ) )
return rect;
}
#endif
return Inherited::focusIndicatorRect();
}
void QskListView::keyPressEvent( QKeyEvent* event )
{
if ( m_data->selectionMode == NoSelection )
{
Inherited::keyPressEvent( event );
return;
}
int row = selectedRow();
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 ...
Inherited::keyPressEvent( event );
return;
}
default:
{
Inherited::keyPressEvent( event );
return;
}
}
const int r = selectedRow();
setSelectedRow( row );
row = selectedRow();
if ( row != r )
{
auto pos = scrollPos();
const qreal rowPos = row * rowHeight();
if ( rowPos < scrollPos().y() )
{
pos.setY( rowPos );
}
else
{
const QRectF vr = viewContentsRect();
const double scrolledBottom = scrollPos().y() + vr.height();
if ( rowPos + rowHeight() > scrolledBottom )
{
const double y = rowPos + rowHeight() - vr.height();
pos.setY( y );
}
}
if ( pos != scrollPos() )
{
if ( event->isAutoRepeat() )
setScrollPos( pos );
else
scrollTo( pos );
}
}
}
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 )
{
m_data->setRowState( this, row, Pressed );
setSelectedRow( row );
return;
}
}
Inherited::mousePressEvent( event );
}
void QskListView::mouseReleaseEvent( QMouseEvent* event )
{
m_data->setRowState( this, -1, Pressed );
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;
}
#ifndef QT_NO_WHEELEVENT
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;
}
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 )
{
const qreal y0 = scrollPos().y();
const auto viewHeight = viewContentsRect().height();
const qreal rowHeight = this->rowHeight();
const int numLines = QGuiApplication::styleHints()->wheelScrollLines();
qreal dy = numLines * rowHeight;
if ( event->modifiers() & ( Qt::ControlModifier | Qt::ShiftModifier ) )
dy = qMax( dy, viewHeight );
dy *= offset.y(); // multiplied by the wheelsteps
// aligning rows that enter the view
dy = qskAlignedToRows( y0, dy, rowHeight, viewHeight );
offset.setY( y0 - dy );
}
// TODO using the animated scrollTo instead ?
return offset;
}
#endif
void QskListView::updateScrollableSize()
{
const double h = rowCount() * rowHeight();
qreal w = 0.0;
for ( int col = 0; col < columnCount(); col++ )
w += columnWidth( col );
const QSizeF sz = scrollableSize();
setScrollableSize( QSizeF( w, h ) );
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"