qskinny/src/layouts/QskSubcontrolLayoutEngine.cpp

562 lines
15 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#include "QskSubcontrolLayoutEngine.h"
#include "QskLayoutElement.h"
#include "QskLayoutChain.h"
#include "QskTextRenderer.h"
#include "QskSkinnable.h"
#include "QskMargins.h"
#include "QskTextOptions.h"
#include <qdebug.h>
#include <qfont.h>
#include <qfontmetrics.h>
#include <qmath.h>
QskSubcontrolLayoutEngine::LayoutElement::LayoutElement(
const QskSkinnable* skinnable, const QskAspect::Subcontrol subControl )
: m_skinnable( skinnable )
, m_subControl( subControl )
{
}
Qt::Alignment QskSubcontrolLayoutEngine::LayoutElement::alignment() const
{
return m_skinnable->alignmentHint( m_subControl );
}
void QskSubcontrolLayoutEngine::LayoutElement::setMaximumSize( const QSizeF& size )
{
setExplicitSizeHint( Qt::MaximumSize, size );
}
void QskSubcontrolLayoutEngine::LayoutElement::setMinimumSize( const QSizeF& size )
{
setExplicitSizeHint( Qt::MinimumSize, size );
}
void QskSubcontrolLayoutEngine::LayoutElement::setPreferredSize( const QSizeF& size )
{
setExplicitSizeHint( Qt::PreferredSize, size );
}
void QskSubcontrolLayoutEngine::LayoutElement::setFixedSize( const QSizeF& size )
{
setSizePolicy( QskSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed ) );
const auto newSize = size.expandedTo( QSizeF( 0, 0 ) );
setExplicitSizeHint( Qt::PreferredSize, newSize );
}
void QskSubcontrolLayoutEngine::LayoutElement::setExplicitSizeHint(
Qt::SizeHint which, const QSizeF& size )
{
if ( which >= Qt::MinimumSize && which <= Qt::MaximumSize )
{
const QSizeF newSize( ( size.width() < 0 ) ? -1.0 : size.width(),
( size.height() < 0 ) ? -1.0 : size.height() );
m_explicitSizeHints[ which ] = size;
}
}
QSizeF QskSubcontrolLayoutEngine::LayoutElement::sizeHint(
Qt::SizeHint which, const QSizeF& constraint ) const
{
if ( which < Qt::MinimumSize || which > Qt::MaximumSize )
return QSizeF( 0, 0 );
if ( constraint.isValid() )
return constraint;
const bool isConstrained =
constraint.width() >= 0 || constraint.height() >= 0;
QSizeF hint;
if ( !isConstrained )
{
// explicit size hints are never constrained
hint = m_explicitSizeHints[ which ];
}
if ( which == Qt::PreferredSize )
{
if ( isConstrained )
{
const QskMargins padding = m_skinnable->paddingHint( m_subControl );
auto innerConstraint = constraint;
if ( constraint.width() > 0 )
{
const auto w = constraint.width() - padding.width();
if ( w <= 0 )
return QSizeF();
innerConstraint.setWidth( w );
}
else if ( constraint.height() > 0 )
{
const auto h = constraint.height() - padding.height();
if ( h <= 0 )
return QSizeF();
innerConstraint.setHeight( h );
}
hint = implicitSize( innerConstraint );
if ( hint.width() >= 0 )
hint.setWidth( hint.width() + padding.width() );
if ( hint.height() >= 0 )
hint.setHeight( hint.height() + padding.height() );
}
else
{
if ( !hint.isValid() )
{
const auto sz = implicitSize( constraint );
if ( hint.width() < 0 )
hint.setWidth( sz.width() );
if ( hint.height() < 0 )
hint.setHeight( sz.height() );
}
}
}
else
{
if ( isConstrained )
hint = QSizeF(); // not implemented
}
return hint;
}
QSizeF QskSubcontrolLayoutEngine::TextElement::implicitSize( const QSizeF& constraint ) const
{
const auto font = skinnable()->effectiveFont( subControl() );
const auto textOptions = skinnable()->textOptionsHint( subControl() );
#if 0
// what about skinnable()->strutSizeHint( subControl() ); ????
#endif
QSizeF hint;
const qreal lineHeight = QFontMetricsF( font ).height();
if ( m_text.isEmpty() )
{
if ( constraint.height() < 0.0 )
hint.setHeight( qCeil( lineHeight ) );
}
else if ( constraint.width() >= 0.0 )
{
if ( textOptions.effectiveElideMode() != Qt::ElideNone )
{
hint.setHeight( qCeil( lineHeight ) );
}
else
{
/*
In case of QskTextOptions::NoWrap we could count
the line numbers and calculate the height from
lineHeight. TODO ...
*/
qreal maxHeight = std::numeric_limits< qreal >::max();
if ( maxHeight / lineHeight > textOptions.maximumLineCount() )
{
// be careful with overflows
maxHeight = textOptions.maximumLineCount() * lineHeight;
}
QSizeF size( constraint.width(), maxHeight );
size = QskTextRenderer::textSize( m_text, font, textOptions, size );
hint.setHeight( qCeil( size.height() ) );
}
}
else if ( constraint.height() >= 0.0 )
{
const qreal maxWidth = std::numeric_limits< qreal >::max();
QSizeF size( maxWidth, constraint.height() );
size = QskTextRenderer::textSize( m_text, font, textOptions, size );
hint.setWidth( qCeil( size.width() ) );
}
else
{
hint = QskTextRenderer::textSize( m_text, font, textOptions );
}
return hint;
}
QSizeF QskSubcontrolLayoutEngine::GraphicElement::effectiveStrutSize() const
{
auto size = skinnable()->strutSizeHint( subControl() );
if ( size.isEmpty() )
{
const qreal aspectRatio = m_sourceSize.width() / m_sourceSize.height();
if ( size.width() > 0 )
{
size.setHeight( size.width() / aspectRatio );
}
else if ( size.height() > 0 )
{
size.setWidth( size.height() * aspectRatio );
}
}
return size;
}
QSizeF QskSubcontrolLayoutEngine::GraphicElement::implicitSize( const QSizeF& constraint ) const
{
auto hint = m_sourceSize;
if ( !hint.isEmpty() )
{
const qreal aspectRatio = hint.width() / hint.height();
if ( constraint.width() >= 0.0 )
{
hint.setHeight( constraint.width() / aspectRatio );
hint.setWidth( -1.0 );
}
else if ( constraint.height() > 0.0 )
{
hint.setWidth( constraint.height() * aspectRatio );
hint.setHeight( -1.0 );
}
}
return hint;
}
static QskLayoutChain::CellData qskCell( const QskSubcontrolLayoutEngine::LayoutElement* element,
Qt::Orientation orientation, bool isLayoutOrientation, qreal constraint )
{
QskLayoutChain::CellData cell;
cell.isValid = true;
const auto policy = element->sizePolicy().policy( orientation );
if ( isLayoutOrientation )
{
const auto stretch = element->stretch();
if ( stretch < 0 )
cell.stretch = ( policy & QskSizePolicy::ExpandFlag ) ? 1 : 0;
else
cell.stretch = stretch;
}
cell.canGrow = policy & QskSizePolicy::GrowFlag;
cell.metrics = element->metrics( orientation, constraint );
return cell;
}
class QskSubcontrolLayoutEngine::PrivateData
{
public:
PrivateData( Qt::Orientation orientation )
: orientation( orientation )
{
elements.reserve( 2 ); // often Graphic + Text
}
Qt::Orientation orientation;
QVector< LayoutElement* > elements;
};
QskSubcontrolLayoutEngine::QskSubcontrolLayoutEngine( Qt::Orientation orientation )
: m_data( new PrivateData( orientation ) )
{
setExtraSpacingAt( Qt::TopEdge | Qt::BottomEdge | Qt::LeftEdge | Qt::RightEdge );
}
QskSubcontrolLayoutEngine::~QskSubcontrolLayoutEngine()
{
qDeleteAll( m_data->elements );
}
bool QskSubcontrolLayoutEngine::setOrientation( Qt::Orientation orientation )
{
if ( m_data->orientation != orientation )
{
m_data->orientation = orientation;
invalidate( LayoutCache );
return true;
}
return false;
}
Qt::Orientation QskSubcontrolLayoutEngine::orientation() const
{
return m_data->orientation;
}
void QskSubcontrolLayoutEngine::setSpacing( qreal spacing )
{
Inherited::setSpacing( spacing, Qt::Horizontal | Qt::Vertical );
}
qreal QskSubcontrolLayoutEngine::spacing() const
{
return Inherited::spacing( m_data->orientation );
}
void QskSubcontrolLayoutEngine::setGraphicTextElements( const QskSkinnable* skinnable,
QskAspect::Subcontrol textSubcontrol, const QString& text,
QskAspect::Subcontrol graphicSubControl, const QSizeF& graphicSize )
{
/*
QskSubcontrolLayoutEngine was initially created to support the
situation of an icon and a text, that can be found at several places
in conrols. This method supports to set up such a layout without
having to deal with the details of the layout classes.
*/
GraphicElement* graphicElement = nullptr;
if ( !graphicSize.isEmpty() && ( graphicSubControl != QskAspect::NoSubcontrol ) )
{
graphicElement = dynamic_cast< GraphicElement* >( element( graphicSubControl ) );
if ( graphicElement == nullptr )
{
graphicElement = new GraphicElement( skinnable, graphicSubControl );
m_data->elements.prepend( graphicElement );
}
graphicElement->setSourceSize( graphicSize );
}
TextElement* textElement = nullptr;
if ( !text.isEmpty() && ( textSubcontrol != QskAspect::NoSubcontrol ) )
{
textElement = dynamic_cast< TextElement* >( element( textSubcontrol ) );
if ( textElement == nullptr )
{
textElement = new TextElement( skinnable, textSubcontrol );
m_data->elements.append( textElement );
}
textElement->setText( text );
}
/*
Now the difficult part: setting up size policies and the preferred size.
The code below is probably not the final word - let's see what type of
default settings we need most often. TODO ...
*/
using SP = QskSizePolicy;
if ( textElement && graphicElement == nullptr )
{
textElement->setSizePolicy( SP::Preferred, SP::Constrained );
}
else if ( graphicElement && textElement == nullptr )
{
const auto size = graphicElement->effectiveStrutSize();
if ( !size.isEmpty() )
graphicElement->setFixedSize( size );
else
graphicElement->setSizePolicy( SP::Ignored, SP::ConstrainedExpanding );
}
else if ( textElement && graphicElement )
{
if ( orientation() == Qt::Horizontal )
{
graphicElement->setSizePolicy( SP::Constrained, SP::Fixed );
textElement->setSizePolicy( SP::Preferred, SP::Preferred );
}
else
{
graphicElement->setSizePolicy( SP::Preferred, SP::Fixed );
textElement->setSizePolicy( SP::Preferred, SP::Constrained );
}
auto size = graphicElement->effectiveStrutSize();
if ( size.isEmpty() )
{
const auto h = 1.5 * skinnable->effectiveFontHeight( textSubcontrol );
size.setWidth( graphicElement->widthForHeight( h ) );
size.setHeight( h );
}
graphicElement->setPreferredSize( size );
}
}
void QskSubcontrolLayoutEngine::setFixedContent(
QskAspect::Subcontrol subcontrol, Qt::Orientation orientation, Qt::Alignment alignment )
{
if( auto* e = element( subcontrol ) )
{
e->setSizePolicy( QskSizePolicy::Fixed, e->sizePolicy().verticalPolicy() );
}
Qt::Edges extraSpacing;
switch( orientation )
{
case Qt::Horizontal:
extraSpacing |= ( extraSpacingAt() & ( Qt::TopEdge | Qt::BottomEdge ) );
if( alignment & Qt::AlignLeft )
{
extraSpacing |= Qt::RightEdge;
}
else if( alignment & Qt::AlignRight )
{
extraSpacing |= Qt::LeftEdge;
}
else if( alignment & Qt::AlignHCenter )
{
extraSpacing |= Qt::LeftEdge | Qt::RightEdge;
}
break;
case Qt::Vertical:
extraSpacing |= ( extraSpacingAt() & ( Qt::LeftEdge | Qt::RightEdge ) );
if( alignment & Qt::AlignTop )
{
extraSpacing |= Qt::BottomEdge;
}
else if( alignment & Qt::AlignBottom )
{
extraSpacing |= Qt::TopEdge;
}
else if( alignment & Qt::AlignVCenter )
{
extraSpacing |= Qt::TopEdge | Qt::BottomEdge;
}
break;
}
setExtraSpacingAt( extraSpacing );
}
void QskSubcontrolLayoutEngine::addElement( LayoutElement* element )
{
m_data->elements += element;
}
QskSubcontrolLayoutEngine::LayoutElement* QskSubcontrolLayoutEngine::elementAt( int index ) const
{
if ( index >= 0 && index < count() )
return m_data->elements[ index ];
return nullptr;
}
QskSubcontrolLayoutEngine::LayoutElement* QskSubcontrolLayoutEngine::element(
QskAspect::Subcontrol subControl ) const
{
for ( auto element : m_data->elements )
{
if ( element->subControl() == subControl )
return element;
}
return nullptr;
}
QskSizePolicy QskSubcontrolLayoutEngine::sizePolicyAt( int index ) const
{
if ( index >= 0 && index < count() )
{
if ( auto element = m_data->elements[ index ] )
return element->sizePolicy();
}
return QskSizePolicy();
}
int QskSubcontrolLayoutEngine::count() const
{
return m_data->elements.count();
}
void QskSubcontrolLayoutEngine::layoutItems()
{
int row = 0;
int col = 0;
int& index = m_data->orientation == Qt::Horizontal ? col : row;
for ( auto element : m_data->elements )
{
const auto rect = geometryAt( element, QRect( col, row, 1, 1 ) );
element->setGeometry( rect );
index++;
}
}
int QskSubcontrolLayoutEngine::effectiveCount( Qt::Orientation orientation ) const
{
const auto count = m_data->elements.count();
if ( orientation == m_data->orientation )
return count;
else
return ( count >= 1 ) ? 1 : 0;
}
QRectF QskSubcontrolLayoutEngine::subControlRect( QskAspect::Subcontrol subControl ) const
{
if ( const auto el = element( subControl ) )
return el->geometry();
return QRectF( 0.0, 0.0, -1.0, -1.0 ); // something invalid
}
void QskSubcontrolLayoutEngine::invalidateElementCache()
{
}
void QskSubcontrolLayoutEngine::setupChain( Qt::Orientation orientation,
const QskLayoutChain::Segments& constraints, QskLayoutChain& chain ) const
{
uint index1 = 0;
uint index2 = 0;
const bool isLayoutOrientation = ( orientation == m_data->orientation );
for ( auto element : m_data->elements )
{
qreal constraint = -1.0;
if ( !constraints.isEmpty() )
constraint = constraints[ index1 ].length;
const auto cell = qskCell( element, orientation, isLayoutOrientation, constraint );
chain.expandCell( index2, cell );
if ( isLayoutOrientation )
index2++;
else
index1++;
}
}