499 lines
15 KiB
C++
499 lines
15 KiB
C++
/******************************************************************************
|
|
* QSkinny - Copyright (C) 2016 Uwe Rathmann
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*****************************************************************************/
|
|
|
|
#include "QskListViewSkinlet.h"
|
|
#include "QskListView.h"
|
|
|
|
#include "QskColorFilter.h"
|
|
#include "QskGraphic.h"
|
|
#include "QskBoxHints.h"
|
|
#include "QskSGNode.h"
|
|
#include "QskSkinStateChanger.h"
|
|
#include "QskQuick.h"
|
|
|
|
#include <qmath.h>
|
|
#include <qsgnode.h>
|
|
#include <qtransform.h>
|
|
|
|
namespace
|
|
{
|
|
class ForegroundNode : public QSGNode
|
|
{
|
|
public:
|
|
void invalidate()
|
|
{
|
|
removeAllChildNodes();
|
|
m_columnCount = m_oldRowMin = m_oldRowMax = -1;
|
|
}
|
|
|
|
void rearrangeNodes( int rowMin, int rowMax, int columnCount )
|
|
{
|
|
const bool doReorder = ( columnCount == m_columnCount )
|
|
&& ( rowMin <= m_oldRowMax ) && ( rowMax >= m_oldRowMin );
|
|
|
|
if ( doReorder )
|
|
{
|
|
/*
|
|
We have nodes that will be at a different position in the
|
|
list of child nodes - however their content might be unchanged.
|
|
|
|
This is a common situation, when resizing or scrolling the list.
|
|
|
|
To avoid that the text/graphic nodes have to be updated we
|
|
try to rearrange the nodes, so that they are in increasing
|
|
row order for further processing.
|
|
|
|
To avoid delete/new calls we append the leading and prepend
|
|
the trailing nodes, so that they might be used later.
|
|
*/
|
|
|
|
if ( rowMin >= m_oldRowMin )
|
|
{
|
|
for ( int row = m_oldRowMin; row < rowMin; row++ )
|
|
{
|
|
for ( int col = 0; col < columnCount; col++ )
|
|
{
|
|
auto childNode = firstChild();
|
|
removeChildNode( childNode );
|
|
appendChildNode( childNode );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for ( int row = rowMin; row < m_oldRowMin; row++ )
|
|
{
|
|
for ( int col = 0; col < columnCount; col++ )
|
|
{
|
|
auto childNode = lastChild();
|
|
removeChildNode( childNode );
|
|
prependChildNode( childNode );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_oldRowMin = rowMin;
|
|
m_oldRowMax = rowMax;
|
|
m_columnCount = columnCount;
|
|
}
|
|
|
|
private:
|
|
/*
|
|
When scrolling the majority of the child nodes are simply translated
|
|
while only few rows appear/disappear. To implement this in an efficient
|
|
way we store how the nodes were related to the rows in the previous update,
|
|
*/
|
|
|
|
int m_oldRowMin = -1;
|
|
int m_oldRowMax = -1;
|
|
int m_columnCount = -1;
|
|
};
|
|
|
|
class ListViewNode final : public QSGTransformNode
|
|
{
|
|
public:
|
|
inline ListViewNode()
|
|
{
|
|
m_backgroundNode.setFlag( QSGNode::OwnedByParent, false );
|
|
appendChildNode( &m_backgroundNode );
|
|
|
|
m_foregroundNode.setFlag( QSGNode::OwnedByParent, false );
|
|
appendChildNode( &m_foregroundNode );
|
|
}
|
|
|
|
void initialize( const QskListView* listView )
|
|
{
|
|
const auto scrollPos = listView->scrollPos();
|
|
setMatrix( QTransform::fromTranslate( -scrollPos.x(), -scrollPos.y() ) );
|
|
|
|
m_clipRect = listView->viewContentsRect();
|
|
m_rowHeight = listView->rowHeight();
|
|
|
|
m_rowMin = qFloor( scrollPos.y() / m_rowHeight );
|
|
|
|
const auto rowMax = ( scrollPos.y() + m_clipRect.height() ) / m_rowHeight;
|
|
m_rowMax = qFloor( rowMax - 10e-6 );
|
|
|
|
if ( m_rowMax >= listView->rowCount() )
|
|
m_rowMax = listView->rowCount() - 1;
|
|
}
|
|
|
|
QRectF clipRect() const { return m_clipRect; }
|
|
|
|
int rowMin() const { return m_rowMin; }
|
|
int rowMax() const { return m_rowMax; }
|
|
int rowCount() const { return m_rowMax - m_rowMin + 1; }
|
|
|
|
int rowHeight() const { return m_rowHeight; }
|
|
|
|
QSGNode* backgroundNode() { return &m_backgroundNode; }
|
|
ForegroundNode* foregroundNode() { return &m_foregroundNode; }
|
|
|
|
private:
|
|
// caching some calculations to speed things up
|
|
|
|
QRectF m_clipRect;
|
|
qreal m_rowHeight;
|
|
|
|
int m_rowMin, m_rowMax;
|
|
|
|
QSGNode m_backgroundNode;
|
|
ForegroundNode m_foregroundNode;
|
|
};
|
|
}
|
|
|
|
static inline ListViewNode* qskListViewNode( const QskListView* listView )
|
|
{
|
|
if ( auto node = const_cast< QSGNode* >( qskPaintNode( listView ) ) )
|
|
{
|
|
using namespace QskSGNode;
|
|
|
|
node = findChildNode( node, QskScrollViewSkinlet::ContentsRootRole );
|
|
if ( node )
|
|
{
|
|
node = node->firstChild();
|
|
if ( node )
|
|
{
|
|
Q_ASSERT( node->type() == QSGNode::TransformNodeType );
|
|
return static_cast< ListViewNode* >( node );
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static inline ListViewNode* qskListViewNode( const QskSkinnable* skinnable )
|
|
{
|
|
return qskListViewNode( static_cast< const QskListView* >( skinnable ) );
|
|
}
|
|
|
|
QskListViewSkinlet::QskListViewSkinlet( QskSkin* skin )
|
|
: Inherited( skin )
|
|
{
|
|
}
|
|
|
|
QskListViewSkinlet::~QskListViewSkinlet() = default;
|
|
|
|
QSGNode* QskListViewSkinlet::updateContentsNode(
|
|
const QskScrollView* scrollView, QSGNode* node ) const
|
|
{
|
|
const auto listView = static_cast< const QskListView* >( scrollView );
|
|
|
|
auto listViewNode = QskSGNode::ensureNode< ListViewNode >( node );
|
|
listViewNode->initialize( listView );
|
|
|
|
updateBackgroundNodes( listView, listViewNode->backgroundNode() );
|
|
updateForegroundNodes( listView, listViewNode->foregroundNode() );
|
|
|
|
return listViewNode;
|
|
}
|
|
|
|
void QskListViewSkinlet::updateBackgroundNodes(
|
|
const QskListView* listView, QSGNode* backgroundNode ) const
|
|
{
|
|
using Q = QskListView;
|
|
|
|
auto listViewNode = static_cast< const ListViewNode* >( backgroundNode->parent() );
|
|
|
|
auto rowNode = backgroundNode->firstChild();
|
|
|
|
for ( int row = listViewNode->rowMin(); row <= listViewNode->rowMax(); row++ )
|
|
{
|
|
QskSkinStateChanger stateChanger( listView );
|
|
stateChanger.setStates( sampleStates( listView, Q::Cell, row ), row );
|
|
|
|
const auto rect = sampleRect( listView, listView->contentsRect(), Q::Cell, row );
|
|
|
|
auto newNode = updateBoxNode( listView, rowNode, rect, Q::Cell );
|
|
if ( newNode )
|
|
{
|
|
if ( newNode->parent() != backgroundNode )
|
|
backgroundNode->appendChildNode( newNode );
|
|
else
|
|
rowNode = newNode->nextSibling();
|
|
}
|
|
}
|
|
|
|
QskSGNode::removeAllChildNodesFrom( backgroundNode, rowNode );
|
|
}
|
|
|
|
void QskListViewSkinlet::updateForegroundNodes(
|
|
const QskListView* listView, QSGNode* parentNode ) const
|
|
{
|
|
auto foregroundNode = static_cast< ForegroundNode* >( parentNode );
|
|
|
|
if ( listView->rowCount() <= 0 || listView->columnCount() <= 0 )
|
|
{
|
|
foregroundNode->invalidate();
|
|
return;
|
|
}
|
|
|
|
auto listViewNode = static_cast< const ListViewNode* >( parentNode->parent() );
|
|
|
|
const auto clipRect = listViewNode->clipRect();
|
|
|
|
const int rowMin = listViewNode->rowMin();
|
|
const int rowMax = listViewNode->rowMax();
|
|
|
|
foregroundNode->rearrangeNodes( rowMin, rowMax, listView->columnCount() );
|
|
|
|
#if 1
|
|
// should be optimized for visible columns only
|
|
const int colMin = 0;
|
|
const int colMax = listView->columnCount() - 1;
|
|
#endif
|
|
|
|
const auto margins = listView->paddingHint( QskListView::Cell );
|
|
|
|
updateVisibleForegroundNodes(
|
|
listView, foregroundNode, rowMin, rowMax, margins );
|
|
|
|
// finally putting the nodes into their position
|
|
auto node = foregroundNode->firstChild();
|
|
|
|
const auto rowHeight = listView->rowHeight();
|
|
auto y = clipRect.top() + rowMin * rowHeight;
|
|
|
|
for ( int row = rowMin; row <= rowMax; row++ )
|
|
{
|
|
qreal x = clipRect.left();
|
|
|
|
for ( int col = colMin; col <= colMax; col++ )
|
|
{
|
|
Q_ASSERT( node->type() == QSGNode::TransformNodeType );
|
|
auto transformNode = static_cast< QSGTransformNode* >( node );
|
|
|
|
transformNode->setMatrix(
|
|
QTransform::fromTranslate( x + margins.left(), y + margins.top() ) );
|
|
|
|
node = node->nextSibling();
|
|
x += listView->columnWidth( col );
|
|
}
|
|
|
|
y += rowHeight;
|
|
}
|
|
}
|
|
|
|
void QskListViewSkinlet::updateVisibleForegroundNodes(
|
|
const QskListView* listView, QSGNode* parentNode,
|
|
int rowMin, int rowMax, const QMarginsF& margins ) const
|
|
{
|
|
auto node = parentNode->firstChild();
|
|
|
|
for ( int row = rowMin; row <= rowMax; row++ )
|
|
{
|
|
const auto h = listView->rowHeight() - ( margins.top() + margins.bottom() );
|
|
|
|
for ( int col = 0; col < listView->columnCount(); col++ )
|
|
{
|
|
const auto w = listView->columnWidth( col ) - ( margins.left() + margins.right() );
|
|
|
|
node = updateForegroundNode( listView,
|
|
parentNode, static_cast< QSGTransformNode* >( node ),
|
|
row, col, QSizeF( w, h ) );
|
|
|
|
node = node->nextSibling();
|
|
}
|
|
}
|
|
|
|
QskSGNode::removeAllChildNodesFrom( parentNode, node );
|
|
}
|
|
|
|
QSGTransformNode* QskListViewSkinlet::updateForegroundNode(
|
|
const QskListView* listView, QSGNode* parentNode, QSGTransformNode* cellNode,
|
|
int row, int col, const QSizeF& size ) const
|
|
{
|
|
const QRectF cellRect( 0.0, 0.0, size.width(), size.height() );
|
|
|
|
/*
|
|
Text nodes already have a transform root node - to avoid inserting extra
|
|
transform nodes, the code below becomes a bit more complicated.
|
|
*/
|
|
QSGTransformNode* newCellNode = nullptr;
|
|
|
|
if ( cellNode && ( cellNode->type() == QSGNode::TransformNodeType ) )
|
|
{
|
|
QSGNode* oldNode = cellNode;
|
|
|
|
auto newNode = updateCellNode( listView, oldNode, cellRect, row, col );
|
|
if ( newNode )
|
|
{
|
|
if ( newNode->type() == QSGNode::TransformNodeType )
|
|
{
|
|
newCellNode = static_cast< QSGTransformNode* >( newNode );
|
|
}
|
|
else
|
|
{
|
|
newCellNode = new QSGTransformNode();
|
|
newCellNode->appendChildNode( newNode );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QSGNode* oldNode = cellNode ? cellNode->firstChild() : nullptr;
|
|
auto newNode = updateCellNode( listView, oldNode, cellRect, row, col );
|
|
|
|
if ( newNode )
|
|
{
|
|
if ( newNode->type() == QSGNode::TransformNodeType )
|
|
{
|
|
newCellNode = static_cast< QSGTransformNode* >( newNode );
|
|
}
|
|
else
|
|
{
|
|
if ( cellNode == nullptr )
|
|
{
|
|
newCellNode = new QSGTransformNode();
|
|
newCellNode->appendChildNode( newNode );
|
|
}
|
|
else
|
|
{
|
|
if ( newNode != oldNode )
|
|
{
|
|
delete cellNode->firstChild();
|
|
cellNode->appendChildNode( newNode );
|
|
|
|
newCellNode = cellNode;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( newCellNode == nullptr )
|
|
newCellNode = new QSGTransformNode();
|
|
|
|
if ( cellNode != newCellNode )
|
|
{
|
|
if ( cellNode )
|
|
{
|
|
parentNode->insertChildNodeAfter( newCellNode, cellNode );
|
|
delete cellNode;
|
|
}
|
|
else
|
|
{
|
|
parentNode->appendChildNode( newCellNode );
|
|
}
|
|
}
|
|
|
|
return newCellNode;
|
|
}
|
|
|
|
QSGNode* QskListViewSkinlet::updateCellNode( const QskListView* listView,
|
|
QSGNode* contentNode, const QRectF& rect, int row, int col ) const
|
|
{
|
|
using Q = QskListView;
|
|
using namespace QskSGNode;
|
|
|
|
QskSkinStateChanger stateChanger( listView );
|
|
stateChanger.setStates( sampleStates( listView, Q::Cell, row ), row );
|
|
|
|
QSGNode* newNode = nullptr;
|
|
|
|
#if 1
|
|
/*
|
|
Alignments, text options etc are user definable attributes and need
|
|
to be adjustable - at least individually for each column - from the
|
|
public API of QskListView TODO ...
|
|
*/
|
|
#endif
|
|
const auto alignment = listView->alignmentHint(
|
|
Q::Cell, Qt::AlignVCenter | Qt::AlignLeft );
|
|
|
|
const auto value = listView->valueAt( row, col );
|
|
|
|
if ( value.canConvert< QskGraphic >() )
|
|
{
|
|
if ( nodeRole( contentNode ) == GraphicRole )
|
|
newNode = contentNode;
|
|
|
|
const auto colorFilter = listView->effectiveGraphicFilter( Q::Graphic );
|
|
|
|
newNode = updateGraphicNode( listView, newNode,
|
|
value.value< QskGraphic >(), colorFilter, rect, alignment );
|
|
|
|
if ( newNode )
|
|
setNodeRole( newNode, GraphicRole );
|
|
}
|
|
else if ( value.canConvert< QString >() )
|
|
{
|
|
if ( nodeRole( contentNode ) == TextRole )
|
|
newNode = contentNode;
|
|
|
|
newNode = updateTextNode( listView, newNode, rect, alignment,
|
|
value.toString(), Q::Text );
|
|
|
|
if ( newNode )
|
|
setNodeRole( newNode, TextRole );
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "QskListViewSkinlet: got unsupported QVariant type" << value.typeName();
|
|
}
|
|
|
|
return newNode;
|
|
}
|
|
|
|
QSizeF QskListViewSkinlet::sizeHint( const QskSkinnable* skinnable,
|
|
Qt::SizeHint which, const QSizeF& ) const
|
|
{
|
|
const auto listView = static_cast< const QskListView* >( skinnable );
|
|
|
|
qreal w = -1.0; // shouldn't we return something ???
|
|
|
|
if ( which != Qt::MaximumSize )
|
|
{
|
|
if ( listView->preferredWidthFromColumns() )
|
|
{
|
|
w = listView->scrollableSize().width();
|
|
w += listView->metric( QskScrollView::VerticalScrollBar | QskAspect::Size );
|
|
}
|
|
}
|
|
|
|
return QSizeF( w, -1.0 );
|
|
}
|
|
|
|
QskAspect::States QskListViewSkinlet::sampleStates( const QskSkinnable* skinnable,
|
|
QskAspect::Subcontrol subControl, int index ) const
|
|
{
|
|
using Q = QskListView;
|
|
|
|
if ( subControl == Q::Cell || subControl == Q::Text || subControl == Q::Graphic )
|
|
{
|
|
const auto listView = static_cast< const QskListView* >( skinnable );
|
|
return listView->rowStates( index );
|
|
}
|
|
|
|
return Inherited::sampleStates( skinnable, subControl, index );
|
|
}
|
|
|
|
QRectF QskListViewSkinlet::sampleRect( const QskSkinnable* skinnable,
|
|
const QRectF& contentsRect, QskAspect::Subcontrol subControl, int index ) const
|
|
{
|
|
using Q = QskListView;
|
|
|
|
const auto listView = static_cast< const QskListView* >( skinnable );
|
|
|
|
if ( subControl == Q::Cell )
|
|
{
|
|
auto node = qskListViewNode( listView );
|
|
const auto clipRect = node ? node->clipRect() : listView->viewContentsRect();
|
|
|
|
const auto w = clipRect.width();
|
|
const auto h = listView->rowHeight();
|
|
const auto x = clipRect.left() + listView->scrollPos().x();
|
|
const auto y = clipRect.top() + index * h;
|
|
|
|
return QRectF( x, y, w, h );
|
|
}
|
|
|
|
return Inherited::sampleRect( skinnable, contentsRect, subControl, index );
|
|
}
|
|
|
|
#include "moc_QskListViewSkinlet.cpp"
|