diff --git a/src/nodes/QskScaleRenderer.cpp b/src/nodes/QskScaleRenderer.cpp new file mode 100644 index 00000000..3c17d1ae --- /dev/null +++ b/src/nodes/QskScaleRenderer.cpp @@ -0,0 +1,338 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#include "QskScaleRenderer.h" +#include "QskSkinlet.h" +#include "QskSGNode.h" +#include "QskTickmarksNode.h" +#include "QskTextNode.h" +#include "QskGraphicNode.h" +#include "QskTextOptions.h" +#include "QskGraphic.h" +#include "QskControl.h" +#include "QskFunctions.h" + +#include +#include + +static QSGNode* qskRemoveTraillingNodes( QSGNode* node, QSGNode* childNode ) +{ + QskSGNode::removeAllChildNodesFrom( node, childNode ); + return nullptr; +} + +static inline void qskInsertRemoveChild( QSGNode* parentNode, + QSGNode* oldNode, QSGNode* newNode, bool append ) +{ + if ( newNode == oldNode ) + return; + + if ( oldNode ) + { + parentNode->removeChildNode( oldNode ); + if ( oldNode->flags() & QSGNode::OwnedByParent ) + delete oldNode; + } + + if ( newNode ) + { + if ( append ) + parentNode->appendChildNode( newNode ); + else + parentNode->prependChildNode( newNode ); + } +} + +void QskScaleRenderer::setOrientation( Qt::Orientation orientation ) +{ + m_orientation = orientation; +} + +void QskScaleRenderer::setBoundaries( const QskIntervalF& boundaries ) +{ + m_boundaries = boundaries; +} + +void QskScaleRenderer::setTickmarks( const QskScaleTickmarks& tickmarks ) +{ + m_tickmarks = tickmarks; +} + +void QskScaleRenderer::setTickColor( const QColor& color ) +{ + m_tickColor = color; +} + +void QskScaleRenderer::setTickWidth( qreal width ) +{ + m_tickWidth = width; +} + +void QskScaleRenderer::setFont( const QFont& font ) +{ + m_font = font; +} + +void QskScaleRenderer::setTextColors( const QskTextColors& textColors ) +{ + m_textColors = textColors; +} + +void QskScaleRenderer::setColorFilter( const QskColorFilter& colorFilter ) +{ + m_colorFilter = colorFilter; +} + +QSGNode* QskScaleRenderer::updateScaleNode( + const QskSkinnable* skinnable, const QRectF& tickmarksRect, + const QRectF& labelsRect, QSGNode* node ) +{ + enum Role + { + Ticks = 1, + Labels = 2 + }; + + if ( node == nullptr ) + node = new QSGNode(); + + { + QSGNode* oldNode = QskSGNode::findChildNode( node, Ticks ); + QSGNode* newNode = nullptr; + + if ( !tickmarksRect.isEmpty() ) + { + newNode = updateTicksNode( skinnable, tickmarksRect, oldNode ); + if ( newNode ) + QskSGNode::setNodeRole( newNode, Ticks ); + } + + qskInsertRemoveChild( node, oldNode, newNode, false ); + } + + { + QSGNode* oldNode = QskSGNode::findChildNode( node, Labels ); + QSGNode* newNode = nullptr; + + if ( !labelsRect.isEmpty() ) + { + newNode = updateLabelsNode( skinnable, labelsRect, oldNode ); + if ( newNode ) + QskSGNode::setNodeRole( newNode, Labels ); + } + + qskInsertRemoveChild( node, oldNode, newNode, true ); + } + + return node; +} + +QSGNode* QskScaleRenderer::updateTicksNode( + const QskSkinnable*, const QRectF& rect, QSGNode* node ) const +{ + if ( rect.isEmpty() ) + return nullptr; + + auto ticksNode = static_cast< QskTickmarksNode* >( node ); + + if( ticksNode == nullptr ) + ticksNode = new QskTickmarksNode; + + ticksNode->update( m_tickColor, rect, m_boundaries, + m_tickmarks, m_tickWidth, m_orientation ); + + return ticksNode; +} + +QSGNode* QskScaleRenderer::updateLabelsNode( + const QskSkinnable* skinnable, const QRectF& rect, QSGNode* node ) const +{ + if ( rect.isEmpty() ) + return nullptr; + + const auto ticks = m_tickmarks.majorTicks(); + if ( ticks.isEmpty() ) + return nullptr; + + if( node == nullptr ) + node = new QSGNode; + + const QFontMetricsF fm( m_font ); + + const qreal length = ( m_orientation == Qt::Horizontal ) ? rect.width() : rect.height(); + const qreal ratio = length / m_boundaries.width(); + + auto nextNode = node->firstChild(); + + for ( auto tick : ticks ) + { + enum LabelNodeRole + { + TextNode = 1, + GraphicNode = 2 + }; + + const auto label = labelAt( tick ); + if ( label.isNull() ) + continue; + + const qreal tickPos = ratio * ( tick - m_boundaries.lowerBound() ); + + if ( label.canConvert< QString >() ) + { + const auto text = label.toString(); + if ( text.isEmpty() ) + continue; + + QRectF r; + Qt::Alignment alignment; + + if( m_orientation == Qt::Horizontal ) + { + const auto w = qskHorizontalAdvance( fm, text ); + + auto pos = tickPos - 0.5 * w; + pos = qBound( 0.0, pos, rect.width() - w ); + + r = QRectF( rect.x() + pos, rect.y(), w, rect.height() ); + + alignment = Qt::AlignLeft; + } + else + { + const auto h = fm.height(); + + /* + when clipping the label we can expand the clip rectangle + by the ascent/descent margins, as nothing gets painted there + anyway. + */ + const qreal distTop = h - fm.ascent(); + const qreal distBottom = fm.descent(); + + auto pos = rect.height() - ( tickPos + 0.5 * h ); + pos = qBound( -distTop, pos, rect.height() - h + distBottom ); + + r = QRectF( rect.x(), rect.y() + pos, rect.width(), rect.height() ); + + alignment = Qt::AlignRight; + } + + if ( nextNode && QskSGNode::nodeRole( nextNode ) != TextNode ) + { + nextNode = qskRemoveTraillingNodes( node, nextNode ); + } + + if ( nextNode == nullptr ) + { + nextNode = new QskTextNode; + QskSGNode::setNodeRole( nextNode, TextNode ); + node->appendChildNode( nextNode ); + } + + auto textNode = static_cast< QskTextNode* >( nextNode ); + textNode->setTextData( skinnable->owningControl(), text, r, m_font, + QskTextOptions(), m_textColors, alignment, Qsk::Normal ); + + nextNode = nextNode->nextSibling(); + } + else if ( label.canConvert< QskGraphic >() ) + { + const auto graphic = label.value< QskGraphic >(); + if ( graphic.isNull() ) + continue; + + QRectF r; + + const auto h = fm.height(); + const auto w = graphic.widthForHeight( h ); + + Qt::Alignment alignment; + + if( m_orientation == Qt::Horizontal ) + { + auto pos = tickPos - 0.5 * w; + pos = qBound( 0.0, pos, rect.width() - w ); + + r = QRectF( rect.x() + pos, rect.y(), w, h ); + alignment = Qt::AlignHCenter | Qt::AlignBottom; + } + else + { + auto pos = rect.height() - ( tickPos + 0.5 * h ); + pos = qBound( 0.0, pos, rect.height() - h ); + + r = QRectF( rect.right() - w, rect.y() + pos, w, h ); + alignment = Qt::AlignRight | Qt::AlignVCenter; + } + + if ( nextNode && QskSGNode::nodeRole( nextNode ) != GraphicNode ) + { + nextNode = qskRemoveTraillingNodes( node, nextNode ); + } + + if ( nextNode == nullptr ) + { + nextNode = new QskGraphicNode; + QskSGNode::setNodeRole( nextNode, GraphicNode ); + node->appendChildNode( nextNode ); + } + + auto graphicNode = static_cast< QskGraphicNode* >( nextNode ); + + QskSkinlet::updateGraphicNode( + skinnable->owningControl(), graphicNode, + graphic, m_colorFilter, r, alignment ); + + nextNode = nextNode->nextSibling(); + } + } + + qskRemoveTraillingNodes( node, nextNode ); + + return node; +} + +QVariant QskScaleRenderer::labelAt( qreal pos ) const +{ + return QString::number( pos, 'g' ); +} + +qreal QskScaleRenderer::maxLabelWidth() const +{ + const auto ticks = m_tickmarks.majorTicks(); + if ( ticks.isEmpty() ) + return 0.0; + + const QFontMetricsF fm( m_font ); + + qreal maxWidth = 0.0; + + for ( auto tick : ticks ) + { + qreal w = 0.0; + + const auto label = labelAt( tick ); + if ( label.isNull() ) + continue; + + if ( label.canConvert< QString >() ) + { + w = qskHorizontalAdvance( fm, label.toString() ); + } + else if ( label.canConvert< QskGraphic >() ) + { + const auto graphic = label.value< QskGraphic >(); + if ( !graphic.isNull() ) + { + w = graphic.widthForHeight( fm.height() ); + } + } + + maxWidth = qMax( w, maxWidth ); + } + + return maxWidth; +} diff --git a/src/nodes/QskScaleRenderer.h b/src/nodes/QskScaleRenderer.h new file mode 100644 index 00000000..65fe40a1 --- /dev/null +++ b/src/nodes/QskScaleRenderer.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#ifndef QSK_SCALE_RENDERER_H +#define QSK_SCALE_RENDERER_H + +#include "QskGlobal.h" +#include "QskIntervalF.h" +#include "QskScaleTickmarks.h" +#include "QskTextColors.h" +#include "QskColorFilter.h" + +#include +#include +#include + +class QskSkinnable; + +class QSGNode; +class QVariant; +class QRectF; + +class QSK_EXPORT QskScaleRenderer +{ + public: + void setOrientation( Qt::Orientation ); + + void setBoundaries( const QskIntervalF& ); + void setTickmarks( const QskScaleTickmarks& ); + + void setTickColor( const QColor& ); + void setTickWidth( qreal ); + + void setFont( const QFont& ); + void setTextColors( const QskTextColors& ); + + void setColorFilter( const QskColorFilter& ); + + QSGNode* updateScaleNode( const QskSkinnable*, + const QRectF& tickmarksRect, const QRectF& labelsRect, QSGNode* ); + + virtual QVariant labelAt( qreal pos ) const; + qreal maxLabelWidth() const; + + virtual QSGNode* updateTicksNode( + const QskSkinnable*, const QRectF&, QSGNode* ) const; + + virtual QSGNode* updateLabelsNode( + const QskSkinnable*, const QRectF&, QSGNode* node ) const; + + private: + Qt::Orientation m_orientation = Qt::Horizontal; + + QskIntervalF m_boundaries; + QskScaleTickmarks m_tickmarks; + + QColor m_tickColor = Qt::black; + qreal m_tickWidth = 1.0; + + QFont m_font; + QskTextColors m_textColors; + + QskColorFilter m_colorFilter; +}; + +#endif diff --git a/src/src.pro b/src/src.pro index 174c1e0c..5d4ab67f 100644 --- a/src/src.pro +++ b/src/src.pro @@ -92,6 +92,7 @@ HEADERS += \ nodes/QskPaintedNode.h \ nodes/QskPlainTextRenderer.h \ nodes/QskRichTextRenderer.h \ + nodes/QskScaleRenderer.h \ nodes/QskSGNode.h \ nodes/QskTextNode.h \ nodes/QskTextRenderer.h \ @@ -110,6 +111,7 @@ SOURCES += \ nodes/QskPaintedNode.cpp \ nodes/QskPlainTextRenderer.cpp \ nodes/QskRichTextRenderer.cpp \ + nodes/QskScaleRenderer.cpp \ nodes/QskSGNode.cpp \ nodes/QskTextNode.cpp \ nodes/QskTextRenderer.cpp \