506 lines
11 KiB
C++
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"
|