diff --git a/src/KChart/Cartesian/KChartStockDiagram.cpp b/src/KChart/Cartesian/KChartStockDiagram.cpp index 71abece..b5abd2e 100644 --- a/src/KChart/Cartesian/KChartStockDiagram.cpp +++ b/src/KChart/Cartesian/KChartStockDiagram.cpp @@ -1,378 +1,378 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "KChartStockDiagram.h" #include "KChartStockDiagram_p.h" #include "KChartPaintContext.h" #include "KChartPainterSaver_p.h" using namespace KChart; #define d d_func() StockDiagram::StockDiagram( QWidget *parent, CartesianCoordinatePlane *plane ) : AbstractCartesianDiagram( new Private(), parent, plane ) { init(); } StockDiagram::~StockDiagram() { } -/** +/* * Initializes the diagram */ void StockDiagram::init() { d->diagram = this; d->compressor.setModel( attributesModel() ); // Set properties to defaults d->type = HighLowClose; d->upTrendCandlestickBrush = QBrush( Qt::white ); d->downTrendCandlestickBrush = QBrush( Qt::black ); d->upTrendCandlestickPen = QPen( Qt::black ); d->downTrendCandlestickPen = QPen( Qt::black ); d->lowHighLinePen = QPen( Qt::black ); setDatasetDimensionInternal( 3 ); //setDatasetDimension( 3 ); setPen( QPen( Qt::black ) ); } /** * Switches between the supported types of stock charts, * depending on \a type */ void StockDiagram::setType( Type type ) { d->type = type; emit propertiesChanged(); } /** * @return the type of this diagram */ StockDiagram::Type StockDiagram::type() const { return d->type; } void StockDiagram::setStockBarAttributes( const StockBarAttributes &attr ) { attributesModel()->setModelData( QVariant::fromValue( attr ), StockBarAttributesRole ); emit propertiesChanged(); } StockBarAttributes StockDiagram::stockBarAttributes() const { return attributesModel()->modelData( StockBarAttributesRole ).value(); } void StockDiagram::setStockBarAttributes( int column, const StockBarAttributes &attr ) { d->setDatasetAttrs( column, QVariant::fromValue( attr ), StockBarAttributesRole ); emit propertiesChanged(); } StockBarAttributes StockDiagram::stockBarAttributes( int column ) const { const QVariant attr( d->datasetAttrs( column, StockBarAttributesRole ) ); if ( attr.isValid() ) return attr.value(); return stockBarAttributes(); } /** * Sets the 3D attributes for all bars (i.e. candlesticks) * * @param attr The 3D attributes to set */ void StockDiagram::setThreeDBarAttributes( const ThreeDBarAttributes &attr ) { attributesModel()->setModelData( QVariant::fromValue( attr ), ThreeDBarAttributesRole ); emit propertiesChanged(); } /** * Returns the 3D attributes for all bars (i.e. candlesticks) * * @return the 3D bar attributes */ ThreeDBarAttributes StockDiagram::threeDBarAttributes() const { return attributesModel()->modelData( ThreeDBarAttributesRole ).value(); } /** * Sets the 3D attributes for the bar (i.e. candlestick) in certain column * of the diagram * * Note: Every column in a StockDiagram is represented by a row in the model * * @param column The column to set the 3D bar attributes for * @param attr The 3D attributes to set */ void StockDiagram::setThreeDBarAttributes( int column, const ThreeDBarAttributes &attr ) { d->setDatasetAttrs( column, QVariant::fromValue( attr ), StockBarAttributesRole ); emit propertiesChanged(); } /** * Returns the 3D attributes for a bars (i.e. candlestick) in a certain column * of the diagram * * Note: Every column in a StockDiagram is represented by a row in the model * * @param column The column to get the 3D bar attributes for * @return The 3D attributes for the specified column */ ThreeDBarAttributes StockDiagram::threeDBarAttributes( int column ) const { const QVariant attr( d->datasetAttrs( column, ThreeDBarAttributesRole ) ); if ( attr.isValid() ) return attr.value(); return threeDBarAttributes(); } void StockDiagram::setLowHighLinePen( const QPen &pen ) { d->lowHighLinePen = pen; } QPen StockDiagram::lowHighLinePen() const { return d->lowHighLinePen; } void StockDiagram::setLowHighLinePen( int column, const QPen &pen ) { d->lowHighLinePens[column] = pen; } QPen StockDiagram::lowHighLinePen( int column ) const { if ( d->lowHighLinePens.contains( column ) ) return d->lowHighLinePens[column]; return d->lowHighLinePen; } void StockDiagram::setUpTrendCandlestickBrush( const QBrush &brush ) { d->upTrendCandlestickBrush = brush; } QBrush StockDiagram::upTrendCandlestickBrush() const { return d->upTrendCandlestickBrush; } void StockDiagram::setDownTrendCandlestickBrush( const QBrush &brush ) { d->downTrendCandlestickBrush = brush; } QBrush StockDiagram::downTrendCandlestickBrush() const { return d->downTrendCandlestickBrush; } void StockDiagram::setUpTrendCandlestickBrush( int column, const QBrush &brush ) { d->upTrendCandlestickBrushes[column] = brush; } QBrush StockDiagram::upTrendCandlestickBrush( int column ) const { if ( d->upTrendCandlestickBrushes.contains( column ) ) return d->upTrendCandlestickBrushes[column]; return d->upTrendCandlestickBrush; } void StockDiagram::setDownTrendCandlestickBrush( int column, const QBrush &brush ) { d->downTrendCandlestickBrushes[column] = brush; } QBrush StockDiagram::downTrendCandlestickBrush( int column ) const { if ( d->downTrendCandlestickBrushes.contains( column ) ) return d->downTrendCandlestickBrushes[column]; return d->downTrendCandlestickBrush; } void StockDiagram::setUpTrendCandlestickPen( const QPen &pen ) { d->upTrendCandlestickPen = pen; } QPen StockDiagram::upTrendCandlestickPen() const { return d->upTrendCandlestickPen; } void StockDiagram::setDownTrendCandlestickPen( const QPen &pen ) { d->downTrendCandlestickPen = pen; } QPen StockDiagram::downTrendCandlestickPen() const { return d->downTrendCandlestickPen; } void StockDiagram::setUpTrendCandlestickPen( int column, const QPen &pen ) { d->upTrendCandlestickPens[column] = pen; } QPen StockDiagram::upTrendCandlestickPen( int column ) const { if ( d->upTrendCandlestickPens.contains( column ) ) return d->upTrendCandlestickPens[column]; return d->upTrendCandlestickPen; } void StockDiagram::setDownTrendCandlestickPen( int column, const QPen &pen ) { d->downTrendCandlestickPens[column] = pen; } QPen StockDiagram::downTrendCandlestickPen( int column ) const { if ( d->downTrendCandlestickPens.contains( column ) ) return d->downTrendCandlestickPens[column]; return d->downTrendCandlestickPen; } #if defined(Q_COMPILER_MANGLES_RETURN_TYPE) const #endif int StockDiagram::numberOfAbscissaSegments() const { return 1; } #if defined(Q_COMPILER_MANGLES_RETURN_TYPE) const #endif int StockDiagram::numberOfOrdinateSegments() const { return 1; } void StockDiagram::paint( PaintContext *context ) { // Clear old reverse mapping data and create new // reverse mapping scene d->reverseMapper.clear(); PainterSaver painterSaver( context->painter() ); const int rowCount = attributesModel()->rowCount( attributesModelRootIndex() ); const int divisor = ( d->type == OpenHighLowClose || d->type == Candlestick ) ? 4 : 3; const int colCount = attributesModel()->columnCount( attributesModelRootIndex() ) / divisor; for ( int col = 0; col < colCount; ++col ) { for ( int row = 0; row < rowCount; row++ ) { CartesianDiagramDataCompressor::DataPoint low; CartesianDiagramDataCompressor::DataPoint high; CartesianDiagramDataCompressor::DataPoint open; CartesianDiagramDataCompressor::DataPoint close; CartesianDiagramDataCompressor::DataPoint volume; if ( d->type == HighLowClose ) { const CartesianDiagramDataCompressor::CachePosition highPos( row, col * divisor ); const CartesianDiagramDataCompressor::CachePosition lowPos( row, col * divisor + 1 ); const CartesianDiagramDataCompressor::CachePosition closePos( row, col * divisor + 2 ); low = d->compressor.data( lowPos ); high = d->compressor.data( highPos ); close = d->compressor.data( closePos ); } else if ( d->type == OpenHighLowClose || d->type == Candlestick ) { const CartesianDiagramDataCompressor::CachePosition openPos( row, col * divisor ); const CartesianDiagramDataCompressor::CachePosition highPos( row, col * divisor + 1 ); const CartesianDiagramDataCompressor::CachePosition lowPos( row, col * divisor + 2 ); const CartesianDiagramDataCompressor::CachePosition closePos( row, col * divisor + 3 ); open = d->compressor.data( openPos ); low = d->compressor.data( lowPos ); high = d->compressor.data( highPos ); close = d->compressor.data( closePos ); } switch ( d->type ) { case HighLowClose: open.hidden = true; // Fall-through intended! case OpenHighLowClose: if ( close.index.isValid() && low.index.isValid() && high.index.isValid() ) d->drawOHLCBar( col, open, high, low, close, context ); break; case Candlestick: d->drawCandlestick( col, open, high, low, close, context ); break; } } } } void StockDiagram::resize( const QSizeF &size ) { d->compressor.setResolution( static_cast< int >( size.width() * coordinatePlane()->zoomFactorX() ), static_cast< int >( size.height() * coordinatePlane()->zoomFactorY() ) ); setDataBoundariesDirty(); AbstractCartesianDiagram::resize( size ); } qreal StockDiagram::threeDItemDepth( int column ) const { Q_UNUSED( column ); //FIXME: Implement threeD functionality return 1.0; } qreal StockDiagram::threeDItemDepth( const QModelIndex &index ) const { Q_UNUSED( index ); //FIXME: Implement threeD functionality return 1.0; } const QPair StockDiagram::calculateDataBoundaries() const { const int rowCount = attributesModel()->rowCount( attributesModelRootIndex() ); const int colCount = attributesModel()->columnCount( attributesModelRootIndex() ); qreal xMin = 0.0; qreal xMax = rowCount; qreal yMin = 0.0; qreal yMax = 0.0; for ( int row = 0; row < rowCount; row++ ) { for ( int col = 0; col < colCount; col++ ) { const CartesianDiagramDataCompressor::CachePosition pos( row, col ); const CartesianDiagramDataCompressor::DataPoint point = d->compressor.data( pos ); yMax = qMax( yMax, point.value ); yMin = qMin( yMin, point.value ); // FIXME: Can stock charts really have negative values? } } return QPair( QPointF( xMin, yMin ), QPointF( xMax, yMax ) ); } diff --git a/src/KChart/Cartesian/KChartStockDiagram_p.cpp b/src/KChart/Cartesian/KChartStockDiagram_p.cpp index ae6beb8..b234a37 100644 --- a/src/KChart/Cartesian/KChartStockDiagram_p.cpp +++ b/src/KChart/Cartesian/KChartStockDiagram_p.cpp @@ -1,537 +1,537 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "KChartStockDiagram_p.h" #include "KChartPainterSaver_p.h" using namespace KChart; class Q_DECL_HIDDEN StockDiagram::Private::ThreeDPainter { public: struct ThreeDProperties { qreal depth; qreal angle; bool useShadowColors; }; ThreeDPainter( QPainter *p ) : painter( p ) {}; QPolygonF drawTwoDLine( const QLineF &line, const QPen &pen, const ThreeDProperties &props ); QPolygonF drawThreeDLine( const QLineF &line, const QBrush &brush, const QPen &pen, const ThreeDProperties &props ); QPolygonF drawThreeDRect( const QRectF &rect, const QBrush &brush, const QPen &pen, const ThreeDProperties &props ); private: QPointF projectPoint( const QPointF &point, qreal depth, qreal angle ) const; QColor calcShadowColor( const QColor &color, qreal angle ) const; QPainter *painter; }; -/** +/* * Projects a point in 3D space * * @param depth The distance from the point and the projected point * @param angle The angle the projected point is rotated by around the original point */ QPointF StockDiagram::Private::ThreeDPainter::projectPoint( const QPointF &point, qreal depth, qreal angle ) const { const qreal angleInRad = DEGTORAD( angle ); const qreal distX = depth * cos( angleInRad ); // Y coordinates are reversed on our coordinate plane const qreal distY = depth * -sin( angleInRad ); return QPointF( point.x() + distX, point.y() + distY ); } -/** +/* * Returns the shadow color for a given color, depending on the angle of rotation * * @param color The color to calculate the shadow color for * @param angle The angle that the colored area is rotated by */ QColor StockDiagram::Private::ThreeDPainter::calcShadowColor( const QColor &color, qreal angle ) const { // The shadow factor determines to how many percent the brightness // of the color can be reduced. That is, the darkest shadow color // is color * shadowFactor. const qreal shadowFactor = 0.5; const qreal sinAngle = 1.0 - qAbs( sin( DEGTORAD( angle ) ) ) * shadowFactor; return QColor( qRound( color.red() * sinAngle ), qRound( color.green() * sinAngle ), qRound( color.blue() * sinAngle ) ); } -/** +/* * Draws a 2D line in 3D space by painting it with a z-coordinate of props.depth / 2.0 * * @param line The line to draw * @param pen The pen to use to draw the line * @param props The 3D properties to draw the line with * @return The drawn line, but with a width of 2px, as a polygon */ QPolygonF StockDiagram::Private::ThreeDPainter::drawTwoDLine( const QLineF &line, const QPen &pen, const ThreeDProperties &props ) { // Restores the painting properties when destroyed PainterSaver painterSaver( painter ); // The z coordinate to use (i.e., at what depth to draw the line) const qreal z = props.depth / 2.0; // Projec the 2D points of the line in 3D const QPointF deepP1 = projectPoint( line.p1(), z, props.angle ); const QPointF deepP2 = projectPoint( line.p2(), z, props.angle ); // The drawn line with a width of 2px QPolygonF threeDArea; // The offset of the line "borders" from the center to each side const QPointF offset( 0.0, 1.0 ); threeDArea << deepP1 - offset << deepP2 - offset << deepP1 + offset << deepP2 + offset << deepP1 - offset; painter->setPen( pen ); painter->drawLine( QLineF( deepP1, deepP2 ) ); return threeDArea; } -/** +/* * Draws an ordinary line in 3D by expanding it in the z-axis by the given depth. * * @param line The line to draw * @param brush The brush to fill the resulting polygon with * @param pen The pen to paint the borders of the resulting polygon with * @param props The 3D properties to draw the line with * @return The 3D shape drawn */ QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDLine( const QLineF &line, const QBrush &brush, const QPen &pen, const ThreeDProperties &props ) { // Restores the painting properties when destroyed PainterSaver painterSaver( painter ); const QPointF p1 = line.p1(); const QPointF p2 = line.p2(); // Project the 2D points of the line in 3D const QPointF deepP1 = projectPoint( p1, props.depth, props.angle ); const QPointF deepP2 = projectPoint( p2, props.depth, props.angle ); // The result is a 3D representation of the 2D line QPolygonF threeDArea; threeDArea << p1 << p2 << deepP2 << deepP1 << p1; // Use shadow colors if ThreeDProperties::useShadowColors is set // Note: Setting a new color on a brush or pen does not effect gradients or textures if ( props.useShadowColors ) { QBrush shadowBrush( brush ); QPen shadowPen( pen ); shadowBrush.setColor( calcShadowColor( brush.color(), props.angle ) ); shadowPen.setColor( calcShadowColor( pen.color(), props.angle ) ); painter->setBrush( shadowBrush ); painter->setPen( shadowPen ); } else { painter->setBrush( brush ); painter->setPen( pen ); } painter->drawPolygon( threeDArea ); return threeDArea; } -/** +/* * Draws a 3D cuboid by extending a 2D rectangle in the z-axis * * @param rect The rectangle to draw * @param brush The brush fill the surfaces of the cuboid with * @param pen The pen to draw the edges with * @param props The 3D properties to use for drawing the cuboid * @return The drawn cuboid as a polygon */ QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDRect( const QRectF &rect, const QBrush &brush, const QPen &pen, const ThreeDProperties &props ) { // Restores the painting properties when destroyed PainterSaver painterSaver( painter ); // Make sure that the top really is the top const QRectF normalizedRect = rect.normalized(); // Calculate all the four sides of the rectangle const QLineF topSide = QLineF( normalizedRect.topLeft(), normalizedRect.topRight() ); const QLineF bottomSide = QLineF( normalizedRect.bottomLeft(), normalizedRect.bottomRight() ); const QLineF leftSide = QLineF( normalizedRect.topLeft(), normalizedRect.bottomLeft() ); const QLineF rightSide = QLineF( normalizedRect.topRight(), normalizedRect.bottomRight() ); QPolygonF drawnPolygon; // Shorter names are easier on the eyes const qreal angle = props.angle; // Only top and right side is visible if ( angle >= 0.0 && angle < 90.0 ) { drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) ); drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) ); // Only top and left side is visible } else if ( angle >= 90.0 && angle < 180.0 ) { drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) ); drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) ); // Only bottom and left side is visible } else if ( angle >= 180.0 && angle < 270.0 ) { drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) ); drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) ); // Only bottom and right side is visible } else if ( angle >= 270.0 && angle <= 360.0 ) { drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) ); drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) ); } // Draw the front side painter->setPen( pen ); painter->setBrush( brush ); painter->drawRect( normalizedRect ); return drawnPolygon; } StockDiagram::Private::Private() : AbstractCartesianDiagram::Private() { } StockDiagram::Private::Private( const Private& r ) : AbstractCartesianDiagram::Private( r ) { } StockDiagram::Private::~Private() { } -/** +/* * Projects a point onto the coordinate plane * * @param context The context to paint the point in * @point The point to project onto the coordinate plane * @return The projected point */ QPointF StockDiagram::Private::projectPoint( PaintContext *context, const QPointF &point ) const { return context->coordinatePlane()->translate( QPointF( point.x() + 0.5, point.y() ) ); } -/** +/* * Projects a candlestick onto the coordinate plane * * @param context The context to paint the candlestick in * @param low The */ QRectF StockDiagram::Private::projectCandlestick( PaintContext *context, const QPointF &open, const QPointF &close, qreal width ) const { const QPointF leftHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 - width / 2.0, close.y() ) ); const QPointF rightLowPoint = context->coordinatePlane()->translate( QPointF( open.x() + 0.5 + width / 2.0, open.y() ) ); const QPointF rightHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 + width / 2.0, close.y() ) ); return QRectF( leftHighPoint, QSizeF( rightHighPoint.x() - leftHighPoint.x(), rightLowPoint.y() - leftHighPoint.y() ) ); } void StockDiagram::Private::drawOHLCBar( int dataset, const CartesianDiagramDataCompressor::DataPoint &open, const CartesianDiagramDataCompressor::DataPoint &high, const CartesianDiagramDataCompressor::DataPoint &low, const CartesianDiagramDataCompressor::DataPoint &close, PaintContext *context ) { // Note: A row in the model is a column in a StockDiagram const int col = low.index.row(); StockBarAttributes attr = stockDiagram()->stockBarAttributes( col ); ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col ); const qreal tickLength = attr.tickLength(); const QPointF leftOpenPoint( open.key + 0.5 - tickLength, open.value ); const QPointF rightOpenPoint( open.key + 0.5, open.value ); const QPointF highPoint( high.key + 0.5, high.value ); const QPointF lowPoint( low.key + 0.5, low.value ); const QPointF leftClosePoint( close.key + 0.5, close.value ); const QPointF rightClosePoint( close.key + 0.5 + tickLength, close.value ); bool reversedOrder = false; // If 3D mode is enabled, we have to make sure the z-order is right if ( threeDAttr.isEnabled() ) { const int angle = threeDAttr.angle(); // Z-order is from right to left if ( ( angle >= 0 && angle < 90 ) || ( angle >= 180 && angle < 270 ) ) reversedOrder = true; // Z-order is from left to right if ( ( angle >= 90 && angle < 180 ) || ( angle >= 270 && angle <= 360 ) ) reversedOrder = false; } if ( reversedOrder ) { if ( !open.hidden ) drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker if ( !low.hidden && !high.hidden ) drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line if ( !close.hidden ) drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker } else { if ( !close.hidden ) drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker if ( !low.hidden && !high.hidden ) drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line if ( !open.hidden ) drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker } LabelPaintCache lpc; if ( !open.hidden ) { addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr, PositionPoints( leftOpenPoint ), Position::South, Position::South, open.value ); } if ( !high.hidden ) { addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr, PositionPoints( highPoint ), Position::South, Position::South, high.value ); } if ( !low.hidden ) { addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr, PositionPoints( lowPoint ), Position::South, Position::South, low.value ); } if ( !close.hidden ) { addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr, PositionPoints( rightClosePoint ), Position::South, Position::South, close.value ); } paintDataValueTextsAndMarkers( context, lpc, false ); } -/** +/* * Draws a line connecting the low and the high value of an OHLC chart * * @param low The low data point * @param high The high data point * @param context The context to draw the candlestick in */ void StockDiagram::Private::drawCandlestick( int /*dataset*/, const CartesianDiagramDataCompressor::DataPoint &open, const CartesianDiagramDataCompressor::DataPoint &high, const CartesianDiagramDataCompressor::DataPoint &low, const CartesianDiagramDataCompressor::DataPoint &close, PaintContext *context ) { PainterSaver painterSaver( context->painter() ); // Note: A row in the model is a column in a StockDiagram, and the other way around const int row = low.index.row(); const int col = low.index.column(); QPointF bottomCandlestickPoint; QPointF topCandlestickPoint; QBrush brush; QPen pen; bool drawLowerLine; bool drawCandlestick = !open.hidden && !close.hidden; bool drawUpperLine; // Find out if we need to paint a down-trend or up-trend candlestick // and set brush and pen accordingly // Also, determine what the top and bottom points of the candlestick are if ( open.value <= close.value ) { pen = stockDiagram()->upTrendCandlestickPen( row ); brush = stockDiagram()->upTrendCandlestickBrush( row ); bottomCandlestickPoint = QPointF( open.key, open.value ); topCandlestickPoint = QPointF( close.key, close.value ); drawLowerLine = !low.hidden && !open.hidden; drawUpperLine = !low.hidden && !close.hidden; } else { pen = stockDiagram()->downTrendCandlestickPen( row ); brush = stockDiagram()->downTrendCandlestickBrush( row ); bottomCandlestickPoint = QPointF( close.key, close.value ); topCandlestickPoint = QPointF( open.key, open.value ); drawLowerLine = !low.hidden && !close.hidden; drawUpperLine = !low.hidden && !open.hidden; } StockBarAttributes attr = stockDiagram()->stockBarAttributes( col ); ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col ); const QPointF lowPoint = projectPoint( context, QPointF( low.key, low.value ) ); const QPointF highPoint = projectPoint( context, QPointF( high.key, high.value ) ); const QLineF lowerLine = QLineF( lowPoint, projectPoint( context, bottomCandlestickPoint ) ); const QLineF upperLine = QLineF( projectPoint( context, topCandlestickPoint ), highPoint ); // Convert the data point into coordinates on the coordinate plane QRectF candlestick = projectCandlestick( context, bottomCandlestickPoint, topCandlestickPoint, attr.candlestickWidth() ); // Remember the drawn polygon to add it to the ReverseMapper later QPolygonF drawnPolygon; // Use the ThreeDPainter class to draw a 3D candlestick if ( threeDAttr.isEnabled() ) { ThreeDPainter threeDPainter( context->painter() ); ThreeDPainter::ThreeDProperties threeDProps; threeDProps.depth = threeDAttr.depth(); threeDProps.angle = threeDAttr.angle(); threeDProps.useShadowColors = threeDAttr.useShadowColors(); // If the perspective angle is within [0,180], we paint from bottom to top, // otherwise from top to bottom to ensure the correct z order if ( threeDProps.angle > 0.0 && threeDProps.angle < 180.0 ) { if ( drawLowerLine ) drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps ); if ( drawCandlestick ) drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps ); if ( drawUpperLine ) drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps ); } else { if ( drawUpperLine ) drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps ); if ( drawCandlestick ) drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps ); if ( drawLowerLine ) drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps ); } } else { QPainter *const painter = context->painter(); painter->setBrush( brush ); painter->setPen( pen ); if ( drawLowerLine ) painter->drawLine( lowerLine ); if ( drawUpperLine ) painter->drawLine( upperLine ); if ( drawCandlestick ) painter->drawRect( candlestick ); // The 2D representation is the projected candlestick itself drawnPolygon = candlestick; // FIXME: Add lower and upper line to reverse mapper } LabelPaintCache lpc; if ( !low.hidden ) addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr, PositionPoints( lowPoint ), Position::South, Position::South, low.value ); if ( drawCandlestick ) { // Both, the open as well as the close value are represented by this candlestick reverseMapper.addPolygon( row, openValueColumn(), drawnPolygon ); reverseMapper.addPolygon( row, closeValueColumn(), drawnPolygon ); addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr, PositionPoints( candlestick.bottomRight() ), Position::South, Position::South, open.value ); addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr, PositionPoints( candlestick.topRight() ), Position::South, Position::South, close.value ); } if ( !high.hidden ) addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr, PositionPoints( highPoint ), Position::South, Position::South, high.value ); paintDataValueTextsAndMarkers( context, lpc, false ); } -/** +/* * Draws a line connecting two points * * @param col The column of the diagram to paint the line in * @param point1 The first point * @param point2 The second point * @param context The context to draw the low-high line in */ void StockDiagram::Private::drawLine( int dataset, int col, const QPointF &point1, const QPointF &point2, PaintContext *context ) { PainterSaver painterSaver( context->painter() ); // A row in the model is a column in the diagram const int modelRow = col; const int modelCol = 0; const QPen pen = diagram->pen( dataset ); const QBrush brush = diagram->brush( dataset ); const ThreeDBarAttributes threeDBarAttr = stockDiagram()->threeDBarAttributes( col ); QPointF transP1 = context->coordinatePlane()->translate( point1 ); QPointF transP2 = context->coordinatePlane()->translate( point2 ); QLineF line = QLineF( transP1, transP2 ); if ( threeDBarAttr.isEnabled() ) { ThreeDPainter::ThreeDProperties threeDProps; threeDProps.angle = threeDBarAttr.angle(); threeDProps.depth = threeDBarAttr.depth(); threeDProps.useShadowColors = threeDBarAttr.useShadowColors(); ThreeDPainter painter( context->painter() ); reverseMapper.addPolygon( modelCol, modelRow, painter.drawThreeDLine( line, brush, pen, threeDProps ) ); } else { context->painter()->setPen( pen ); //context->painter()->setBrush( brush ); reverseMapper.addLine( modelCol, modelRow, transP1, transP2 ); context->painter()->drawLine( line ); } } -/** +/* * Returns the column of the open value in the model * * @return The column of the open value */ int StockDiagram::Private::openValueColumn() const { // Return an invalid column if diagram has no open values return type == HighLowClose ? -1 : 0; } -/** +/* * Returns the column of the high value in the model * * @return The column of the high value */ int StockDiagram::Private::highValueColumn() const { return type == HighLowClose ? 0 : 1; } -/** +/* * Returns the column of the low value in the model * * @return The column of the low value */ int StockDiagram::Private::lowValueColumn() const { return type == HighLowClose ? 1 : 2; } -/** +/* * Returns the column of the close value in the model * * @return The column of the close value */ int StockDiagram::Private::closeValueColumn() const { return type == HighLowClose ? 2 : 3; } diff --git a/src/KChart/Cartesian/PaintingHelpers_p.cpp b/src/KChart/Cartesian/PaintingHelpers_p.cpp index b2dcbb1..49c03bb 100644 --- a/src/KChart/Cartesian/PaintingHelpers_p.cpp +++ b/src/KChart/Cartesian/PaintingHelpers_p.cpp @@ -1,299 +1,299 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PaintingHelpers_p.h" #include "KChartGlobal.h" #include "KChartAbstractDiagram.h" #include "KChartAbstractDiagram_p.h" #include "KChartCartesianCoordinatePlane.h" #include "KChartLineDiagram.h" #include "KChartValueTrackerAttributes.h" #include "KChartPaintContext.h" #include "KChartPainterSaver_p.h" #include "KChartPlotter.h" #include "KChartPrintingParameters.h" #include "KChartLineAttributes.h" #include "KChartThreeDLineAttributes.h" #include "ReverseMapper.h" namespace KChart { namespace PaintingHelpers { -/*! +/* Projects a point in a space defined by its x, y, and z coordinates into a point on a plane, given two rotation angles around the x resp. y axis. */ const QPointF project( const QPointF& point, const ThreeDLineAttributes& tdAttributes ) { //Pending Michel FIXME - the rotation does not work as expected atm qreal xrad = DEGTORAD( tdAttributes.lineXRotation() ); qreal yrad = DEGTORAD( tdAttributes.lineYRotation() ); return QPointF( point.x() * cos( yrad ) + tdAttributes.depth() * sin( yrad ), point.y() * cos( xrad ) - tdAttributes.depth() * sin( xrad ) ); } void paintPolyline( PaintContext* ctx, const QBrush& brush, const QPen& pen, const QPolygonF& points ) { ctx->painter()->setBrush( brush ); ctx->painter()->setPen( PrintingParameters::scalePen( QPen( pen.color(), pen.width(), pen.style(), Qt::FlatCap, Qt::MiterJoin ) ) ); ctx->painter()->drawPolyline( points ); } void paintThreeDLines( PaintContext* ctx, AbstractDiagram *diagram, const QModelIndex& index, const QPointF& from, const QPointF& to, const ThreeDLineAttributes& tdAttributes, ReverseMapper* reverseMapper ) { const QPointF topLeft = project( from, tdAttributes ); const QPointF topRight = project ( to, tdAttributes ); const QPolygonF segment = QPolygonF() << from << topLeft << topRight << to; QBrush indexBrush( diagram->brush( index ) ); indexBrush = tdAttributes.threeDBrush( indexBrush, QRectF(topLeft, topRight) ); const PainterSaver painterSaver( ctx->painter() ); ctx->painter()->setRenderHint( QPainter::Antialiasing, diagram->antiAliasing() ); ctx->painter()->setBrush( indexBrush ); ctx->painter()->setPen( PrintingParameters::scalePen( diagram->pen( index ) ) ); reverseMapper->addPolygon( index.row(), index.column(), segment ); ctx->painter()->drawPolygon( segment ); } void paintValueTracker( PaintContext* ctx, const ValueTrackerAttributes& vt, const QPointF& at ) { CartesianCoordinatePlane* plane = qobject_cast( ctx->coordinatePlane() ); if ( !plane ) return; DataDimensionsList gridDimensions = ctx->coordinatePlane()->gridDimensionsList(); const QPointF bottomLeft( ctx->coordinatePlane()->translate( QPointF( plane->isHorizontalRangeReversed() ? gridDimensions.at( 0 ).end : gridDimensions.at( 0 ).start, plane->isVerticalRangeReversed() ? gridDimensions.at( 1 ).end : gridDimensions.at( 1 ).start ) ) ); const QPointF topRight( ctx->coordinatePlane()->translate( QPointF( plane->isHorizontalRangeReversed() ? gridDimensions.at( 0 ).start : gridDimensions.at( 0 ).end, plane->isVerticalRangeReversed() ? gridDimensions.at( 1 ).start : gridDimensions.at( 1 ).end ) ) ); const QPointF markerPoint = at; QPointF startPoint; if ( vt.orientations() & Qt::Horizontal ) { startPoint = QPointF( bottomLeft.x(), at.y() ); } else { startPoint = QPointF( at.x(), topRight.y() ); } QPointF endPoint; if ( vt.orientations() & Qt::Vertical ) { endPoint = QPointF( at.x(), bottomLeft.y() ); } else { endPoint = QPointF( topRight.x(), at.y() ); } const QSizeF markerSize = vt.markerSize(); const QRectF ellipseMarker = QRectF( at.x() - markerSize.width() / 2, at.y() - markerSize.height() / 2, markerSize.width(), markerSize.height() ); QPointF startMarker[3]; if ( vt.orientations() & Qt::Horizontal ) { startMarker[0] = startPoint + QPointF( 0, markerSize.height() / 2 ); startMarker[1] = startPoint + QPointF( markerSize.width() / 2, 0 ); startMarker[2] = startPoint - QPointF( 0, markerSize.height() / 2 ); } else { startMarker[0] = startPoint + QPointF( 0, markerSize.height() / 2 ); startMarker[1] = startPoint + QPointF( markerSize.width() / 2, 0 ); startMarker[2] = startPoint - QPointF( markerSize.width() / 2, 0 ); } QPointF endMarker[3]; if ( vt.orientations() & Qt::Vertical ) { endMarker[0] = endPoint + QPointF( markerSize.width() / 2, 0 ); endMarker[1] = endPoint - QPointF( 0, markerSize.height() / 2 ); endMarker[2] = endPoint - QPointF( markerSize.width() / 2, 0 ); } else { endMarker[0] = endPoint + QPointF( 0, markerSize.width() / 2 ); endMarker[1] = endPoint - QPointF( 0, markerSize.height() / 2 ); endMarker[2] = endPoint - QPointF( markerSize.width() / 2, 0 ); } QPointF topLeft = startPoint; QPointF bottomRightOffset = endPoint - topLeft; QSizeF size( bottomRightOffset.x(), bottomRightOffset.y() ); QRectF area( topLeft, size ); PainterSaver painterSaver( ctx->painter() ); ctx->painter()->setPen( PrintingParameters::scalePen( vt.linePen() ) ); ctx->painter()->setBrush( QBrush() ); ctx->painter()->drawLine( markerPoint, startPoint ); ctx->painter()->drawLine( markerPoint, endPoint ); ctx->painter()->fillRect( area, vt.areaBrush() ); ctx->painter()->setPen( PrintingParameters::scalePen( vt.markerPen() ) ); ctx->painter()->setBrush( vt.markerBrush() ); ctx->painter()->drawEllipse( ellipseMarker ); ctx->painter()->setPen( PrintingParameters::scalePen( vt.arrowBrush().color() ) ); ctx->painter()->setBrush( vt.arrowBrush() ); ctx->painter()->drawPolygon( startMarker, 3 ); ctx->painter()->drawPolygon( endMarker, 3 ); } // ### for BC reasons we cannot insert a common interface for LineDiagram and Plotter into the class // hierarchy, so we have to use hacks to use their common methods static ThreeDLineAttributes threeDLineAttributes( AbstractDiagram* diagram, const QModelIndex& index ) { if ( Plotter *plotter = qobject_cast< Plotter* >( diagram ) ) { return plotter->threeDLineAttributes( index ); } else if ( LineDiagram *lineDiagram = qobject_cast< LineDiagram* >( diagram ) ) { return lineDiagram->threeDLineAttributes( index ); } Q_ASSERT( false ); return ThreeDLineAttributes(); } static LineAttributes lineAttributes( AbstractDiagram* diagram, const QModelIndex& index ) { if ( Plotter *plotter = qobject_cast< Plotter* >( diagram ) ) { return plotter->lineAttributes( index ); } else if ( LineDiagram *lineDiagram = qobject_cast< LineDiagram* >( diagram ) ) { return lineDiagram->lineAttributes( index ); } Q_ASSERT( false ); return LineAttributes(); } static ValueTrackerAttributes valueTrackerAttributes( AbstractDiagram* diagram, const QModelIndex& index ) { if ( Plotter *plotter = qobject_cast< Plotter* >( diagram ) ) { return plotter->valueTrackerAttributes( index ); } else if ( LineDiagram *lineDiagram = qobject_cast< LineDiagram* >( diagram ) ) { return lineDiagram->valueTrackerAttributes( index ); } Q_ASSERT( false ); return ValueTrackerAttributes(); } void paintElements( AbstractDiagram::Private *diagramPrivate, PaintContext* ctx, const LabelPaintCache& lpc, const LineAttributesInfoList& lineList ) { AbstractDiagram* diagram = diagramPrivate->diagram; // paint all lines and their attributes const PainterSaver painterSaver( ctx->painter() ); ctx->painter()->setRenderHint( QPainter::Antialiasing, diagram->antiAliasing() ); QBrush curBrush; QPen curPen; QPolygonF points; Q_FOREACH ( const LineAttributesInfo& lineInfo, lineList ) { const QModelIndex& index = lineInfo.index; const ThreeDLineAttributes td = threeDLineAttributes( diagram, index ); const LineAttributes la = lineAttributes( diagram, index ); if ( !la.isVisible() ) { // Do not draw lines, but do draw text and markers } else if( td.isEnabled() ){ PaintingHelpers::paintThreeDLines( ctx, diagram, index, lineInfo.value, lineInfo.nextValue, td, &diagramPrivate->reverseMapper ); } else { const QBrush brush( diagram->brush( index ) ); const QPen pen( diagram->pen( index ) ); // line goes from lineInfo.value to lineInfo.nextValue diagramPrivate->reverseMapper.addLine( lineInfo.index.row(), lineInfo.index.column(), lineInfo.value, lineInfo.nextValue ); if ( points.count() && points.last() == lineInfo.value && curBrush == brush && curPen == pen ) { // continue the current run of lines } else { // different painter settings or discontinuous line: start a new run of lines if ( points.count() ) { PaintingHelpers::paintPolyline( ctx, curBrush, curPen, points ); } curBrush = brush; curPen = pen; points.clear(); points << lineInfo.value; } points << lineInfo.nextValue; } } if ( points.count() ) { // the last run of lines is yet to be painted - do it now PaintingHelpers::paintPolyline( ctx, curBrush, curPen, points ); } Q_FOREACH ( const LineAttributesInfo& lineInfo, lineList ) { const ValueTrackerAttributes vt = valueTrackerAttributes( diagram, lineInfo.index ); if ( vt.isEnabled() ) { PaintingHelpers::paintValueTracker( ctx, vt, lineInfo.nextValue ); } } // paint all data value texts and the point markers diagramPrivate->paintDataValueTextsAndMarkers( ctx, lpc, true ); } void paintAreas( AbstractDiagram::Private* diagramPrivate, PaintContext* ctx, const QModelIndex& index, const QList< QPolygonF >& areas, uint opacity ) { AbstractDiagram* diagram = diagramPrivate->diagram; QPainterPath path; for ( int i = 0; i < areas.count(); ++i ) { const QPolygonF& p = areas[ i ]; path.addPolygon( p ); diagramPrivate->reverseMapper.addPolygon( index.row(), index.column(), p ); path.closeSubpath(); } ThreeDLineAttributes threeDAttrs = threeDLineAttributes( diagram, index ); QBrush trans = diagram->brush( index ); if ( threeDAttrs.isEnabled() ) { trans = threeDAttrs.threeDBrush( trans, path.boundingRect() ); } QColor transColor = trans.color(); transColor.setAlpha( opacity ); trans.setColor(transColor); QPen indexPen = diagram->pen(index); indexPen.setBrush( trans ); const PainterSaver painterSaver( ctx->painter() ); ctx->painter()->setRenderHint( QPainter::Antialiasing, diagram->antiAliasing() ); ctx->painter()->setPen( PrintingParameters::scalePen( indexPen ) ); ctx->painter()->setBrush( trans ); ctx->painter()->drawPath( path ); } } // namespace PaintingHelpers } // namespace KChart diff --git a/src/KChart/KChartAbstractDiagram_p.cpp b/src/KChart/KChartAbstractDiagram_p.cpp index ed4d691..92331d9 100644 --- a/src/KChart/KChartAbstractDiagram_p.cpp +++ b/src/KChart/KChartAbstractDiagram_p.cpp @@ -1,678 +1,678 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // // W A R N I N G // ------------- // // This file is not part of the KD Chart API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // #include "KChartAbstractDiagram_p.h" #include "KChartBarDiagram.h" #include "KChartFrameAttributes.h" #include "KChartPainterSaver_p.h" #include #include #include using namespace KChart; LabelPaintInfo::LabelPaintInfo() : isValuePositive( false ) { } LabelPaintInfo::LabelPaintInfo( const QModelIndex& _index, const DataValueAttributes& _attrs, const QPainterPath& _labelArea, const QPointF& _markerPos, bool _isValuePositive, const QString& _value ) : index( _index ) , attrs( _attrs ) , labelArea( _labelArea ) , markerPos( _markerPos ) , isValuePositive( _isValuePositive ) , value( _value ) { } LabelPaintInfo::LabelPaintInfo( const LabelPaintInfo& other ) : index( other.index ) , attrs( other.attrs ) , labelArea( other.labelArea ) , markerPos( other.markerPos ) , isValuePositive( other.isValuePositive ) , value( other.value ) { } AbstractDiagram::Private::Private() : diagram( nullptr ) , doDumpPaintTime( false ) , plane( nullptr ) , attributesModel( new PrivateAttributesModel(nullptr,nullptr) ) , allowOverlappingDataValueTexts( false ) , antiAliasing( true ) , percent( false ) , datasetDimension( 1 ) , databoundariesDirty( true ) , mCachedFontMetrics( QFontMetrics( qApp->font() ) ) { } AbstractDiagram::Private::~Private() { if ( attributesModel && qobject_cast(attributesModel) ) delete attributesModel; } void AbstractDiagram::Private::init() { } void AbstractDiagram::Private::init( AbstractCoordinatePlane* newPlane ) { plane = newPlane; } bool AbstractDiagram::Private::usesExternalAttributesModel() const { return ( ! attributesModel.isNull() ) && ( ! qobject_cast(attributesModel) ); } void AbstractDiagram::Private::setAttributesModel( AttributesModel* amodel ) { if ( attributesModel == amodel ) { return; } if ( !attributesModel.isNull() ) { if ( qobject_cast< PrivateAttributesModel* >( attributesModel ) ) { delete attributesModel; } else { disconnect( attributesModel, SIGNAL(rowsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(columnsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(modelReset()), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(layoutChanged()), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), diagram, SIGNAL(modelDataChanged())); } } emit diagram->attributesModelAboutToChange( amodel, attributesModel ); connect( amodel, SIGNAL(rowsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(columnsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(rowsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(columnsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(modelReset()), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(layoutChanged()), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), diagram, SIGNAL(modelDataChanged())); attributesModel = amodel; } AbstractDiagram::Private::Private( const AbstractDiagram::Private& rhs ) : diagram( nullptr ), doDumpPaintTime( rhs.doDumpPaintTime ), // Do not copy the plane plane( nullptr ), attributesModelRootIndex( QModelIndex() ), attributesModel( rhs.attributesModel ), allowOverlappingDataValueTexts( rhs.allowOverlappingDataValueTexts ), antiAliasing( rhs.antiAliasing ), percent( rhs.percent ), datasetDimension( rhs.datasetDimension ), mCachedFontMetrics( rhs.cachedFontMetrics() ) { attributesModel = new PrivateAttributesModel( nullptr, nullptr); attributesModel->initFrom( rhs.attributesModel ); } // FIXME: Optimize if necessary qreal AbstractDiagram::Private::calcPercentValue( const QModelIndex & index ) const { qreal sum = 0.0; for ( int col = 0; col < attributesModel->columnCount( QModelIndex() ); col++ ) sum += attributesModel->data( attributesModel->index( index.row(), col, QModelIndex() ) ).toReal(); // checked if ( sum == 0.0 ) return 0.0; return attributesModel->data( attributesModel->mapFromSource( index ) ).toReal() / sum * 100.0; } void AbstractDiagram::Private::addLabel( LabelPaintCache* cache, const QModelIndex& index, const CartesianDiagramDataCompressor::CachePosition* position, const PositionPoints& points, const Position& autoPositionPositive, const Position& autoPositionNegative, const qreal value, qreal favoriteAngle /* = 0.0 */ ) { CartesianDiagramDataCompressor::AggregatedDataValueAttributes allAttrs( aggregatedAttrs( index, position ) ); QMap::const_iterator it; for ( it = allAttrs.constBegin(); it != allAttrs.constEnd(); ++it ) { DataValueAttributes dva = it.value(); if ( !dva.isVisible() ) { continue; } const bool isPositive = ( value >= 0.0 ); RelativePosition relPos( dva.position( isPositive ) ); relPos.setReferencePoints( points ); if ( relPos.referencePosition().isUnknown() ) { relPos.setReferencePosition( isPositive ? autoPositionPositive : autoPositionNegative ); } // Rotate the label position (not the label itself) if the diagram is rotated so that the defaults still work if ( isTransposed() ) { KChartEnums::PositionValue posValue = relPos.referencePosition().value(); if ( posValue >= KChartEnums::PositionNorthWest && posValue <= KChartEnums::PositionWest ) { // rotate 90 degrees clockwise posValue = static_cast< KChartEnums::PositionValue >( posValue + 2 ); if ( posValue > KChartEnums::PositionWest ) { // wraparound posValue = static_cast< KChartEnums::PositionValue >( posValue - ( KChartEnums::PositionWest - KChartEnums::PositionNorthWest ) ); } relPos.setReferencePosition( Position( posValue ) ); } } const QPointF referencePoint = relPos.referencePoint(); if ( !diagram->coordinatePlane()->isVisiblePoint( referencePoint ) ) { continue; } const qreal fontHeight = cachedFontMetrics( dva.textAttributes(). calculatedFont( plane, KChartEnums::MeasureOrientationMinimum ), diagram )->height(); // Note: When printing data value texts and padding's Measure is using automatic reference area // detection, the font height is used as reference size for both horizontal and vertical // padding. QSizeF relativeMeasureSize( fontHeight, fontHeight ); if ( !dva.textAttributes().hasRotation() ) { TextAttributes ta = dva.textAttributes(); ta.setRotation( favoriteAngle ); dva.setTextAttributes( ta ); } // get the size of the label text using a subset of the information going into the final layout const QString text = formatDataValueText( dva, index, value ); QTextDocument doc; doc.setDocumentMargin( 0 ); if ( Qt::mightBeRichText( text ) ) { doc.setHtml( text ); } else { doc.setPlainText( text ); } const QFont calculatedFont( dva.textAttributes() .calculatedFont( plane, KChartEnums::MeasureOrientationMinimum ) ); doc.setDefaultFont( calculatedFont ); const QRectF plainRect = doc.documentLayout()->frameBoundingRect( doc.rootFrame() ); - /** + /* * A few hints on how the positioning of the text frame is done: * * Let's assume we have a bar chart, a text for a positive value * to be drawn, and "North" as attrs.positivePosition(). * * The reference point (pos) is then set to the top center point * of a bar. The offset now depends on the alignment: * * Top: text is centered horizontally to the bar, bottom of * text frame starts at top of bar * * Bottom: text is centered horizontally to the bar, top of * text frame starts at top of bar * * Center: text is centered horizontally to the bar, center * line of text frame is same as top of bar * * TopLeft: right edge of text frame is horizontal center of * bar, bottom of text frame is top of bar. * * ... * * Positive and negative value labels are treated equally, "North" * also refers to the top of a negative bar, and *not* to the bottom. * * * "NorthEast" likewise refers to the top right edge of the bar, * "NorthWest" to the top left edge of the bar, and so on. * * In other words, attrs.positivePosition() always refers to a * position of the *bar*, and relPos.alignment() always refers * to an alignment of the text frame relative to this position. */ QTransform transform; { // move to the general area where the label should be QPointF calcPoint = relPos.calculatedPoint( relativeMeasureSize ); transform.translate( calcPoint.x(), calcPoint.y() ); // align the text rect; find out by how many half-widths / half-heights to move. int dx = -1; if ( relPos.alignment() & Qt::AlignLeft ) { dx -= 1; } else if ( relPos.alignment() & Qt::AlignRight ) { dx += 1; } int dy = -1; if ( relPos.alignment() & Qt::AlignTop ) { dy -= 1; } else if ( relPos.alignment() & Qt::AlignBottom ) { dy += 1; } transform.translate( qreal( dx ) * plainRect.width() * 0.5, qreal( dy ) * plainRect.height() * 0.5 ); // rotate the text rect around its center transform.translate( plainRect.center().x(), plainRect.center().y() ); int rotation = dva.textAttributes().rotation(); if ( !isPositive && dva.mirrorNegativeValueTextRotation() ) { rotation *= -1; } transform.rotate( rotation ); transform.translate( -plainRect.center().x(), -plainRect.center().y() ); } QPainterPath labelArea; //labelArea.addPolygon( transform.mapToPolygon( plainRect.toRect() ) ); //labelArea.closeSubpath(); // Not doing that because QTransform has a special case for 180° that gives a different than // usual ordering of the points in the polygon returned by mapToPolygon( const QRect & ). // We expect a particular ordering in paintDataValueTextsAndMarkers() by using elementAt( 0 ), // and similar things might happen elsewhere. labelArea.addPolygon( transform.map( QPolygon( plainRect.toRect(), true ) ) ); // store the label geometry and auxiliary data cache->paintReplay.append( LabelPaintInfo( it.key(), dva, labelArea, referencePoint, value >= 0.0, text ) ); } } const QFontMetrics* AbstractDiagram::Private::cachedFontMetrics( const QFont& font, const QPaintDevice* paintDevice) const { if ( ( font != mCachedFont ) || ( paintDevice != mCachedPaintDevice ) ) { mCachedFontMetrics = QFontMetrics( font, const_cast( paintDevice ) ); // TODO what about setting mCachedFont and mCachedPaintDevice? } return &mCachedFontMetrics; } const QFontMetrics AbstractDiagram::Private::cachedFontMetrics() const { return mCachedFontMetrics; } QString AbstractDiagram::Private::formatNumber( qreal value, int decimalDigits ) const { const int digits = qMax(decimalDigits, 0); const qreal roundingEpsilon = pow( 0.1, digits ) * ( value >= 0.0 ? 0.5 : -0.5 ); QString asString = QString::number( value + roundingEpsilon, 'f' ); const int decimalPos = asString.indexOf( QLatin1Char( '.' ) ); if ( decimalPos < 0 ) { return asString; } int last = qMin( decimalPos + digits, asString.length() - 1 ); // remove trailing zeros (and maybe decimal dot) while ( last > decimalPos && asString[ last ] == QLatin1Char( '0' ) ) { last--; } if ( last == decimalPos ) { last--; } asString.chop( asString.length() - last - 1 ); return asString; } void AbstractDiagram::Private::forgetAlreadyPaintedDataValues() { alreadyDrawnDataValueTexts.clear(); prevPaintedDataValueText.clear(); } void AbstractDiagram::Private::paintDataValueTextsAndMarkers( PaintContext* ctx, const LabelPaintCache &cache, bool paintMarkers, bool justCalculateRect /* = false */, QRectF* cumulatedBoundingRect /* = 0 */ ) { if ( justCalculateRect && !cumulatedBoundingRect ) { qWarning() << Q_FUNC_INFO << "Neither painting nor finding the bounding rect, what are we doing?"; } const PainterSaver painterSaver( ctx->painter() ); ctx->painter()->setClipping( false ); if ( paintMarkers && !justCalculateRect ) { Q_FOREACH ( const LabelPaintInfo& info, cache.paintReplay ) { diagram->paintMarker( ctx->painter(), info.index, info.markerPos ); } } TextAttributes ta; { Measure m( 18.0, KChartEnums::MeasureCalculationModeRelative, KChartEnums::MeasureOrientationMinimum ); m.setReferenceArea( ctx->coordinatePlane() ); ta.setFontSize( m ); m.setAbsoluteValue( 6.0 ); ta.setMinimalFontSize( m ); } forgetAlreadyPaintedDataValues(); Q_FOREACH ( const LabelPaintInfo& info, cache.paintReplay ) { const QPointF pos = info.labelArea.elementAt( 0 ); paintDataValueText( ctx->painter(), info.attrs, pos, info.isValuePositive, info.value, justCalculateRect, cumulatedBoundingRect ); const QString comment = info.index.data( KChart::CommentRole ).toString(); if ( comment.isEmpty() ) { continue; } TextBubbleLayoutItem item( comment, ta, ctx->coordinatePlane()->parent(), KChartEnums::MeasureOrientationMinimum, Qt::AlignHCenter | Qt::AlignVCenter ); const QRect rect( pos.toPoint(), item.sizeHint() ); if (cumulatedBoundingRect) { (*cumulatedBoundingRect) |= rect; } if ( !justCalculateRect ) { item.setGeometry( rect ); item.paint( ctx->painter() ); } } if ( cumulatedBoundingRect ) { *cumulatedBoundingRect = ctx->painter()->transform().inverted().mapRect( *cumulatedBoundingRect ); } } QString AbstractDiagram::Private::formatDataValueText( const DataValueAttributes &dva, const QModelIndex& index, qreal value ) const { if ( !dva.isVisible() ) { return QString(); } if ( dva.usePercentage() ) { value = calcPercentValue( index ); } QString ret; if ( dva.dataLabel().isNull() ) { ret = formatNumber( value, dva.decimalDigits() ); } else { ret = dva.dataLabel(); } ret.prepend( dva.prefix() ); ret.append( dva.suffix() ); return ret; } void AbstractDiagram::Private::paintDataValueText( QPainter* painter, const QModelIndex& index, const QPointF& pos, qreal value, bool justCalculateRect /* = false */, QRectF* cumulatedBoundingRect /* = 0 */ ) { const DataValueAttributes dva( diagram->dataValueAttributes( index ) ); const QString text = formatDataValueText( dva, index, value ); paintDataValueText( painter, dva, pos, value >= 0.0, text, justCalculateRect, cumulatedBoundingRect ); } void AbstractDiagram::Private::paintDataValueText( QPainter* painter, const DataValueAttributes& attrs, const QPointF& pos, bool valueIsPositive, const QString& text, bool justCalculateRect /* = false */, QRectF* cumulatedBoundingRect /* = 0 */ ) { if ( !attrs.isVisible() ) { return; } const TextAttributes ta( attrs.textAttributes() ); if ( !ta.isVisible() || ( !attrs.showRepetitiveDataLabels() && prevPaintedDataValueText == text ) ) { return; } prevPaintedDataValueText = text; QTextDocument doc; doc.setDocumentMargin( 0.0 ); if ( Qt::mightBeRichText( text ) ) { doc.setHtml( text ); } else { doc.setPlainText( text ); } const QFont calculatedFont( ta.calculatedFont( plane, KChartEnums::MeasureOrientationMinimum ) ); const PainterSaver painterSaver( painter ); painter->setPen( PrintingParameters::scalePen( ta.pen() ) ); doc.setDefaultFont( calculatedFont ); QAbstractTextDocumentLayout::PaintContext context; context.palette = diagram->palette(); context.palette.setColor( QPalette::Text, ta.pen().color() ); QAbstractTextDocumentLayout* const layout = doc.documentLayout(); layout->setPaintDevice( painter->device() ); painter->translate( pos.x(), pos.y() ); int rotation = ta.rotation(); if ( !valueIsPositive && attrs.mirrorNegativeValueTextRotation() ) { rotation *= -1; } painter->rotate( rotation ); // do overlap detection "as seen by the painter" QTransform transform = painter->worldTransform(); bool drawIt = true; // note: This flag can be set differently for every label text! // In theory a user could e.g. have some small red text on one of the // values that she wants to have written in any case - so we just // do not test if such texts would cover some of the others. if ( !attrs.showOverlappingDataLabels() ) { const QRectF br( layout->frameBoundingRect( doc.rootFrame() ) ); QPolygon pr = transform.mapToPolygon( br.toRect() ); // Using QPainterPath allows us to use intersects() (which has many early-exits) // instead of QPolygon::intersected (which calculates a slow and precise intersection polygon) QPainterPath path; path.addPolygon( pr ); // iterate backwards because recently added items are more likely to overlap, so we spend // less time checking irrelevant items when there is overlap for ( int i = alreadyDrawnDataValueTexts.count() - 1; i >= 0; i-- ) { if ( alreadyDrawnDataValueTexts.at( i ).intersects( path ) ) { // qDebug() << "not painting this label due to overlap"; drawIt = false; break; } } if ( drawIt ) { alreadyDrawnDataValueTexts << path; } } if ( drawIt ) { QRectF rect = layout->frameBoundingRect( doc.rootFrame() ); if ( cumulatedBoundingRect ) { (*cumulatedBoundingRect) |= transform.mapRect( rect ); } if ( !justCalculateRect ) { bool paintBack = false; BackgroundAttributes back( attrs.backgroundAttributes() ); if ( back.isVisible() ) { paintBack = true; painter->setBrush( back.brush() ); } else { painter->setBrush( QBrush() ); } qreal radius = 0.0; FrameAttributes frame( attrs.frameAttributes() ); if ( frame.isVisible() ) { paintBack = true; painter->setPen( frame.pen() ); radius = frame.cornerRadius(); } if ( paintBack ) { QRectF borderRect( QPointF( 0, 0 ), rect.size() ); painter->drawRoundedRect( borderRect, radius, radius ); } layout->draw( painter, context ); } } } QModelIndex AbstractDiagram::Private::indexAt( const QPoint& point ) const { QModelIndexList l = indexesAt( point ); std::sort(l.begin(), l.end()); if ( !l.isEmpty() ) return l.first(); else return QModelIndex(); } QModelIndexList AbstractDiagram::Private::indexesAt( const QPoint& point ) const { return reverseMapper.indexesAt( point ); // which could be empty } QModelIndexList AbstractDiagram::Private::indexesIn( const QRect& rect ) const { return reverseMapper.indexesIn( rect ); } CartesianDiagramDataCompressor::AggregatedDataValueAttributes AbstractDiagram::Private::aggregatedAttrs( const QModelIndex& index, const CartesianDiagramDataCompressor::CachePosition* position ) const { Q_UNUSED( position ); // used by cartesian diagrams only CartesianDiagramDataCompressor::AggregatedDataValueAttributes allAttrs; allAttrs[index] = diagram->dataValueAttributes( index ); return allAttrs; } void AbstractDiagram::Private::setDatasetAttrs( int dataset, const QVariant& data, int role ) { // To store attributes for a dataset, we use the first column // that's associated with it. (i.e., with a dataset dimension // of two, the column of the keys). In most cases however, there's // only one data dimension, and thus also only one column per data set. int column = dataset * datasetDimension; // For DataHiddenRole, also store the flag in the other data points that belong to this data set, // otherwise it's impossible to hide data points in a plotter diagram because there will always // be one model index that belongs to this data point that is not hidden. // For more details on how hiding works, see the data compressor. // Also see KDCH-503 for which this is a workaround. int columnSpan = role == DataHiddenRole ? datasetDimension : 1; for ( int i = 0; i < columnSpan; i++ ) { attributesModel->setHeaderData( column + i, Qt::Horizontal, data, role ); } } QVariant AbstractDiagram::Private::datasetAttrs( int dataset, int role ) const { // See setDataSetAttrs for explanation of column int column = dataset * datasetDimension; return attributesModel->headerData( column, Qt::Horizontal, role ); } void AbstractDiagram::Private::resetDatasetAttrs( int dataset, int role ) { // See setDataSetAttrs for explanation of column int column = dataset * datasetDimension; attributesModel->resetHeaderData( column, Qt::Horizontal, role ); } bool AbstractDiagram::Private::isTransposed() const { // Determine the diagram that specifies the orientation. // That diagram is the reference diagram, if it exists, or otherwise the diagram itself. // Note: In KChart 2.3 or earlier, only a bar diagram can be transposed. const AbstractCartesianDiagram* refDiagram = qobject_cast< const AbstractCartesianDiagram * >( diagram ); if ( !refDiagram ) { return false; } if ( refDiagram->referenceDiagram() ) { refDiagram = refDiagram->referenceDiagram(); } const BarDiagram* barDiagram = qobject_cast< const BarDiagram* >( refDiagram ); if ( !barDiagram ) { return false; } return barDiagram->orientation() == Qt::Horizontal; } LineAttributesInfo::LineAttributesInfo() { } LineAttributesInfo::LineAttributesInfo( const QModelIndex& _index, const QPointF& _value, const QPointF& _nextValue ) : index( _index ) , value ( _value ) , nextValue ( _nextValue ) { } diff --git a/src/KChart/KChartChart.cpp b/src/KChart/KChartChart.cpp index fe3babc..dae6e2c 100644 --- a/src/KChart/KChartChart.cpp +++ b/src/KChart/KChartChart.cpp @@ -1,1773 +1,1773 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "KChartChart.h" #include "KChartChart_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "KChartCartesianCoordinatePlane.h" #include "KChartAbstractCartesianDiagram.h" #include "KChartHeaderFooter.h" #include "KChartEnums.h" #include "KChartLegend.h" #include "KChartLayoutItems.h" #include #include #include "KChartPainterSaver_p.h" #include "KChartPrintingParameters.h" #include #if defined KDAB_EVAL #include "../evaldialog/evaldialog.h" #endif #if 0 // dumpLayoutTree dumps a QLayout tree in a hopefully easy to read format to stderr - feel free to // use, improve and extend; it is very useful for looking at any layout problem. #include // this is this different from both QRect::isEmpty() and QRect::isNull() for "wrong" QRects, // i.e. those where topLeft() is actually below and / or right of bottomRight(). static bool isZeroArea(const QRect &r) { return !r.width() || !r.height(); } static QString lineProlog(int nestingDepth, int lineno) { QString numbering(QString::number(lineno).rightJustified(5).append(QChar::fromAscii(':'))); QString indent(nestingDepth * 4, QLatin1Char(' ')); return numbering + indent; } static void dumpLayoutTreeRecurse(QLayout *l, int *counter, int depth) { const QLatin1String colorOn(isZeroArea(l->geometry()) ? "\033[0m" : "\033[32m"); const QLatin1String colorOff("\033[0m"); QString prolog = lineProlog(depth, *counter); (*counter)++; qDebug() << colorOn + prolog << l->metaObject()->className() << l->geometry() << "hint" << l->sizeHint() << l->hasHeightForWidth() << "min" << l->minimumSize() << "max" << l->maximumSize() << l->expandingDirections() << l->alignment() << colorOff; for (int i = 0; i < l->count(); i++) { QLayoutItem *child = l->itemAt(i); if (QLayout *childL = child->layout()) { dumpLayoutTreeRecurse(childL, counter, depth + 1); } else { // The isZeroArea check culls usually largely useless output - you might want to remove it in // some debugging situations. Add a boolean parameter to this and dumpLayoutTree() if you do. if (!isZeroArea(child->geometry())) { prolog = lineProlog(depth + 1, *counter); (*counter)++; qDebug() << colorOn + prolog << typeid(*child).name() << child->geometry() << "hint" << child->sizeHint() << child->hasHeightForWidth() << "min" << child->minimumSize() << "max" << child->maximumSize() << child->expandingDirections() << child->alignment() << colorOff; } } } } static void dumpLayoutTree(QLayout *l) { int counter = 0; dumpLayoutTreeRecurse(l, &counter, 0); } #endif static const Qt::Alignment s_gridAlignments[ 3 ][ 3 ] = { // [ row ][ column ] { Qt::AlignTop | Qt::AlignLeft, Qt::AlignTop | Qt::AlignHCenter, Qt::AlignTop | Qt::AlignRight }, { Qt::AlignVCenter | Qt::AlignLeft, Qt::AlignVCenter | Qt::AlignHCenter, Qt::AlignVCenter | Qt::AlignRight }, { Qt::AlignBottom | Qt::AlignLeft, Qt::AlignBottom | Qt::AlignHCenter, Qt::AlignBottom | Qt::AlignRight } }; static void getRowAndColumnForPosition(KChartEnums::PositionValue pos, int* row, int* column) { switch ( pos ) { case KChartEnums::PositionNorthWest: *row = 0; *column = 0; break; case KChartEnums::PositionNorth: *row = 0; *column = 1; break; case KChartEnums::PositionNorthEast: *row = 0; *column = 2; break; case KChartEnums::PositionEast: *row = 1; *column = 2; break; case KChartEnums::PositionSouthEast: *row = 2; *column = 2; break; case KChartEnums::PositionSouth: *row = 2; *column = 1; break; case KChartEnums::PositionSouthWest: *row = 2; *column = 0; break; case KChartEnums::PositionWest: *row = 1; *column = 0; break; case KChartEnums::PositionCenter: *row = 1; *column = 1; break; default: *row = -1; *column = -1; break; } } using namespace KChart; // Layout widgets even if they are not visible (that's why isEmpty() is overridden) - at least that // was the original reason... class MyWidgetItem : public QWidgetItem { public: explicit MyWidgetItem(QWidget *w, Qt::Alignment alignment = Qt::Alignment()) : QWidgetItem( w ) { setAlignment( alignment ); } // All of the methods are reimplemented from QWidgetItem, and work around some oddity in QLayout and / or // KD Chart - I forgot the details between writing this code as an experiment and committing it, very // sorry about that! // Feel free to comment out any of them and then try the line-breaking feature in horizontal legends in // the Legends/Advanced example. It will not work well in various ways - won't get enough space and look // very broken, will inhibit resizing the window etc. QSize sizeHint() const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); return w->sizeHint(); } QSize minimumSize() const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); return w->minimumSize(); } QSize maximumSize() const override { // Not just passing on w->maximumSize() fixes that the size policy of Legend is disregarded, making // Legend take all available space, which makes both the Legend internal layout and the overall // layout of chart + legend look bad. QWidget::maximumSize() is not a virtual method, it's a // property, so "overriding" that one would be even uglier, and prevent user-set property // values from doing anything. QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); QSize ret = w->maximumSize(); const QSize hint = w->sizeHint(); const QSizePolicy::Policy hPolicy = w->sizePolicy().horizontalPolicy(); if (hPolicy == QSizePolicy::Fixed || hPolicy == QSizePolicy::Maximum) { ret.rwidth() = hint.width(); } const QSizePolicy::Policy vPolicy = w->sizePolicy().verticalPolicy(); if (vPolicy == QSizePolicy::Fixed || vPolicy == QSizePolicy::Maximum) { ret.rheight() = hint.height(); } return ret; } Qt::Orientations expandingDirections() const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); if ( isEmpty() ) { return Qt::Orientations(); } Qt::Orientations e = w->sizePolicy().expandingDirections(); return e; } void setGeometry(const QRect &g) override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); w->setGeometry(g); } QRect geometry() const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); return w->geometry(); } bool hasHeightForWidth() const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); bool ret = !isEmpty() && qobject_cast< Legend* >( w )->hasHeightForWidth(); return ret; } int heightForWidth( int width ) const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); int ret = w->heightForWidth( width ); return ret; } bool isEmpty() const override { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); // legend->hide() should indeed hide the legend, // but a legend in a chart that hasn't been shown yet isn't hidden // (as can happen when using Chart::paint() without showing the chart) return w->isHidden() && w->testAttribute( Qt::WA_WState_ExplicitShowHide ); } }; // When "abusing" QLayouts to lay out items with different geometry from the backing QWidgets, // some manual work is required to correctly update all the sublayouts. // This is because all the convenient ways to deal with QLayouts assume QWidgets somewhere. // What this does is somewhat similar to QLayout::activate(), but it never refers to the parent // QWidget which has the wrong geometry. static void invalidateLayoutTree( QLayoutItem *item ) { QLayout *layout = item->layout(); if ( layout ) { const int count = layout->count(); for ( int i = 0; i < count; i++ ) { invalidateLayoutTree( layout->itemAt( i ) ); } } item->invalidate(); } void Chart::Private::slotUnregisterDestroyedLegend( Legend *l ) { chart->takeLegend( l ); } void Chart::Private::slotUnregisterDestroyedHeaderFooter( HeaderFooter* hf ) { chart->takeHeaderFooter( hf ); } void Chart::Private::slotUnregisterDestroyedPlane( AbstractCoordinatePlane* plane ) { coordinatePlanes.removeAll( plane ); Q_FOREACH ( AbstractCoordinatePlane* p, coordinatePlanes ) { if ( p->referenceCoordinatePlane() == plane) { p->setReferenceCoordinatePlane( nullptr ); } } plane->layoutPlanes(); } Chart::Private::Private( Chart* chart_ ) : chart( chart_ ) , useNewLayoutSystem( false ) , layout(nullptr) , vLayout(nullptr) , planesLayout(nullptr) , headerLayout(nullptr) , footerLayout(nullptr) , dataAndLegendLayout(nullptr) , leftOuterSpacer(nullptr) , rightOuterSpacer(nullptr) , topOuterSpacer(nullptr) , bottomOuterSpacer(nullptr) , isFloatingLegendsLayoutDirty( true ) , isPlanesLayoutDirty( true ) , globalLeadingLeft(0) , globalLeadingRight(0) , globalLeadingTop(0) , globalLeadingBottom(0) { for ( int row = 0; row < 3; ++row ) { for ( int column = 0; column < 3; ++column ) { for ( int i = 0; i < 2; i++ ) { innerHdFtLayouts[ i ][ row ][ column ] = nullptr; } } } } Chart::Private::~Private() { } enum VisitorState{ Visited, Unknown }; struct ConnectedComponentsComparator{ bool operator()( const LayoutGraphNode *lhs, const LayoutGraphNode *rhs ) const { return lhs->priority < rhs->priority; } }; static QVector< LayoutGraphNode* > getPrioritySortedConnectedComponents( QVector< LayoutGraphNode* > &nodeList ) { QVector< LayoutGraphNode* >connectedComponents; QHash< LayoutGraphNode*, VisitorState > visitedComponents; Q_FOREACH ( LayoutGraphNode* node, nodeList ) visitedComponents[ node ] = Unknown; for ( int i = 0; i < nodeList.size(); ++i ) { LayoutGraphNode *curNode = nodeList[ i ]; LayoutGraphNode *representativeNode = curNode; if ( visitedComponents[ curNode ] != Visited ) { QStack< LayoutGraphNode* > stack; stack.push( curNode ); while ( !stack.isEmpty() ) { curNode = stack.pop(); Q_ASSERT( visitedComponents[ curNode ] != Visited ); visitedComponents[ curNode ] = Visited; if ( curNode->bottomSuccesor && visitedComponents[ curNode->bottomSuccesor ] != Visited ) stack.push( curNode->bottomSuccesor ); if ( curNode->leftSuccesor && visitedComponents[ curNode->leftSuccesor ] != Visited ) stack.push( curNode->leftSuccesor ); if ( curNode->sharedSuccesor && visitedComponents[ curNode->sharedSuccesor ] != Visited ) stack.push( curNode->sharedSuccesor ); if ( curNode->priority < representativeNode->priority ) representativeNode = curNode; } connectedComponents.append( representativeNode ); } } std::sort( connectedComponents.begin(), connectedComponents.end(), ConnectedComponentsComparator() ); return connectedComponents; } struct PriorityComparator{ public: PriorityComparator( QHash< AbstractCoordinatePlane*, LayoutGraphNode* > mapping ) : m_mapping( mapping ) {} bool operator() ( AbstractCoordinatePlane *lhs, AbstractCoordinatePlane *rhs ) const { const LayoutGraphNode *lhsNode = m_mapping[ lhs ]; Q_ASSERT( lhsNode ); const LayoutGraphNode *rhsNode = m_mapping[ rhs ]; Q_ASSERT( rhsNode ); return lhsNode->priority < rhsNode->priority; } const QHash< AbstractCoordinatePlane*, LayoutGraphNode* > m_mapping; }; void checkExistingAxes( LayoutGraphNode* node ) { if ( node && node->diagramPlane && node->diagramPlane->diagram() ) { AbstractCartesianDiagram *diag = qobject_cast< AbstractCartesianDiagram* >( node->diagramPlane->diagram() ); if ( diag ) { Q_FOREACH( const CartesianAxis* axis, diag->axes() ) { switch ( axis->position() ) { case( CartesianAxis::Top ): node->topAxesLayout = true; break; case( CartesianAxis::Bottom ): node->bottomAxesLayout = true; break; case( CartesianAxis::Left ): node->leftAxesLayout = true; break; case( CartesianAxis::Right ): node->rightAxesLayout = true; break; } } } } } static void mergeNodeAxisInformation( LayoutGraphNode* lhs, LayoutGraphNode* rhs ) { lhs->topAxesLayout |= rhs->topAxesLayout; rhs->topAxesLayout = lhs->topAxesLayout; lhs->bottomAxesLayout |= rhs->bottomAxesLayout; rhs->bottomAxesLayout = lhs->bottomAxesLayout; lhs->leftAxesLayout |= rhs->leftAxesLayout; rhs->leftAxesLayout = lhs->leftAxesLayout; lhs->rightAxesLayout |= rhs->rightAxesLayout; rhs->rightAxesLayout = lhs->rightAxesLayout; } static CoordinatePlaneList findSharingAxisDiagrams( AbstractCoordinatePlane* plane, const CoordinatePlaneList& list, Chart::Private::AxisType type, QVector< CartesianAxis* >* sharedAxes ) { if ( !plane || !plane->diagram() ) return CoordinatePlaneList(); Q_ASSERT( plane ); Q_ASSERT( plane->diagram() ); CoordinatePlaneList result; AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* >( plane->diagram() ); if ( !diagram ) return CoordinatePlaneList(); QList< CartesianAxis* > axes; Q_FOREACH( CartesianAxis* axis, diagram->axes() ) { if ( ( type == Chart::Private::Ordinate && ( axis->position() == CartesianAxis::Left || axis->position() == CartesianAxis::Right ) ) || ( type == Chart::Private::Abscissa && ( axis->position() == CartesianAxis::Top || axis->position() == CartesianAxis::Bottom ) ) ) { axes.append( axis ); } } Q_FOREACH( AbstractCoordinatePlane *curPlane, list ) { AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* > ( curPlane->diagram() ); if ( !diagram ) continue; Q_FOREACH( CartesianAxis* curSearchedAxis, axes ) { Q_FOREACH( CartesianAxis* curAxis, diagram->axes() ) { if ( curSearchedAxis == curAxis ) { result.append( curPlane ); if ( !sharedAxes->contains( curSearchedAxis ) ) sharedAxes->append( curSearchedAxis ); } } } } return result; } -/** +/* * this method determines the needed layout of the graph * taking care of the sharing problematic * its NOT allowed to have a diagram that shares * more than one axis in the same direction */ QVector< LayoutGraphNode* > Chart::Private::buildPlaneLayoutGraph() { QHash< AbstractCoordinatePlane*, LayoutGraphNode* > planeNodeMapping; QVector< LayoutGraphNode* > allNodes; // create all nodes and a mapping between plane and nodes Q_FOREACH( AbstractCoordinatePlane* curPlane, coordinatePlanes ) { if ( curPlane->diagram() ) { allNodes.append( new LayoutGraphNode ); allNodes[ allNodes.size() - 1 ]->diagramPlane = curPlane; allNodes[ allNodes.size() - 1 ]->priority = allNodes.size(); checkExistingAxes( allNodes[ allNodes.size() - 1 ] ); planeNodeMapping[ curPlane ] = allNodes[ allNodes.size() - 1 ]; } } // build the graph connections Q_FOREACH( LayoutGraphNode* curNode, allNodes ) { QVector< CartesianAxis* > sharedAxes; CoordinatePlaneList xSharedPlanes = findSharingAxisDiagrams( curNode->diagramPlane, coordinatePlanes, Abscissa, &sharedAxes ); Q_ASSERT( sharedAxes.size() < 2 ); // TODO duplicated code make a method out of it if ( sharedAxes.size() == 1 && xSharedPlanes.size() > 1 ) { //xSharedPlanes.removeAll( sharedAxes.first()->diagram()->coordinatePlane() ); //std::sort( xSharedPlanes.begin(), xSharedPlanes.end(), PriorityComparator( planeNodeMapping ) ); for ( int i = 0; i < xSharedPlanes.size() - 1; ++i ) { LayoutGraphNode *tmpNode = planeNodeMapping[ xSharedPlanes[ i ] ]; Q_ASSERT( tmpNode ); LayoutGraphNode *tmpNode2 = planeNodeMapping[ xSharedPlanes[ i + 1 ] ]; Q_ASSERT( tmpNode2 ); tmpNode->bottomSuccesor = tmpNode2; } // if ( sharedAxes.first()->diagram() && sharedAxes.first()->diagram()->coordinatePlane() ) // { // LayoutGraphNode *lastNode = planeNodeMapping[ xSharedPlanes.last() ]; // Q_ASSERT( lastNode ); // Q_ASSERT( sharedAxes.first()->diagram()->coordinatePlane() ); // LayoutGraphNode *ownerNode = planeNodeMapping[ sharedAxes.first()->diagram()->coordinatePlane() ]; // Q_ASSERT( ownerNode ); // lastNode->bottomSuccesor = ownerNode; // } //merge AxisInformation, needs a two pass run LayoutGraphNode axisInfoNode; for ( int count = 0; count < 2; ++count ) { for ( int i = 0; i < xSharedPlanes.size(); ++i ) { mergeNodeAxisInformation( &axisInfoNode, planeNodeMapping[ xSharedPlanes[ i ] ] ); } } } sharedAxes.clear(); CoordinatePlaneList ySharedPlanes = findSharingAxisDiagrams( curNode->diagramPlane, coordinatePlanes, Ordinate, &sharedAxes ); Q_ASSERT( sharedAxes.size() < 2 ); if ( sharedAxes.size() == 1 && ySharedPlanes.size() > 1 ) { //ySharedPlanes.removeAll( sharedAxes.first()->diagram()->coordinatePlane() ); //std::sort( ySharedPlanes.begin(), ySharedPlanes.end(), PriorityComparator( planeNodeMapping ) ); for ( int i = 0; i < ySharedPlanes.size() - 1; ++i ) { LayoutGraphNode *tmpNode = planeNodeMapping[ ySharedPlanes[ i ] ]; Q_ASSERT( tmpNode ); LayoutGraphNode *tmpNode2 = planeNodeMapping[ ySharedPlanes[ i + 1 ] ]; Q_ASSERT( tmpNode2 ); tmpNode->leftSuccesor = tmpNode2; } // if ( sharedAxes.first()->diagram() && sharedAxes.first()->diagram()->coordinatePlane() ) // { // LayoutGraphNode *lastNode = planeNodeMapping[ ySharedPlanes.last() ]; // Q_ASSERT( lastNode ); // Q_ASSERT( sharedAxes.first()->diagram()->coordinatePlane() ); // LayoutGraphNode *ownerNode = planeNodeMapping[ sharedAxes.first()->diagram()->coordinatePlane() ]; // Q_ASSERT( ownerNode ); // lastNode->bottomSuccesor = ownerNode; // } //merge AxisInformation, needs a two pass run LayoutGraphNode axisInfoNode; for ( int count = 0; count < 2; ++count ) { for ( int i = 0; i < ySharedPlanes.size(); ++i ) { mergeNodeAxisInformation( &axisInfoNode, planeNodeMapping[ ySharedPlanes[ i ] ] ); } } } sharedAxes.clear(); if ( curNode->diagramPlane->referenceCoordinatePlane() ) curNode->sharedSuccesor = planeNodeMapping[ curNode->diagramPlane->referenceCoordinatePlane() ]; } return allNodes; } QHash Chart::Private::buildPlaneLayoutInfos() { /* There are two ways in which planes can be caused to interact in * where they are put layouting wise: The first is the reference plane. If * such a reference plane is set, on a plane, it will use the same cell in the * layout as that one. In addition to this, planes can share an axis. In that case * they will be laid out in relation to each other as suggested by the position * of the axis. If, for example Plane1 and Plane2 share an axis at position Left, * that will result in the layout: Axis Plane1 Plane 2, vertically. If Plane1 * also happens to be Plane2's referece plane, both planes are drawn over each * other. The reference plane concept allows two planes to share the same space * even if neither has any axis, and in case there are shared axis, it is used * to decided, whether the planes should be painted on top of each other or * laid out vertically or horizontally next to each other. */ QHash axisInfos; QHash planeInfos; Q_FOREACH(AbstractCoordinatePlane* plane, coordinatePlanes ) { PlaneInfo p; // first check if we share space with another plane p.referencePlane = plane->referenceCoordinatePlane(); planeInfos.insert( plane, p ); Q_FOREACH( AbstractDiagram* abstractDiagram, plane->diagrams() ) { AbstractCartesianDiagram* diagram = qobject_cast ( abstractDiagram ); if ( !diagram ) { continue; } Q_FOREACH( CartesianAxis* axis, diagram->axes() ) { if ( !axisInfos.contains( axis ) ) { /* If this is the first time we see this axis, add it, with the * current plane. The first plane added to the chart that has * the axis associated with it thus "owns" it, and decides about * layout. */ AxisInfo i; i.plane = plane; axisInfos.insert( axis, i ); } else { AxisInfo i = axisInfos[axis]; if ( i.plane == plane ) { continue; // we don't want duplicates, only shared } /* The user expects diagrams to be added on top, and to the right * so that horizontally we need to move the new diagram, vertically * the reference one. */ PlaneInfo pi = planeInfos[plane]; // plane-to-plane linking overrides linking via axes if ( !pi.referencePlane ) { // we're not the first plane to see this axis, mark us as a slave pi.referencePlane = i.plane; if ( axis->position() == CartesianAxis::Left || axis->position() == CartesianAxis::Right ) { pi.horizontalOffset += 1; } planeInfos[plane] = pi; pi = planeInfos[i.plane]; if ( axis->position() == CartesianAxis::Top || axis->position() == CartesianAxis::Bottom ) { pi.verticalOffset += 1; } planeInfos[i.plane] = pi; } } } } // Create a new grid layout for each plane that has no reference. p = planeInfos[plane]; if ( p.referencePlane == nullptr ) { p.gridLayout = new QGridLayout(); p.gridLayout->setContentsMargins( 0, 0, 0, 0 ); planeInfos[plane] = p; } } return planeInfos; } void Chart::Private::slotLayoutPlanes() { /*TODO make sure this is really needed */ const QBoxLayout::Direction oldPlanesDirection = planesLayout ? planesLayout->direction() : QBoxLayout::TopToBottom; if ( planesLayout && dataAndLegendLayout ) dataAndLegendLayout->removeItem( planesLayout ); const bool hadPlanesLayout = planesLayout != nullptr; int left, top, right, bottom; if ( hadPlanesLayout ) planesLayout->getContentsMargins(&left, &top, &right, &bottom); Q_FOREACH( AbstractLayoutItem* plane, planeLayoutItems ) { plane->removeFromParentLayout(); } //TODO they should get a correct parent, but for now it works Q_FOREACH( AbstractLayoutItem* plane, planeLayoutItems ) { if ( dynamic_cast< AutoSpacerLayoutItem* >( plane ) ) delete plane; } planeLayoutItems.clear(); delete planesLayout; //hint: The direction is configurable by the user now, as // we are using a QBoxLayout rather than a QVBoxLayout. (khz, 2007/04/25) planesLayout = new QBoxLayout( oldPlanesDirection ); isPlanesLayoutDirty = true; // here we create the layouts; we need to "run" them before painting if ( useNewLayoutSystem ) { gridPlaneLayout = new QGridLayout; planesLayout->addLayout( gridPlaneLayout ); if (hadPlanesLayout) planesLayout->setContentsMargins(left, top, right, bottom); planesLayout->setObjectName( QString::fromLatin1( "planesLayout" ) ); /* First go through all planes and all axes and figure out whether the planes * need to coordinate. If they do, they share a grid layout, if not, each * get their own. See buildPlaneLayoutInfos() for more details. */ QVector< LayoutGraphNode* > vals = buildPlaneLayoutGraph(); //qDebug() << Q_FUNC_INFO << "GraphNodes" << vals.size(); QVector< LayoutGraphNode* > connectedComponents = getPrioritySortedConnectedComponents( vals ); //qDebug() << Q_FUNC_INFO << "SubGraphs" << connectedComponents.size(); int row = 0; int col = 0; QSet< CartesianAxis* > laidOutAxes; for ( int i = 0; i < connectedComponents.size(); ++i ) { LayoutGraphNode *curComponent = connectedComponents[ i ]; for ( LayoutGraphNode *curRowComponent = curComponent; curRowComponent; curRowComponent = curRowComponent->bottomSuccesor ) { col = 0; for ( LayoutGraphNode *curColComponent = curRowComponent; curColComponent; curColComponent = curColComponent->leftSuccesor ) { Q_ASSERT( curColComponent->diagramPlane->diagrams().size() == 1 ); Q_FOREACH( AbstractDiagram* diagram, curColComponent->diagramPlane->diagrams() ) { const int planeRowOffset = 1;//curColComponent->topAxesLayout ? 1 : 0; const int planeColOffset = 1;//curColComponent->leftAxesLayout ? 1 : 0; //qDebug() << Q_FUNC_INFO << row << col << planeRowOffset << planeColOffset; //qDebug() << Q_FUNC_INFO << row + planeRowOffset << col + planeColOffset; planeLayoutItems << curColComponent->diagramPlane; AbstractCartesianDiagram *cartDiag = qobject_cast< AbstractCartesianDiagram* >( diagram ); if ( cartDiag ) { gridPlaneLayout->addItem( curColComponent->diagramPlane, row + planeRowOffset, col + planeColOffset, 2, 2 ); curColComponent->diagramPlane->setParentLayout( gridPlaneLayout ); QHBoxLayout *leftLayout = nullptr; QHBoxLayout *rightLayout = nullptr; QVBoxLayout *topLayout = nullptr; QVBoxLayout *bottomLayout = nullptr; if ( curComponent->sharedSuccesor ) { gridPlaneLayout->addItem( curColComponent->sharedSuccesor->diagramPlane, row + planeRowOffset, col + planeColOffset, 2, 2 ); curColComponent->sharedSuccesor->diagramPlane->setParentLayout( gridPlaneLayout ); planeLayoutItems << curColComponent->sharedSuccesor->diagramPlane; } Q_FOREACH( CartesianAxis* axis, cartDiag->axes() ) { if ( axis->isAbscissa() ) { if ( curColComponent->bottomSuccesor ) continue; } if ( laidOutAxes.contains( axis ) ) continue; // if ( axis->diagram() != diagram ) // continue; switch ( axis->position() ) { case( CartesianAxis::Top ): if ( !topLayout ) topLayout = new QVBoxLayout; topLayout->addItem( axis ); axis->setParentLayout( topLayout ); break; case( CartesianAxis::Bottom ): if ( !bottomLayout ) bottomLayout = new QVBoxLayout; bottomLayout->addItem( axis ); axis->setParentLayout( bottomLayout ); break; case( CartesianAxis::Left ): if ( !leftLayout ) leftLayout = new QHBoxLayout; leftLayout->addItem( axis ); axis->setParentLayout( leftLayout ); break; case( CartesianAxis::Right ): if ( !rightLayout ) { rightLayout = new QHBoxLayout; } rightLayout->addItem( axis ); axis->setParentLayout( rightLayout ); break; } planeLayoutItems << axis; laidOutAxes.insert( axis ); } if ( leftLayout ) gridPlaneLayout->addLayout( leftLayout, row + planeRowOffset, col, 2, 1, Qt::AlignRight | Qt::AlignVCenter ); if ( rightLayout ) gridPlaneLayout->addLayout( rightLayout, row, col + planeColOffset + 2, 2, 1, Qt::AlignLeft | Qt::AlignVCenter ); if ( topLayout ) gridPlaneLayout->addLayout( topLayout, row, col + planeColOffset, 1, 2, Qt::AlignBottom | Qt::AlignHCenter ); if ( bottomLayout ) gridPlaneLayout->addLayout( bottomLayout, row + planeRowOffset + 2, col + planeColOffset, 1, 2, Qt::AlignTop | Qt::AlignHCenter ); } else { gridPlaneLayout->addItem( curColComponent->diagramPlane, row, col, 4, 4 ); curColComponent->diagramPlane->setParentLayout( gridPlaneLayout ); } col += planeColOffset + 2 + ( 1 ); } } int axisOffset = 2;//curRowComponent->topAxesLayout ? 1 : 0; //axisOffset += curRowComponent->bottomAxesLayout ? 1 : 0; const int rowOffset = axisOffset + 2; row += rowOffset; } // if ( planesLayout->direction() == QBoxLayout::TopToBottom ) // ++row; // else // ++col; } qDeleteAll( vals ); // re-add our grid(s) to the chart's layout if ( dataAndLegendLayout ) { dataAndLegendLayout->addLayout( planesLayout, 1, 1 ); dataAndLegendLayout->setRowStretch( 1, 1000 ); dataAndLegendLayout->setColumnStretch( 1, 1000 ); } slotResizePlanes(); #ifdef NEW_LAYOUT_DEBUG for ( int i = 0; i < gridPlaneLayout->rowCount(); ++i ) { for ( int j = 0; j < gridPlaneLayout->columnCount(); ++j ) { if ( gridPlaneLayout->itemAtPosition( i, j ) ) qDebug() << Q_FUNC_INFO << "item at" << i << j << gridPlaneLayout->itemAtPosition( i, j )->geometry(); else qDebug() << Q_FUNC_INFO << "item at" << i << j << "no item present"; } } //qDebug() << Q_FUNC_INFO << "Relayout ended"; #endif } else { if ( hadPlanesLayout ) { planesLayout->setContentsMargins( left, top, right, bottom ); } planesLayout->setContentsMargins( 0, 0, 0, 0 ); planesLayout->setSpacing( 0 ); planesLayout->setObjectName( QString::fromLatin1( "planesLayout" ) ); /* First go through all planes and all axes and figure out whether the planes * need to coordinate. If they do, they share a grid layout, if not, each * gets their own. See buildPlaneLayoutInfos() for more details. */ QHash planeInfos = buildPlaneLayoutInfos(); QHash axisInfos; Q_FOREACH( AbstractCoordinatePlane* plane, coordinatePlanes ) { Q_ASSERT( planeInfos.contains(plane) ); PlaneInfo& pi = planeInfos[ plane ]; const int column = pi.horizontalOffset; const int row = pi.verticalOffset; //qDebug() << "processing plane at column" << column << "and row" << row; QGridLayout *planeLayout = pi.gridLayout; if ( !planeLayout ) { PlaneInfo& refPi = pi; // if this plane is sharing an axis with another one, recursively check for the original plane and use // the grid of that as planeLayout. while ( !planeLayout && refPi.referencePlane ) { refPi = planeInfos[refPi.referencePlane]; planeLayout = refPi.gridLayout; } Q_ASSERT_X( planeLayout, "Chart::Private::slotLayoutPlanes()", "Invalid reference plane. Please check that the reference plane has been added to the Chart." ); } else { planesLayout->addLayout( planeLayout ); } /* Put the plane in the center of the layout. If this is our own, that's * the middle of the layout, if we are sharing, it's a cell in the center * column of the shared grid. */ planeLayoutItems << plane; plane->setParentLayout( planeLayout ); planeLayout->addItem( plane, row, column, 1, 1 ); //qDebug() << "Chart slotLayoutPlanes() calls planeLayout->addItem("<< row << column << ")"; planeLayout->setRowStretch( row, 2 ); planeLayout->setColumnStretch( column, 2 ); Q_FOREACH( AbstractDiagram* abstractDiagram, plane->diagrams() ) { AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* >( abstractDiagram ); if ( !diagram ) { continue; // FIXME what about polar ? } if ( pi.referencePlane != nullptr ) { pi.topAxesLayout = planeInfos[ pi.referencePlane ].topAxesLayout; pi.bottomAxesLayout = planeInfos[ pi.referencePlane ].bottomAxesLayout; pi.leftAxesLayout = planeInfos[ pi.referencePlane ].leftAxesLayout; pi.rightAxesLayout = planeInfos[ pi.referencePlane ].rightAxesLayout; } // collect all axes of a kind into sublayouts if ( pi.topAxesLayout == nullptr ) { pi.topAxesLayout = new QVBoxLayout; pi.topAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.topAxesLayout->setObjectName( QString::fromLatin1( "topAxesLayout" ) ); } if ( pi.bottomAxesLayout == nullptr ) { pi.bottomAxesLayout = new QVBoxLayout; pi.bottomAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.bottomAxesLayout->setObjectName( QString::fromLatin1( "bottomAxesLayout" ) ); } if ( pi.leftAxesLayout == nullptr ) { pi.leftAxesLayout = new QHBoxLayout; pi.leftAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.leftAxesLayout->setObjectName( QString::fromLatin1( "leftAxesLayout" ) ); } if ( pi.rightAxesLayout == nullptr ) { pi.rightAxesLayout = new QHBoxLayout; pi.rightAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.rightAxesLayout->setObjectName( QString::fromLatin1( "rightAxesLayout" ) ); } if ( pi.referencePlane != nullptr ) { planeInfos[ pi.referencePlane ].topAxesLayout = pi.topAxesLayout; planeInfos[ pi.referencePlane ].bottomAxesLayout = pi.bottomAxesLayout; planeInfos[ pi.referencePlane ].leftAxesLayout = pi.leftAxesLayout; planeInfos[ pi.referencePlane ].rightAxesLayout = pi.rightAxesLayout; } //pi.leftAxesLayout->setSizeConstraint( QLayout::SetFixedSize ); Q_FOREACH( CartesianAxis* axis, diagram->axes() ) { if ( axisInfos.contains( axis ) ) { continue; // already laid out this one } Q_ASSERT ( axis ); axis->setCachedSizeDirty(); //qDebug() << "--------------- axis added to planeLayoutItems -----------------"; planeLayoutItems << axis; switch ( axis->position() ) { case CartesianAxis::Top: axis->setParentLayout( pi.topAxesLayout ); pi.topAxesLayout->addItem( axis ); break; case CartesianAxis::Bottom: axis->setParentLayout( pi.bottomAxesLayout ); pi.bottomAxesLayout->addItem( axis ); break; case CartesianAxis::Left: axis->setParentLayout( pi.leftAxesLayout ); pi.leftAxesLayout->addItem( axis ); break; case CartesianAxis::Right: axis->setParentLayout( pi.rightAxesLayout ); pi.rightAxesLayout->addItem( axis ); break; default: Q_ASSERT_X( false, "Chart::paintEvent", "unknown axis position" ); break; }; axisInfos.insert( axis, AxisInfo() ); } /* Put each stack of axes-layouts in the cells surrounding the * associated plane. We are laying out in the oder the planes * were added, and the first one gets to lay out shared axes. * Private axes go here as well, of course. */ if ( !pi.topAxesLayout->parent() ) { planeLayout->addLayout( pi.topAxesLayout, row - 1, column ); } if ( !pi.bottomAxesLayout->parent() ) { planeLayout->addLayout( pi.bottomAxesLayout, row + 1, column ); } if ( !pi.leftAxesLayout->parent() ) { planeLayout->addLayout( pi.leftAxesLayout, row, column - 1 ); } if ( !pi.rightAxesLayout->parent() ) { planeLayout->addLayout( pi.rightAxesLayout,row, column + 1 ); } } // use up to four auto-spacer items in the corners around the diagrams: #define ADD_AUTO_SPACER_IF_NEEDED( \ spacerRow, spacerColumn, hLayoutIsAtTop, hLayout, vLayoutIsAtLeft, vLayout ) \ { \ if ( hLayout || vLayout ) { \ AutoSpacerLayoutItem * spacer \ = new AutoSpacerLayoutItem( hLayoutIsAtTop, hLayout, vLayoutIsAtLeft, vLayout ); \ planeLayout->addItem( spacer, spacerRow, spacerColumn, 1, 1 ); \ spacer->setParentLayout( planeLayout ); \ planeLayoutItems << spacer; \ } \ } if ( plane->isCornerSpacersEnabled() ) { ADD_AUTO_SPACER_IF_NEEDED( row - 1, column - 1, false, pi.leftAxesLayout, false, pi.topAxesLayout ) ADD_AUTO_SPACER_IF_NEEDED( row + 1, column - 1, true, pi.leftAxesLayout, false, pi.bottomAxesLayout ) ADD_AUTO_SPACER_IF_NEEDED( row - 1, column + 1, false, pi.rightAxesLayout, true, pi.topAxesLayout ) ADD_AUTO_SPACER_IF_NEEDED( row + 1, column + 1, true, pi.rightAxesLayout, true, pi.bottomAxesLayout ) } } // re-add our grid(s) to the chart's layout if ( dataAndLegendLayout ) { dataAndLegendLayout->addLayout( planesLayout, 1, 1 ); dataAndLegendLayout->setRowStretch( 1, 1000 ); dataAndLegendLayout->setColumnStretch( 1, 1000 ); } slotResizePlanes(); } } void Chart::Private::createLayouts() { // The toplevel layout provides the left and right global margins layout = new QHBoxLayout( chart ); layout->setContentsMargins( 0, 0, 0, 0 ); layout->setObjectName( QString::fromLatin1( "Chart::Private::layout" ) ); layout->addSpacing( globalLeadingLeft ); leftOuterSpacer = layout->itemAt( layout->count() - 1 )->spacerItem(); // The vLayout provides top and bottom global margins and lays // out headers, footers and the diagram area. vLayout = new QVBoxLayout(); vLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->setObjectName( QString::fromLatin1( "vLayout" ) ); layout->addLayout( vLayout, 1000 ); layout->addSpacing( globalLeadingRight ); rightOuterSpacer = layout->itemAt( layout->count() - 1 )->spacerItem(); // 1. the spacing above the header area vLayout->addSpacing( globalLeadingTop ); topOuterSpacer = vLayout->itemAt( vLayout->count() - 1 )->spacerItem(); // 2. the header area headerLayout = new QGridLayout(); headerLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->addLayout( headerLayout ); // 3. the area containing coordinate planes, axes, and legends dataAndLegendLayout = new QGridLayout(); dataAndLegendLayout->setContentsMargins( 0, 0, 0, 0 ); dataAndLegendLayout->setObjectName( QString::fromLatin1( "dataAndLegendLayout" ) ); vLayout->addLayout( dataAndLegendLayout, 1000 ); // 4. the footer area footerLayout = new QGridLayout(); footerLayout->setContentsMargins( 0, 0, 0, 0 ); footerLayout->setObjectName( QString::fromLatin1( "footerLayout" ) ); vLayout->addLayout( footerLayout ); // 5. Prepare the header / footer layout cells: // Each of the 9 header cells (the 9 footer cells) // contain their own QVBoxLayout // since there can be more than one header (footer) per cell. for ( int row = 0; row < 3; ++row ) { for ( int column = 0; column < 3; ++ column ) { const Qt::Alignment align = s_gridAlignments[ row ][ column ]; for ( int headOrFoot = 0; headOrFoot < 2; headOrFoot++ ) { QVBoxLayout* innerLayout = new QVBoxLayout(); innerLayout->setContentsMargins( 0, 0, 0, 0 ); innerLayout->setAlignment( align ); innerHdFtLayouts[ headOrFoot ][ row ][ column ] = innerLayout; QGridLayout* outerLayout = headOrFoot == 0 ? headerLayout : footerLayout; outerLayout->addLayout( innerLayout, row, column, align ); } } } // 6. the spacing below the footer area vLayout->addSpacing( globalLeadingBottom ); bottomOuterSpacer = vLayout->itemAt( vLayout->count() - 1 )->spacerItem(); // the data+axes area dataAndLegendLayout->addLayout( planesLayout, 1, 1 ); dataAndLegendLayout->setRowStretch( 1, 1 ); dataAndLegendLayout->setColumnStretch( 1, 1 ); } void Chart::Private::slotResizePlanes() { if ( !dataAndLegendLayout ) { return; } if ( !overrideSize.isValid() ) { // activate() takes the size from the layout's parent QWidget, which is not updated when overrideSize // is set. So don't let the layout grab the wrong size in that case. // When overrideSize *is* set, we call layout->setGeometry() in paint( QPainter*, const QRect& ), // which also "activates" the layout in the sense that it distributes space internally. layout->activate(); } // Adapt diagram drawing to the new size Q_FOREACH (AbstractCoordinatePlane* plane, coordinatePlanes ) { plane->layoutDiagrams(); } } void Chart::Private::updateDirtyLayouts() { if ( isPlanesLayoutDirty ) { Q_FOREACH ( AbstractCoordinatePlane* p, coordinatePlanes ) { p->setGridNeedsRecalculate(); p->layoutPlanes(); p->layoutDiagrams(); } } if ( isPlanesLayoutDirty || isFloatingLegendsLayoutDirty ) { chart->reLayoutFloatingLegends(); } isPlanesLayoutDirty = false; isFloatingLegendsLayoutDirty = false; } void Chart::Private::reapplyInternalLayouts() { QRect geo = layout->geometry(); invalidateLayoutTree( layout ); layout->setGeometry( geo ); slotResizePlanes(); } void Chart::Private::paintAll( QPainter* painter ) { updateDirtyLayouts(); QRect rect( QPoint( 0, 0 ), overrideSize.isValid() ? overrideSize : chart->size() ); //qDebug() << this<<"::paintAll() uses layout size" << currentLayoutSize; // Paint the background (if any) AbstractAreaBase::paintBackgroundAttributes( *painter, rect, backgroundAttributes ); // Paint the frame (if any) AbstractAreaBase::paintFrameAttributes( *painter, rect, frameAttributes ); chart->reLayoutFloatingLegends(); Q_FOREACH( AbstractLayoutItem* planeLayoutItem, planeLayoutItems ) { planeLayoutItem->paintAll( *painter ); } Q_FOREACH( TextArea* textLayoutItem, textLayoutItems ) { textLayoutItem->paintAll( *painter ); } Q_FOREACH( Legend *legend, legends ) { const bool hidden = legend->isHidden() && legend->testAttribute( Qt::WA_WState_ExplicitShowHide ); if ( !hidden ) { //qDebug() << "painting legend at " << legend->geometry(); legend->paintIntoRect( *painter, legend->geometry() ); } } } // ******** Chart interface implementation *********** #define d d_func() Chart::Chart ( QWidget* parent ) : QWidget ( parent ) , _d( new Private( this ) ) { #if defined KDAB_EVAL EvalDialog::checkEvalLicense( "KD Chart" ); #endif FrameAttributes frameAttrs; // no frame per default... // frameAttrs.setVisible( true ); frameAttrs.setPen( QPen( Qt::black ) ); frameAttrs.setPadding( 1 ); setFrameAttributes( frameAttrs ); addCoordinatePlane( new CartesianCoordinatePlane ( this ) ); d->createLayouts(); } Chart::~Chart() { delete d; } void Chart::setFrameAttributes( const FrameAttributes &a ) { d->frameAttributes = a; } FrameAttributes Chart::frameAttributes() const { return d->frameAttributes; } void Chart::setBackgroundAttributes( const BackgroundAttributes &a ) { d->backgroundAttributes = a; } BackgroundAttributes Chart::backgroundAttributes() const { return d->backgroundAttributes; } //TODO KChart 3.0; change QLayout into QBoxLayout::Direction void Chart::setCoordinatePlaneLayout( QLayout * layout ) { if (layout == d->planesLayout) return; if (d->planesLayout) { // detach all QLayoutItem's the previous planesLayout has cause // otherwise deleting the planesLayout would delete them too. for(int i = d->planesLayout->count() - 1; i >= 0; --i) { d->planesLayout->takeAt(i); } delete d->planesLayout; } d->planesLayout = qobject_cast( layout ); d->slotLayoutPlanes(); } QLayout* Chart::coordinatePlaneLayout() { return d->planesLayout; } AbstractCoordinatePlane* Chart::coordinatePlane() { if ( d->coordinatePlanes.isEmpty() ) { qWarning() << "Chart::coordinatePlane: warning: no coordinate plane defined."; return nullptr; } else { return d->coordinatePlanes.first(); } } CoordinatePlaneList Chart::coordinatePlanes() { return d->coordinatePlanes; } void Chart::addCoordinatePlane( AbstractCoordinatePlane* plane ) { // Append insertCoordinatePlane( d->coordinatePlanes.count(), plane ); } void Chart::insertCoordinatePlane( int index, AbstractCoordinatePlane* plane ) { if ( index < 0 || index > d->coordinatePlanes.count() ) { return; } connect( plane, SIGNAL(destroyedCoordinatePlane(AbstractCoordinatePlane*)), d, SLOT(slotUnregisterDestroyedPlane(AbstractCoordinatePlane*)) ); connect( plane, SIGNAL(needUpdate()), this, SLOT(update()) ); connect( plane, SIGNAL(needRelayout()), d, SLOT(slotResizePlanes()) ) ; connect( plane, SIGNAL(needLayoutPlanes()), d, SLOT(slotLayoutPlanes()) ) ; connect( plane, SIGNAL(propertiesChanged()),this, SIGNAL(propertiesChanged()) ); d->coordinatePlanes.insert( index, plane ); plane->setParent( this ); d->slotLayoutPlanes(); } void Chart::replaceCoordinatePlane( AbstractCoordinatePlane* plane, AbstractCoordinatePlane* oldPlane_ ) { if ( plane && oldPlane_ != plane ) { AbstractCoordinatePlane* oldPlane = oldPlane_; if ( d->coordinatePlanes.count() ) { if ( ! oldPlane ) { oldPlane = d->coordinatePlanes.first(); if ( oldPlane == plane ) return; } takeCoordinatePlane( oldPlane ); } delete oldPlane; addCoordinatePlane( plane ); } } void Chart::takeCoordinatePlane( AbstractCoordinatePlane* plane ) { const int idx = d->coordinatePlanes.indexOf( plane ); if ( idx != -1 ) { d->coordinatePlanes.takeAt( idx ); disconnect( plane, nullptr, d, nullptr ); disconnect( plane, nullptr, this, nullptr ); plane->removeFromParentLayout(); plane->setParent( nullptr ); d->mouseClickedPlanes.removeAll(plane); } d->slotLayoutPlanes(); // Need to emit the signal: In case somebody has connected the signal // to her own slot for e.g. calling update() on a widget containing the chart. emit propertiesChanged(); } void Chart::setGlobalLeading( int left, int top, int right, int bottom ) { setGlobalLeadingLeft( left ); setGlobalLeadingTop( top ); setGlobalLeadingRight( right ); setGlobalLeadingBottom( bottom ); } void Chart::setGlobalLeadingLeft( int leading ) { d->globalLeadingLeft = leading; d->leftOuterSpacer->changeSize( leading, 0, QSizePolicy::Fixed, QSizePolicy::Minimum ); d->reapplyInternalLayouts(); } int Chart::globalLeadingLeft() const { return d->globalLeadingLeft; } void Chart::setGlobalLeadingTop( int leading ) { d->globalLeadingTop = leading; d->topOuterSpacer->changeSize( 0, leading, QSizePolicy::Minimum, QSizePolicy::Fixed ); d->reapplyInternalLayouts(); } int Chart::globalLeadingTop() const { return d->globalLeadingTop; } void Chart::setGlobalLeadingRight( int leading ) { d->globalLeadingRight = leading; d->rightOuterSpacer->changeSize( leading, 0, QSizePolicy::Fixed, QSizePolicy::Minimum ); d->reapplyInternalLayouts(); } int Chart::globalLeadingRight() const { return d->globalLeadingRight; } void Chart::setGlobalLeadingBottom( int leading ) { d->globalLeadingBottom = leading; d->bottomOuterSpacer->changeSize( 0, leading, QSizePolicy::Minimum, QSizePolicy::Fixed ); d->reapplyInternalLayouts(); } int Chart::globalLeadingBottom() const { return d->globalLeadingBottom; } void Chart::paint( QPainter* painter, const QRect& rect ) { if ( rect.isEmpty() || !painter ) { return; } QPaintDevice* prevDevice = GlobalMeasureScaling::paintDevice(); GlobalMeasureScaling::setPaintDevice( painter->device() ); int prevScaleFactor = PrintingParameters::scaleFactor(); PrintingParameters::setScaleFactor( qreal( painter->device()->logicalDpiX() ) / qreal( logicalDpiX() ) ); const QRect oldGeometry( geometry() ); if ( oldGeometry != rect ) setGeometry( rect ); painter->translate( rect.left(), rect.top() ); d->paintAll( painter ); // for debugging // painter->setPen( QPen( Qt::blue, 8 ) ); // painter->drawRect( rect ); painter->translate( -rect.left(), -rect.top() ); if ( oldGeometry != rect ) setGeometry( oldGeometry ); PrintingParameters::setScaleFactor( prevScaleFactor ); GlobalMeasureScaling::setPaintDevice( prevDevice ); } void Chart::resizeEvent ( QResizeEvent* event ) { d->isPlanesLayoutDirty = true; d->isFloatingLegendsLayoutDirty = true; QWidget::resizeEvent( event ); } void Chart::reLayoutFloatingLegends() { Q_FOREACH( Legend *legend, d->legends ) { const bool hidden = legend->isHidden() && legend->testAttribute( Qt::WA_WState_ExplicitShowHide ); if ( legend->position().isFloating() && !hidden ) { // resize the legend const QSize legendSize( legend->sizeHint() ); legend->setGeometry( QRect( legend->geometry().topLeft(), legendSize ) ); // find the legends corner point (reference point plus any paddings) const RelativePosition relPos( legend->floatingPosition() ); QPointF pt( relPos.calculatedPoint( size() ) ); //qDebug() << pt; // calculate the legend's top left point const Qt::Alignment alignTopLeft = Qt::AlignBottom | Qt::AlignLeft; if ( (relPos.alignment() & alignTopLeft) != alignTopLeft ) { if ( relPos.alignment() & Qt::AlignRight ) pt.rx() -= legendSize.width(); else if ( relPos.alignment() & Qt::AlignHCenter ) pt.rx() -= 0.5 * legendSize.width(); if ( relPos.alignment() & Qt::AlignBottom ) pt.ry() -= legendSize.height(); else if ( relPos.alignment() & Qt::AlignVCenter ) pt.ry() -= 0.5 * legendSize.height(); } //qDebug() << pt << endl; legend->move( static_cast(pt.x()), static_cast(pt.y()) ); } } } void Chart::paintEvent( QPaintEvent* ) { QPainter painter( this ); d->paintAll( &painter ); emit finishedDrawing(); } void Chart::addHeaderFooter( HeaderFooter* hf ) { Q_ASSERT( hf->type() == HeaderFooter::Header || hf->type() == HeaderFooter::Footer ); int row; int column; getRowAndColumnForPosition( hf->position().value(), &row, &column ); if ( row == -1 ) { qWarning( "Unknown header/footer position" ); return; } d->headerFooters.append( hf ); d->textLayoutItems.append( hf ); connect( hf, SIGNAL(destroyedHeaderFooter(HeaderFooter*)), d, SLOT(slotUnregisterDestroyedHeaderFooter(HeaderFooter*)) ); connect( hf, SIGNAL(positionChanged(HeaderFooter*)), d, SLOT(slotHeaderFooterPositionChanged(HeaderFooter*)) ); // set the text attributes (why?) TextAttributes textAttrs( hf->textAttributes() ); Measure measure( textAttrs.fontSize() ); measure.setRelativeMode( this, KChartEnums::MeasureOrientationMinimum ); measure.setValue( 20 ); textAttrs.setFontSize( measure ); hf->setTextAttributes( textAttrs ); // add it to the appropriate layout int innerLayoutIdx = hf->type() == HeaderFooter::Header ? 0 : 1; QVBoxLayout* headerFooterLayout = d->innerHdFtLayouts[ innerLayoutIdx ][ row ][ column ]; hf->setParentLayout( headerFooterLayout ); hf->setAlignment( s_gridAlignments[ row ][ column ] ); headerFooterLayout->addItem( hf ); d->slotResizePlanes(); } void Chart::replaceHeaderFooter( HeaderFooter* headerFooter, HeaderFooter* oldHeaderFooter_ ) { if ( headerFooter && oldHeaderFooter_ != headerFooter ) { HeaderFooter* oldHeaderFooter = oldHeaderFooter_; if ( d->headerFooters.count() ) { if ( ! oldHeaderFooter ) { oldHeaderFooter = d->headerFooters.first(); if ( oldHeaderFooter == headerFooter ) return; } takeHeaderFooter( oldHeaderFooter ); } delete oldHeaderFooter; addHeaderFooter( headerFooter ); } } void Chart::takeHeaderFooter( HeaderFooter* headerFooter ) { const int idx = d->headerFooters.indexOf( headerFooter ); if ( idx == -1 ) { return; } disconnect( headerFooter, SIGNAL(destroyedHeaderFooter(HeaderFooter*)), d, SLOT(slotUnregisterDestroyedHeaderFooter(HeaderFooter*)) ); d->headerFooters.takeAt( idx ); headerFooter->removeFromParentLayout(); headerFooter->setParentLayout( nullptr ); d->textLayoutItems.remove( d->textLayoutItems.indexOf( headerFooter ) ); d->slotResizePlanes(); } void Chart::Private::slotHeaderFooterPositionChanged( HeaderFooter* hf ) { chart->takeHeaderFooter( hf ); chart->addHeaderFooter( hf ); } HeaderFooter* Chart::headerFooter() { if ( d->headerFooters.isEmpty() ) { return nullptr; } else { return d->headerFooters.first(); } } HeaderFooterList Chart::headerFooters() { return d->headerFooters; } void Chart::Private::slotLegendPositionChanged( AbstractAreaWidget* aw ) { Legend* legend = qobject_cast< Legend* >( aw ); Q_ASSERT( legend ); chart->takeLegend( legend ); chart->addLegendInternal( legend, false ); } void Chart::addLegend( Legend* legend ) { legend->show(); addLegendInternal( legend, true ); emit propertiesChanged(); } void Chart::addLegendInternal( Legend* legend, bool setMeasures ) { if ( !legend ) { return; } KChartEnums::PositionValue pos = legend->position().value(); if ( pos == KChartEnums::PositionCenter ) { qWarning( "Not showing legend because PositionCenter is not supported for legends." ); } int row; int column; getRowAndColumnForPosition( pos, &row, &column ); if ( row < 0 && pos != KChartEnums::PositionFloating ) { qWarning( "Not showing legend because of unknown legend position." ); return; } d->legends.append( legend ); legend->setParent( this ); // set text attributes (why?) if ( setMeasures ) { TextAttributes textAttrs( legend->textAttributes() ); Measure measure( textAttrs.fontSize() ); measure.setRelativeMode( this, KChartEnums::MeasureOrientationMinimum ); measure.setValue( 20 ); textAttrs.setFontSize( measure ); legend->setTextAttributes( textAttrs ); textAttrs = legend->titleTextAttributes(); measure.setRelativeMode( this, KChartEnums::MeasureOrientationMinimum ); measure.setValue( 24 ); textAttrs.setFontSize( measure ); legend->setTitleTextAttributes( textAttrs ); legend->setReferenceArea( this ); } // add it to the appropriate layout if ( pos != KChartEnums::PositionFloating ) { legend->needSizeHint(); // in each edge and corner of the outer layout, there's a grid for the different alignments that we create // on demand. we don't remove it when empty. QLayoutItem* edgeItem = d->dataAndLegendLayout->itemAtPosition( row, column ); QGridLayout* alignmentsLayout = dynamic_cast< QGridLayout* >( edgeItem ); Q_ASSERT( !edgeItem || alignmentsLayout ); // if it exists, it must be a QGridLayout if ( !alignmentsLayout ) { alignmentsLayout = new QGridLayout; d->dataAndLegendLayout->addLayout( alignmentsLayout, row, column ); alignmentsLayout->setContentsMargins( 0, 0, 0, 0 ); } // in case there are several legends in the same edge or corner with the same alignment, they are stacked // vertically using a QVBoxLayout. it is created on demand as above. row = 1; column = 1; for ( int i = 0; i < 3; i++ ) { for ( int j = 0; j < 3; j++ ) { Qt::Alignment align = s_gridAlignments[ i ][ j ]; if ( align == legend->alignment() ) { row = i; column = j; break; } } } QLayoutItem* alignmentItem = alignmentsLayout->itemAtPosition( row, column ); QVBoxLayout* sameAlignmentLayout = dynamic_cast< QVBoxLayout* >( alignmentItem ); Q_ASSERT( !alignmentItem || sameAlignmentLayout ); // if it exists, it must be a QVBoxLayout if ( !sameAlignmentLayout ) { sameAlignmentLayout = new QVBoxLayout; alignmentsLayout->addLayout( sameAlignmentLayout, row, column ); sameAlignmentLayout->setContentsMargins( 0, 0, 0, 0 ); } sameAlignmentLayout->addItem( new MyWidgetItem( legend, legend->alignment() ) ); } connect( legend, SIGNAL(destroyedLegend(Legend*)), d, SLOT(slotUnregisterDestroyedLegend(Legend*)) ); connect( legend, SIGNAL(positionChanged(AbstractAreaWidget*)), d, SLOT(slotLegendPositionChanged(AbstractAreaWidget*)) ); connect( legend, SIGNAL(propertiesChanged()), this, SIGNAL(propertiesChanged()) ); d->slotResizePlanes(); } void Chart::replaceLegend( Legend* legend, Legend* oldLegend_ ) { if ( legend && oldLegend_ != legend ) { Legend* oldLegend = oldLegend_; if ( d->legends.count() ) { if ( ! oldLegend ) { oldLegend = d->legends.first(); if ( oldLegend == legend ) return; } takeLegend( oldLegend ); } delete oldLegend; addLegend( legend ); } } void Chart::takeLegend( Legend* legend ) { const int idx = d->legends.indexOf( legend ); if ( idx == -1 ) { return; } d->legends.takeAt( idx ); disconnect( legend, nullptr, d, nullptr ); disconnect( legend, nullptr, this, nullptr ); // the following removes the legend from its layout and destroys its MyWidgetItem (the link to the layout) legend->setParent( nullptr ); d->slotResizePlanes(); emit propertiesChanged(); } Legend* Chart::legend() { return d->legends.isEmpty() ? nullptr : d->legends.first(); } LegendList Chart::legends() { return d->legends; } void Chart::mousePressEvent( QMouseEvent* event ) { const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { QMouseEvent ev( QEvent::MouseButtonPress, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mousePressEvent( &ev ); d->mouseClickedPlanes.append( plane ); } } } void Chart::mouseDoubleClickEvent( QMouseEvent* event ) { const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { QMouseEvent ev( QEvent::MouseButtonPress, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mouseDoubleClickEvent( &ev ); } } } void Chart::mouseMoveEvent( QMouseEvent* event ) { QSet< AbstractCoordinatePlane* > eventReceivers = QSet< AbstractCoordinatePlane* >::fromList( d->mouseClickedPlanes ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { eventReceivers.insert( plane ); } } const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, eventReceivers ) { QMouseEvent ev( QEvent::MouseMove, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mouseMoveEvent( &ev ); } } void Chart::mouseReleaseEvent( QMouseEvent* event ) { QSet< AbstractCoordinatePlane* > eventReceivers = QSet< AbstractCoordinatePlane* >::fromList( d->mouseClickedPlanes ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { eventReceivers.insert( plane ); } } const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, eventReceivers ) { QMouseEvent ev( QEvent::MouseButtonRelease, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mouseReleaseEvent( &ev ); } d->mouseClickedPlanes.clear(); } bool Chart::event( QEvent* event ) { if ( event->type() == QEvent::ToolTip ) { const QHelpEvent* const helpEvent = static_cast< QHelpEvent* >( event ); Q_FOREACH( const AbstractCoordinatePlane* const plane, d->coordinatePlanes ) { // iterate diagrams in reverse, so that the top-most painted diagram is // queried first for a tooltip before the diagrams behind it const ConstAbstractDiagramList& diagrams = plane->diagrams(); for (int i = diagrams.size() - 1; i >= 0; --i) { const AbstractDiagram* diagram = diagrams[i]; if (diagram->isHidden()) { continue; } const QModelIndex index = diagram->indexAt( helpEvent->pos() ); const QVariant toolTip = index.data( Qt::ToolTipRole ); if ( toolTip.isValid() ) { QPoint pos = mapFromGlobal( helpEvent->pos() ); QRect rect( pos - QPoint( 1, 1 ), QSize( 3, 3 ) ); QToolTip::showText( QCursor::pos(), toolTip.toString(), this, rect ); return true; } } } } return QWidget::event( event ); } bool Chart::useNewLayoutSystem() const { return d_func()->useNewLayoutSystem; } void Chart::setUseNewLayoutSystem( bool value ) { if ( d_func()->useNewLayoutSystem != value ) d_func()->useNewLayoutSystem = value; } diff --git a/src/KGantt/kganttgraphicsview.cpp b/src/KGantt/kganttgraphicsview.cpp index eeef037..8f2b7c0 100644 --- a/src/KGantt/kganttgraphicsview.cpp +++ b/src/KGantt/kganttgraphicsview.cpp @@ -1,827 +1,827 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KGantt library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kganttgraphicsview.h" #include "kganttgraphicsview_p.h" #include "kganttabstractrowcontroller.h" #include "kganttgraphicsitem.h" #include "kganttconstraintmodel.h" #include "kganttdatetimetimelinedialog.h" #include #include #include #include #include #include #include #include #include #include #if defined KDAB_EVAL #include "../evaldialog/evaldialog.h" #endif -/*!\class KGantt::HeaderWidget +/*\class KGantt::HeaderWidget * \internal */ using namespace KGantt; HeaderWidget::HeaderWidget( GraphicsView* parent ) : QWidget( parent ), m_offset( 0. ) { assert( parent ); // Parent must be set } HeaderWidget::~HeaderWidget() { } void HeaderWidget::scrollTo( int v ) { m_offset = v; // QWidget::scroll() wont work properly for me on Mac //scroll( static_cast( old-v ), 0 ); update(); } void HeaderWidget::paintEvent( QPaintEvent* ev ) { QPainter p( this ); view()->grid()->paintHeader( &p, rect(), ev->rect(), m_offset, this ); } bool HeaderWidget::event( QEvent* event ) { if ( event->type() == QEvent::ToolTip ) { DateTimeGrid* const grid = qobject_cast< DateTimeGrid* >( view()->grid() ); if ( grid ) { QHelpEvent *e = static_cast( event ); QDateTime dt = grid->mapFromChart( view()->mapToScene( e->x(), 0 ).x() ).toDateTime(); setToolTip( dt.toString() ); } } return QWidget::event( event ); } void HeaderWidget::contextMenuEvent( QContextMenuEvent* event ) { QMenu contextMenu; DateTimeGrid* const grid = qobject_cast< DateTimeGrid* >( view()->grid() ); QAction* actionScaleAuto = nullptr; QAction* actionScaleMonth = nullptr; QAction* actionScaleWeek = nullptr; QAction* actionScaleDay = nullptr; QAction* actionScaleHour = nullptr; QAction* actionZoomIn = nullptr; QAction* actionZoomOut = nullptr; QAction* actionTimeline = nullptr; if ( grid != nullptr ) { QMenu* menuScale = new QMenu( tr( "Scale", "@title:menu" ), &contextMenu ); QActionGroup* scaleGroup = new QActionGroup( &contextMenu ); scaleGroup->setExclusive( true ); actionScaleAuto = new QAction( tr( "Auto", "@item:inmenu Automatic scale" ), menuScale ); actionScaleAuto->setCheckable( true ); actionScaleAuto->setChecked( grid->scale() == DateTimeGrid::ScaleAuto ); actionScaleMonth = new QAction( tr( "Month", "@item:inmenu" ), menuScale ); actionScaleMonth->setCheckable( true ); actionScaleMonth->setChecked( grid->scale() == DateTimeGrid::ScaleMonth ); actionScaleWeek = new QAction( tr( "Week", "@item:inmenu" ), menuScale ); actionScaleWeek->setCheckable( true ); actionScaleWeek->setChecked( grid->scale() == DateTimeGrid::ScaleWeek ); actionScaleDay = new QAction( tr( "Day", "@item:inmenu" ), menuScale ); actionScaleDay->setCheckable( true ); actionScaleDay->setChecked( grid->scale() == DateTimeGrid::ScaleDay ); actionScaleHour = new QAction( tr( "Hour", "@item:inmenu" ), menuScale ); actionScaleHour->setCheckable( true ); actionScaleHour->setChecked( grid->scale() == DateTimeGrid::ScaleHour ); scaleGroup->addAction( actionScaleAuto ); menuScale->addAction( actionScaleAuto ); scaleGroup->addAction( actionScaleMonth ); menuScale->addAction( actionScaleMonth ); scaleGroup->addAction( actionScaleWeek ); menuScale->addAction( actionScaleWeek ); scaleGroup->addAction( actionScaleDay ); menuScale->addAction( actionScaleDay ); scaleGroup->addAction( actionScaleHour ); menuScale->addAction( actionScaleHour ); contextMenu.addMenu( menuScale ); contextMenu.addSeparator(); actionZoomIn = new QAction( tr( "Zoom In", "@action:inmenu" ), &contextMenu ); contextMenu.addAction( actionZoomIn ); actionZoomOut = new QAction( tr( "Zoom Out", "@action:inmenu" ), &contextMenu ); contextMenu.addAction( actionZoomOut ); contextMenu.addSeparator(); actionTimeline = new QAction( tr( "Timeline...", "@action:inmenu" ), &contextMenu ); contextMenu.addAction( actionTimeline ); } if ( contextMenu.isEmpty() ) { event->ignore(); return; } const QAction* const action = contextMenu.exec( event->globalPos() ); if ( action == nullptr ) {} else if ( action == actionScaleAuto ) { assert( grid != nullptr ); grid->setScale( DateTimeGrid::ScaleAuto ); } else if ( action == actionScaleMonth ) { assert( grid != nullptr ); grid->setScale( DateTimeGrid::ScaleMonth ); } else if ( action == actionScaleWeek ) { assert( grid != nullptr ); grid->setScale( DateTimeGrid::ScaleWeek ); } else if ( action == actionScaleDay ) { assert( grid != nullptr ); grid->setScale( DateTimeGrid::ScaleDay ); } else if ( action == actionScaleHour ) { assert( grid != nullptr ); grid->setScale( DateTimeGrid::ScaleHour ); } else if ( action == actionZoomIn ) { assert( grid != nullptr ); grid->setDayWidth( grid->dayWidth() * 1.25 ); } else if ( action == actionZoomOut ) { assert( grid != nullptr ); // daywidth *MUST NOT* go below 1.0, it is used as an integer later on grid->setDayWidth( qMax( 1.0, grid->dayWidth() * 0.8 ) ); } else if ( action == actionTimeline ) { assert( grid != nullptr ); DateTimeTimeLineDialog dlg(grid->timeLine()); dlg.exec(); } event->accept(); } GraphicsView::Private::Private( GraphicsView* _q ) : q( _q ), rowcontroller(nullptr), headerwidget( _q ) { } void GraphicsView::Private::updateHeaderGeometry() { q->setViewportMargins(0,rowcontroller->headerHeight(),0,0); headerwidget.setGeometry( q->viewport()->x(), q->viewport()->y() - rowcontroller->headerHeight(), q->viewport()->width(), rowcontroller->headerHeight() ); } void GraphicsView::Private::slotGridChanged() { updateHeaderGeometry(); headerwidget.update(); q->updateSceneRect(); q->update(); } void GraphicsView::Private::slotHorizontalScrollValueChanged( int val ) { const QRectF viewRect = q->transform().mapRect( q->sceneRect() ); headerwidget.scrollTo( val-q->horizontalScrollBar()->minimum()+static_cast( viewRect.left() ) ); } void GraphicsView::Private::slotColumnsInserted( const QModelIndex& parent, int start, int end ) { Q_UNUSED( start ); Q_UNUSED( end ); QModelIndex idx = scene.model()->index( 0, 0, scene.summaryHandlingModel()->mapToSource( parent ) ); do { scene.updateRow( scene.summaryHandlingModel()->mapFromSource( idx ) ); } while ( ( idx = rowcontroller->indexBelow( idx ) ) != QModelIndex() && rowcontroller->isRowVisible( idx ) ); //} while ( ( idx = d->treeview.indexBelow( idx ) ) != QModelIndex() && d->treeview.visualRect(idx).isValid() ); q->updateSceneRect(); } void GraphicsView::Private::slotColumnsRemoved( const QModelIndex& parent, int start, int end ) { // TODO Q_UNUSED( start ); Q_UNUSED( end ); Q_UNUSED( parent ); q->updateScene(); } void GraphicsView::Private::slotDataChanged( const QModelIndex& topLeft, const QModelIndex& bottomRight ) { //qDebug() << "GraphicsView::slotDataChanged("<index( row, 0, parent ) ); } } void GraphicsView::Private::slotLayoutChanged() { //qDebug() << "slotLayoutChanged()"; q->updateScene(); } void GraphicsView::Private::slotModelReset() { //qDebug() << "slotModelReset()"; q->updateScene(); } void GraphicsView::Private::slotRowsInserted( const QModelIndex& parent, int start, int end ) { Q_UNUSED( parent ); Q_UNUSED( start ); Q_UNUSED( end ); q->updateScene(); // TODO: This might be optimised } void GraphicsView::Private::removeConstraintsRecursive( QAbstractProxyModel *summaryModel, const QModelIndex& index ) { if ( summaryModel->hasChildren( index ) ) { //qDebug() << "removing constraints from children of"<rowCount( index ); ++row ) { const QModelIndex child = summaryModel->index( row, index.column(), index ); removeConstraintsRecursive( summaryModel, child ); } } //qDebug() << "removing constraints from"<sourceModel() QList clst = scene.constraintModel()->constraintsForIndex( summaryModel->mapToSource( index ) ); Q_FOREACH( Constraint c, clst ) { scene.constraintModel()->removeConstraint( c ); } } void GraphicsView::Private::slotRowsAboutToBeRemoved( const QModelIndex& parent, int start, int end ) { //qDebug() << "GraphicsView::Private::slotRowsAboutToBeRemoved("<columnCount( parent ); ++col ) { const QModelIndex idx = summaryModel->index( row, col, parent ); removeConstraintsRecursive( summaryModel, idx ); scene.removeItem( idx ); } } } void GraphicsView::Private::slotRowsRemoved( const QModelIndex& parent, int start, int end ) { //qDebug() << "GraphicsView::Private::slotRowsRemoved("<updateScene(); } void GraphicsView::Private::slotItemClicked( const QModelIndex& idx ) { QModelIndex sidx = idx;//scene.summaryHandlingModel()->mapToSource( idx ); emit q->clicked( sidx ); if (q->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, q)) emit q->activated( sidx ); } void GraphicsView::Private::slotItemDoubleClicked( const QModelIndex& idx ) { QModelIndex sidx = idx;//scene.summaryHandlingModel()->mapToSource( idx ); emit q->qrealClicked( sidx ); if (!q->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, q)) emit q->activated( sidx ); } void GraphicsView::Private::slotHeaderContextMenuRequested( const QPoint& pt ) { emit q->headerContextMenuRequested( headerwidget.mapToGlobal( pt ) ); } /*!\class KGantt::GraphicsView kganttgraphicsview.h KGanttGraphicsView * \ingroup KGantt * \brief The GraphicsView class provides a model/view implementation of a gantt chart. * * */ /*! \fn void GraphicsView::activated( const QModelIndex & index ) */ /*! \fn void GraphicsView::clicked( const QModelIndex & index ); */ /*! \fn void GraphicsView::qrealClicked( const QModelIndex & index ); */ /*! \fn void GraphicsView::entered( const QModelIndex & index ); */ /*! \fn void GraphicsView::pressed( const QModelIndex & index ); */ /*! \fn void GraphicsView::headerContextMenuRequested( const QPoint& pt ) * This signal is emitted when the header has contextMenuPolicy Qt::CustomContextMenu * and the widget wants to show a context menu for the header. Unlike in * QWidget::customContextMenuRequested() signal, \a pt is here in global coordinates. */ /*! Constructor. Creates a new KGantt::GraphicsView with parent * \a parent. */ GraphicsView::GraphicsView( QWidget* parent ) : QGraphicsView( parent ), _d( new Private( this ) ) { #if defined KDAB_EVAL EvalDialog::checkEvalLicense( "KD Gantt" ); #endif connect( horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(slotHorizontalScrollValueChanged(int)) ); connect( &_d->scene, SIGNAL(gridChanged()), this, SLOT(slotGridChanged()) ); connect( &_d->scene, SIGNAL(entered(QModelIndex)), this, SIGNAL(entered(QModelIndex)) ); connect( &_d->scene, SIGNAL(pressed(QModelIndex)), this, SIGNAL(pressed(QModelIndex)) ); connect( &_d->scene, SIGNAL(clicked(QModelIndex)), this, SLOT(slotItemClicked(QModelIndex)) ); connect( &_d->scene, SIGNAL(qrealClicked(QModelIndex)), this, SLOT(slotItemDoubleClicked(QModelIndex)) ); connect( &_d->scene, SIGNAL(sceneRectChanged(QRectF)), this, SLOT(updateSceneRect()) ); connect( &_d->headerwidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotHeaderContextMenuRequested(QPoint)) ); setScene( &_d->scene ); // HACK! setSummaryHandlingModel( _d->scene.summaryHandlingModel() ); // So that AbstractGrid::drawBackground() and AbstractGrid::drawForeground() // works properly setViewportUpdateMode(QGraphicsView::FullViewportUpdate); //setCacheMode( CacheBackground ); } /*! Destroys this view. */ GraphicsView::~GraphicsView() { delete _d; } #define d d_func() /*! Sets the model to be displayed in this view to * \a model. The view does not take ownership of the model. * * To make a model work well with GraphicsView it must * have a certain layout. Whether the model is flat or has a * treestrucure is not important, as long as an * AbstractRowController is provided that can navigate the * model. * * GraphicsView operates per row in the model. The data is always * taken from the _last_ item in the row. The ItemRoles used are * Qt::DisplayRole and the roles defined in KGantt::ItemDataRole. * * Note: This model is not returned by \a model() * * \see GraphicsView::model */ void GraphicsView::setModel( QAbstractItemModel* model ) { if ( d->scene.model() ) { disconnect( d->scene.model() ); } d->scene.setModel( model ); if (model) { connect( model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateSceneRect()) ); } updateScene(); } /*! \returns the current model displayed by this view * * Note: The returned model is not the model set with \a setModel() * * \see GraphicsView::setModel */ QAbstractItemModel* GraphicsView::model() const { return d->scene.model(); } void GraphicsView::setSummaryHandlingModel( QAbstractProxyModel* proxyModel ) { disconnect( d->scene.summaryHandlingModel() ); d->scene.setSummaryHandlingModel( proxyModel ); /* Connections. We have to rely on the treeview * to receive the signals before we do(!) */ connect( proxyModel, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(slotColumnsInserted(QModelIndex,int,int)) ); connect( proxyModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(slotColumnsRemoved(QModelIndex,int,int)) ); connect( proxyModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(slotDataChanged(QModelIndex,QModelIndex)) ); connect( proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()) ); connect( proxyModel, SIGNAL(modelReset()), this, SLOT(slotModelReset()) ); connect( proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(slotRowsInserted(QModelIndex,int,int)) ); connect( proxyModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int)) ); connect( proxyModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(slotRowsRemoved(QModelIndex,int,int)) ); updateScene(); } /*! Sets the constraintmodel displayed by this view. * \see KGantt::ConstraintModel. */ void GraphicsView::setConstraintModel( ConstraintModel* cmodel ) { d->scene.setConstraintModel( cmodel ); } /*! \returns the KGantt::ConstraintModel displayed by this view. */ ConstraintModel* GraphicsView::constraintModel() const { return d->scene.constraintModel(); } /*! \returns the KGantt::SummaryHandlingProxyModel used by this view. */ QAbstractProxyModel* GraphicsView::summaryHandlingModel() const { return d->scene.summaryHandlingModel(); } /*! Sets the root index of the model displayed by this view. * Similar to QAbstractItemView::setRootIndex, default is QModelIndex(). */ void GraphicsView::setRootIndex( const QModelIndex& idx ) { d->scene.setRootIndex( idx ); } /*! \returns the rootindex for this view. */ QModelIndex GraphicsView::rootIndex() const { return d->scene.rootIndex(); } /*! Sets the QItemSelectionModel used by this view to manage * selections. Similar to QAbstractItemView::setSelectionModel */ void GraphicsView::setSelectionModel( QItemSelectionModel* model ) { d->scene.setSelectionModel( model ); } /*! \returns the QItemSelectionModel used by this view */ QItemSelectionModel* GraphicsView::selectionModel() const { return d->scene.selectionModel(); } /*! Sets the KGantt::ItemDelegate used for rendering items on this * view. \see ItemDelegate and QAbstractItemDelegate. */ void GraphicsView::setItemDelegate( ItemDelegate* delegate ) { d->scene.setItemDelegate( delegate ); } /*! \returns the ItemDelegate used by this view to render items */ ItemDelegate* GraphicsView::itemDelegate() const { return d->scene.itemDelegate(); } /*! Sets the AbstractRowController used by this view. The * AbstractRowController deals with the height and position * of each row and with which parts of the model are * displayed. \see AbstractRowController */ void GraphicsView::setRowController( AbstractRowController* rowcontroller ) { d->rowcontroller = rowcontroller; d->scene.setRowController( rowcontroller ); updateScene(); } /*! \returns the AbstractRowController * for this view. \see setRowController */ AbstractRowController* GraphicsView::rowController() const { return d->rowcontroller; } /*! Sets the AbstractGrid for this view. The grid is an * object that controls how QModelIndexes are mapped * to and from the view and how the background and header * is rendered. \see AbstractGrid and DateTimeGrid. */ void GraphicsView::setGrid( AbstractGrid* grid ) { d->scene.setGrid( grid ); d->slotGridChanged(); } /*! \returns the AbstractGrid used by this view. */ AbstractGrid* GraphicsView::grid() const { return d->scene.grid(); } /*! \returns the AbstractGrid used by this view. */ AbstractGrid* GraphicsView::takeGrid() { AbstractGrid *grid = d->scene.grid(); if ( grid ) { d->scene.setGrid( nullptr) ; } return grid; } /*! Sets the view to read-only mode if \a to is true. The default is * read/write if the model permits it. */ void GraphicsView::setReadOnly( bool ro ) { d->scene.setReadOnly( ro ); } /*!\returns true iff the view is in read-only mode */ bool GraphicsView::isReadOnly() const { return d->scene.isReadOnly(); } /*! Sets the context menu policy for the header. The default value * Qt::DefaultContextMenu results in a standard context menu on the header * that allows the user to set the scale and zoom. * * Setting this to Qt::CustomContextMenu will cause the signal * headerContextMenuRequested(const QPoint& pt) to be emitted instead. * * \see QWidget::setContextMenuPolicy( Qt::ContextMenuPolicy ) */ void GraphicsView::setHeaderContextMenuPolicy( Qt::ContextMenuPolicy p ) { d->headerwidget.setContextMenuPolicy( p ); } /*! \returns the context menu policy for the header */ Qt::ContextMenuPolicy GraphicsView::headerContextMenuPolicy() const { return d->headerwidget.contextMenuPolicy(); } /*! Adds a constraint from \a from to \a to. \a modifiers are the * keyboard modifiers pressed by the user when the action is invoked. * * Override this to control how contraints are added. The default * implementation adds a soft constraint unless the Shift key is pressed, * in that case it adds a hard constraint. If a constraint is already * present, it is removed and nothing is added. */ void GraphicsView::addConstraint( const QModelIndex& from, const QModelIndex& to, Qt::KeyboardModifiers modifiers ) { if ( isReadOnly() ) return; ConstraintModel* cmodel = constraintModel(); assert( cmodel ); Constraint c( from, to, ( modifiers&Qt::ShiftModifier )?Constraint::TypeHard:Constraint::TypeSoft ); if ( cmodel->hasConstraint( c ) ) cmodel->removeConstraint( c ); else cmodel->addConstraint( c ); } void GraphicsView::resizeEvent( QResizeEvent* ev ) { d->updateHeaderGeometry(); QRectF r = scene()->itemsBoundingRect(); // To scroll more to the left than the actual item start, bug #4516 r.setLeft( qMin( 0.0, r.left() ) ); // TODO: take scrollbars into account (if not always on) // The scene should be at least the size of the viewport QSizeF size = viewport()->size(); //TODO: why -2 below? size should be ex. frames etc? if ( size.width() > r.width() ) { r.setWidth( size.width() - 2 ); } if ( size.height() > r.height() ) { r.setHeight( size.height() - 2 ); } const int totalh = rowController()->totalHeight(); if ( r.height() < totalh ) { r.setHeight( totalh ); } scene()->setSceneRect( r ); QGraphicsView::resizeEvent( ev ); } /*!\returns The QModelIndex for the item located at * position \a pos in the view or an invalid index * if no item was present at that position. * * This is useful for for example contextmenus. */ QModelIndex GraphicsView::indexAt( const QPoint& pos ) const { QGraphicsItem* item = itemAt( pos ); if ( GraphicsItem* gitem = qgraphicsitem_cast( item ) ) { return d->scene.summaryHandlingModel()->mapToSource( gitem->index() ); } else { return QModelIndex(); } } /*! \internal */ void GraphicsView::clearItems() { d->scene.clearItems(); } /*! \internal */ void GraphicsView::updateRow( const QModelIndex& idx ) { d->scene.updateRow( d->scene.summaryHandlingModel()->mapFromSource( idx ) ); } /*! \internal * Adjusts the bounding rectangle of the scene. */ void GraphicsView::updateSceneRect() { /* What to do with this? We need to shrink the view to * make collapsing items work */ qreal range = horizontalScrollBar()->maximum()-horizontalScrollBar()->minimum(); const qreal hscroll = horizontalScrollBar()->value()/( range>0?range:1 ); QRectF r = d->scene.itemsBoundingRect(); // To scroll more to the left than the actual item start, bug #4516 r.setTop( 0. ); r.setLeft( qMin( 0.0, r.left() ) ); r.setSize( r.size().expandedTo( viewport()->size() ) ); const int totalh = rowController()->totalHeight(); if ( r.height() < totalh ) r.setHeight( totalh ); d->scene.setSceneRect( r ); /* set scrollbar to keep the same time in view */ range = horizontalScrollBar()->maximum()-horizontalScrollBar()->minimum(); if ( range>0 ) horizontalScrollBar()->setValue( qRound( hscroll*range ) ); /* We have to update here to adjust for any rows with no * information because they are painted with a different * background brush */ d->scene.invalidate( QRectF(), QGraphicsScene::BackgroundLayer ); } /*! \internal * Resets the state of the view. */ void GraphicsView::updateScene() { clearItems(); if ( !model()) return; if ( !rowController()) return; QModelIndex idx = model()->index( 0, 0, rootIndex() ); do { updateRow( idx ); } while ( ( idx = rowController()->indexBelow( idx ) ) != QModelIndex() && rowController()->isRowVisible(idx) ); //constraintModel()->cleanup(); //qDebug() << constraintModel(); updateSceneRect(); if ( scene() ) scene()->invalidate( QRectF(), QGraphicsScene::BackgroundLayer ); } #if 0 TODO: For 3.0 /*! Creates a new GraphicsItem * Re-iplement to create your own flavour of GraphicsItem */ GraphicsItem* GraphicsView::createItem( ItemType type ) const { Q_UNUSED(type) return new GraphicsItem; } #endif /*! \internal */ void GraphicsView::deleteSubtree( const QModelIndex& idx ) { d->scene.deleteSubtree( d->scene.summaryHandlingModel()->mapFromSource( idx ) ); } /*! Print the Gantt chart using \a printer. If \a drawRowLabels * is true (the default), each row will have it's label printed * on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. * * This version of print() will print multiple pages. */ void GraphicsView::print( QPrinter* printer, bool drawRowLabels, bool drawColumnLabels ) { d->scene.print( printer, drawRowLabels, drawColumnLabels ); } /*! Print part of the Gantt chart from \a start to \a end using \a printer. * If \a drawRowLabels is true (the default), each row will have it's * label printed on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. * * This version of print() will print multiple pages. * * To print a certain range of a chart with a DateTimeGrid, use * qreal DateTimeGrid::mapFromDateTime( const QDateTime& dt) const * to figure out the values for \a start and \a end. */ void GraphicsView::print( QPrinter* printer, qreal start, qreal end, bool drawRowLabels, bool drawColumnLabels ) { d->scene.print( printer, start, end, drawRowLabels, drawColumnLabels ); } /*! Render the GanttView inside the rectangle \a target using the painter \a painter. * If \a drawRowLabels is true (the default), each row will have it's * label printed on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. */ void GraphicsView::print( QPainter* painter, const QRectF& targetRect, bool drawRowLabels, bool drawColumnLabels ) { d->scene.print(painter, targetRect, drawRowLabels, drawColumnLabels); } /*! Render the GanttView inside the rectangle \a target using the painter \a painter. * If \a drawRowLabels is true (the default), each row will have it's * label printed on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. * * To print a certain range of a chart with a DateTimeGrid, use * qreal DateTimeGrid::mapFromDateTime( const QDateTime& dt) const * to figure out the values for \a start and \a end. */ void GraphicsView::print( QPainter* painter, qreal start, qreal end, const QRectF& targetRect, bool drawRowLabels, bool drawColumnLabels ) { d->scene.print(painter, start, end, targetRect, drawRowLabels, drawColumnLabels); } #include "moc_kganttgraphicsview.cpp"