qskinny/src/controls/QskMenuSkinlet.cpp
2023-05-15 13:42:19 +02:00

554 lines
15 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskMenuSkinlet.h"
#include "QskMenu.h"
#include "QskGraphic.h"
#include "QskColorFilter.h"
#include "QskTextOptions.h"
#include "QskSGNode.h"
#include "QskFunctions.h"
#include "QskMargins.h"
#include "QskFunctions.h"
#include "QskLabelData.h"
#include <qfontmetrics.h>
#include <qmath.h>
class QskMenuSkinlet::PrivateData
{
public:
class CacheGuard
{
public:
CacheGuard( PrivateData* data )
: m_data( data )
{
m_data->enableCache( true );
}
~CacheGuard()
{
m_data->enableCache( false );
}
private:
PrivateData* m_data;
};
void enableCache( bool on )
{
m_isCaching = on;
m_segmentHeight = m_segmentWidth = m_graphicWidth = m_textWidth = -1.0;
}
inline int separatorsBefore( const QskMenu* menu, int index ) const
{
int i = 0;
for ( ; i < menu->separatorCount(); i++ )
{
if ( menu->separatorPosition( i ) > index )
break;
}
return i;
}
inline qreal graphicWidth( const QskMenu* menu ) const
{
if ( m_isCaching )
{
if ( m_graphicWidth < 0.0 )
m_graphicWidth = graphicWidthInternal( menu );
return m_graphicWidth;
}
return graphicWidthInternal( menu );
}
inline qreal textWidth( const QskMenu* menu ) const
{
if ( m_isCaching )
{
if ( m_textWidth < 0.0 )
m_textWidth = textWidthInternal( menu );
return m_textWidth;
}
return textWidthInternal( menu );
}
inline qreal segmentWidth( const QskMenu* menu ) const
{
if ( m_isCaching )
{
if ( m_segmentWidth < 0.0 )
m_segmentWidth = segmentWidthInternal( menu );
return m_segmentWidth;
}
return segmentWidthInternal( menu );
}
inline qreal segmentHeight( const QskMenu* menu ) const
{
if ( m_isCaching )
{
if ( m_segmentHeight < 0.0 )
m_segmentHeight = segmentHeightInternal( menu );
return m_segmentHeight;
}
return segmentHeightInternal( menu );
}
private:
qreal graphicWidthInternal( const QskMenu* menu ) const
{
const auto skinlet = menu->effectiveSkinlet();
const auto hint = menu->strutSizeHint( QskMenu::Icon );
const qreal textHeight = menu->effectiveFontHeight( QskMenu::Text );
const auto h = qMax( hint.height(), textHeight );
qreal maxW = 0.0;
const auto options = menu->options();
for ( auto& option : options )
{
const auto graphic = option.icon().graphic();
if ( !graphic.isNull() )
{
const auto w = graphic.widthForHeight( h );
if( w > maxW )
maxW = w;
}
}
return qMax( hint.width(), maxW );
}
qreal textWidthInternal( const QskMenu* menu ) const
{
const auto skinlet = menu->effectiveSkinlet();
const QFontMetricsF fm( menu->effectiveFont( QskMenu::Text ) );
auto maxWidth = 0.0;
const auto options = menu->options();
for ( auto& option : options )
{
if( !option.text().isEmpty() )
{
const auto w = qskHorizontalAdvance( fm, option.text() );
if( w > maxWidth )
maxWidth = w;
}
}
return maxWidth;
}
qreal segmentWidthInternal( const QskMenu* menu ) const
{
using Q = QskMenu;
const auto spacing = menu->spacingHint( Q::Segment );
const auto padding = menu->paddingHint( Q::Segment );
auto w = graphicWidth( menu ) + spacing + textWidth( menu );
w += padding.left() + padding.right();
const auto minWidth = menu->strutSizeHint( Q::Segment ).width();
return qMax( w, minWidth );
}
qreal segmentHeightInternal( const QskMenu* menu ) const
{
using Q = QskMenu;
const auto graphicHeight = menu->strutSizeHint( Q::Icon ).height();
const auto textHeight = menu->effectiveFontHeight( Q::Text );
const auto padding = menu->paddingHint( Q::Segment );
qreal h = qMax( graphicHeight, textHeight );
h += padding.top() + padding.bottom();
const auto minHeight = menu->strutSizeHint( Q::Segment ).height();
h = qMax( h, minHeight );
return h;
}
bool m_isCaching;
mutable qreal m_graphicWidth = -1.0;
mutable qreal m_textWidth = -1.0;
mutable qreal m_segmentHeight = -1.0;
mutable qreal m_segmentWidth = -1.0;
};
QskMenuSkinlet::QskMenuSkinlet( QskSkin* skin )
: Inherited( skin )
, m_data( new PrivateData() )
{
appendNodeRoles( { PanelRole } );
}
QskMenuSkinlet::~QskMenuSkinlet() = default;
QRectF QskMenuSkinlet::cursorRect(
const QskSkinnable* skinnable, const QRectF& contentsRect, int index ) const
{
const auto count = sampleCount( skinnable, QskMenu::Segment );
auto rect = sampleRect( skinnable, contentsRect,
QskMenu::Segment, qBound( 0, index, count ) );
if ( index < 0 )
rect.setBottom( rect.top() );
if ( index >= count )
rect.setTop( rect.bottom() );
return rect;
}
QRectF QskMenuSkinlet::subControlRect(
const QskSkinnable* skinnable, const QRectF& contentsRect,
QskAspect::Subcontrol subControl ) const
{
using Q = QskMenu;
const auto menu = static_cast< const QskMenu* >( skinnable );
if( subControl == Q::Panel )
{
return contentsRect;
}
if( subControl == Q::Cursor )
{
if ( menu->currentIndex() < 0 )
return QRectF();
const qreal pos = menu->positionHint( Q::Cursor );
const int pos1 = qFloor( pos );
const int pos2 = qCeil( pos );
auto rect = cursorRect( skinnable, contentsRect, pos1 );
if ( pos1 != pos2 )
{
const auto r = cursorRect( skinnable, contentsRect, pos2 );
const qreal ratio = ( pos - pos1 ) / ( pos2 - pos1 );
rect = qskInterpolatedRect( rect, r, ratio );
}
return rect;
}
return Inherited::subControlRect( skinnable, contentsRect, subControl );
}
QRectF QskMenuSkinlet::sampleRect(
const QskSkinnable* skinnable, const QRectF& contentsRect,
QskAspect::Subcontrol subControl, int index ) const
{
using Q = QskMenu;
const auto menu = static_cast< const QskMenu* >( skinnable );
if ( subControl == Q::Segment )
{
const auto r = menu->subControlContentsRect( Q::Panel );
auto h = m_data->segmentHeight( menu );
if ( int n = m_data->separatorsBefore( menu, index ) )
{
// spacing ???
const qreal separatorH = menu->metric( Q::Separator | QskAspect::Size );
h += n * separatorH;
}
return QRectF( r.x(), r.y() + index * h, r.width(), h );
}
if ( subControl == QskMenu::Icon || subControl == QskMenu::Text )
{
const auto r = sampleRect( menu, contentsRect, Q::Segment, index );
const auto graphicWidth = m_data->graphicWidth( menu );
if ( subControl == QskMenu::Icon )
{
auto graphicRect = r;
graphicRect.setWidth( graphicWidth );
const auto padding = menu->paddingHint( QskMenu::Icon );
graphicRect = graphicRect.marginsRemoved( padding );
return graphicRect;
}
else
{
auto textRect = r;
if ( graphicWidth > 0.0 )
{
const auto spacing = skinnable->spacingHint( Q::Segment );
textRect.setX( r.x() + graphicWidth + spacing );
}
return textRect;
}
}
if ( subControl == QskMenu::Separator )
{
const int pos = menu->separatorPosition( index );
if ( pos < 0 )
return QRectF();
QRectF r = menu->subControlContentsRect( Q::Panel );
if ( pos < menu->count() )
{
const auto segmentRect = sampleRect( skinnable, contentsRect, Q::Segment, pos );
r.setBottom( segmentRect.top() ); // spacing ???
}
const qreal h = menu->metric( Q::Separator | QskAspect::Size );
r.setTop( r.bottom() - h );
return r;
}
return Inherited::sampleRect(
skinnable, contentsRect, subControl, index );
}
int QskMenuSkinlet::sampleIndexAt(
const QskSkinnable* skinnable, const QRectF& contentsRect,
QskAspect::Subcontrol subControl, const QPointF& pos ) const
{
const PrivateData::CacheGuard guard( m_data.get() );
return Inherited::sampleIndexAt( skinnable, contentsRect, subControl, pos );
}
int QskMenuSkinlet::sampleCount(
const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const
{
using Q = QskMenu;
if ( subControl == Q::Segment || subControl == Q::Icon || subControl == Q::Text )
{
const auto menu = static_cast< const QskMenu* >( skinnable );
return menu->count();
}
if ( subControl == Q::Separator )
{
const auto menu = static_cast< const QskMenu* >( skinnable );
return menu->separatorCount();
}
return Inherited::sampleCount( skinnable, subControl );
}
QskAspect::States QskMenuSkinlet::sampleStates(
const QskSkinnable* skinnable, QskAspect::Subcontrol subControl, int index ) const
{
using Q = QskMenu;
auto states = Inherited::sampleStates( skinnable, subControl, index );
if ( subControl == Q::Segment || subControl == Q::Icon || subControl == Q::Text )
{
const auto menu = static_cast< const QskMenu* >( skinnable );
if ( menu->currentIndex() == index )
states |= QskMenu::Selected;
}
return states;
}
QVariant QskMenuSkinlet::sampleAt( const QskSkinnable* skinnable,
QskAspect::Subcontrol subControl, int index ) const
{
using Q = QskMenu;
if ( subControl == Q::Icon || subControl == Q::Text )
{
const auto menu = static_cast< const QskMenu* >( skinnable );
const auto option = menu->optionAt( index );
if ( subControl == Q::Icon )
return QVariant::fromValue( option.icon().graphic() );
else
return QVariant::fromValue( option.text() );
}
return Inherited::sampleAt( skinnable, subControl, index );
}
QSGNode* QskMenuSkinlet::updateContentsNode(
const QskPopup* popup, QSGNode* contentsNode ) const
{
const PrivateData::CacheGuard guard( m_data.get() );
return updateMenuNode( popup, contentsNode );
}
QSGNode* QskMenuSkinlet::updateMenuNode(
const QskSkinnable* skinnable, QSGNode* contentsNode ) const
{
enum { Panel, Segment, Cursor, Icon, Text, Separator };
static QVector< quint8 > roles = { Panel, Separator, Segment, Cursor, Icon, Text };
if ( contentsNode == nullptr )
contentsNode = new QSGNode();
for ( const auto role : roles )
{
auto oldNode = QskSGNode::findChildNode( contentsNode, role );
QSGNode* newNode = nullptr;
switch( role )
{
case Panel:
{
newNode = updateBoxNode( skinnable, oldNode, QskMenu::Panel );
break;
}
case Segment:
{
newNode = updateSeriesNode( skinnable, QskMenu::Segment, oldNode );
break;
}
case Cursor:
{
newNode = updateBoxNode( skinnable, oldNode, QskMenu::Cursor );
break;
}
case Icon:
{
newNode = updateSeriesNode( skinnable, QskMenu::Icon, oldNode );
break;
}
case Text:
{
newNode = updateSeriesNode( skinnable, QskMenu::Text, oldNode );
break;
}
case Separator:
{
newNode = updateSeriesNode( skinnable, QskMenu::Separator, oldNode );
break;
}
}
QskSGNode::replaceChildNode( roles, role, contentsNode, oldNode, newNode );
}
return contentsNode;
}
QSGNode* QskMenuSkinlet::updateSampleNode( const QskSkinnable* skinnable,
QskAspect::Subcontrol subControl, int index, QSGNode* node ) const
{
using Q = QskMenu;
auto menu = static_cast< const QskMenu* >( skinnable );
const auto rect = sampleRect( menu, menu->contentsRect(), subControl, index );
if ( subControl == Q::Segment )
{
return updateBoxNode( menu, node, rect, subControl );
}
if ( subControl == Q::Icon )
{
const auto graphic = menu->optionAt( index ).icon().graphic();
if ( graphic.isNull() )
return nullptr;
const auto alignment = menu->alignmentHint( subControl, Qt::AlignCenter );
const auto filter = menu->effectiveGraphicFilter( subControl );
return QskSkinlet::updateGraphicNode(
menu, node, graphic, filter, rect, alignment );
}
if ( subControl == Q::Text )
{
const auto text = menu->optionAt( index ).text();
if ( text.isEmpty() )
return nullptr;
const auto alignment = menu->alignmentHint(
subControl, Qt::AlignVCenter | Qt::AlignLeft );
return QskSkinlet::updateTextNode( menu, node, rect,
alignment, text, Q::Text );
}
if ( subControl == Q::Separator )
{
auto gradient = menu->gradientHint( subControl );
if ( ( gradient.type() == QskGradient::Stops ) && !gradient.isMonochrome() )
gradient.setLinearDirection( Qt::Vertical );
return updateBoxNode( menu, node, rect, gradient, subControl );
}
return nullptr;
}
QSizeF QskMenuSkinlet::sizeHint( const QskSkinnable* skinnable,
Qt::SizeHint which, const QSizeF& ) const
{
if ( which != Qt::PreferredSize )
return QSizeF();
using Q = QskMenu;
const auto menu = static_cast< const QskMenu* >( skinnable );
const PrivateData::CacheGuard guard( m_data.get() );
qreal w = 0.0;
qreal h = 0.0;
if ( const auto count = sampleCount( skinnable, Q::Segment ) )
{
w = m_data->segmentWidth( menu );
h = count * m_data->segmentHeight( menu );
}
if ( const auto count = sampleCount( skinnable, Q::Separator ) )
{
h += count * menu->metric( Q::Separator | QskAspect::Size );
}
auto hint = skinnable->outerBoxSize( QskMenu::Panel, QSizeF( w, h ) );
hint = hint.expandedTo( skinnable->strutSizeHint( QskMenu::Panel ) );
return hint;
}
#include "moc_QskMenuSkinlet.cpp"