diff --git a/src/KChart/Cartesian/KChartCartesianGrid.cpp b/src/KChart/Cartesian/KChartCartesianGrid.cpp index ab3ed4f..9246955 100644 --- a/src/KChart/Cartesian/KChartCartesianGrid.cpp +++ b/src/KChart/Cartesian/KChartCartesianGrid.cpp @@ -1,501 +1,502 @@ /* * 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 "KChartCartesianGrid.h" #include "KChartAbstractCartesianDiagram.h" #include "KChartPaintContext.h" #include "KChartPainterSaver_p.h" #include "KChartPrintingParameters.h" #include "KChartFrameAttributes.h" #include "KChartCartesianAxis_p.h" #include "KChartMath_p.h" #include +#include using namespace KChart; CartesianGrid::CartesianGrid() : AbstractGrid(), m_minsteps( 2 ), m_maxsteps( 12 ) { } CartesianGrid::~CartesianGrid() { } int CartesianGrid::minimalSteps() const { return m_minsteps; } void CartesianGrid::setMinimalSteps(int minsteps) { m_minsteps = minsteps; } int CartesianGrid::maximalSteps() const { return m_maxsteps; } void CartesianGrid::setMaximalSteps(int maxsteps) { m_maxsteps = maxsteps; } void CartesianGrid::drawGrid( PaintContext* context ) { CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( context->coordinatePlane() ); const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) ); const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) ); if ( !gridAttrsX.isGridVisible() && !gridAttrsX.isSubGridVisible() && !gridAttrsY.isGridVisible() && !gridAttrsY.isSubGridVisible() ) { return; } // This plane is used for translating the coordinates - not for the data boundaries QPainter *p = context->painter(); PainterSaver painterSaver( p ); // sharedAxisMasterPlane() changes the painter's coordinate transformation(!) plane = qobject_cast< CartesianCoordinatePlane* >( plane->sharedAxisMasterPlane( context->painter() ) ); Q_ASSERT_X ( plane, "CartesianGrid::drawGrid", "Bad function call: PaintContext::coodinatePlane() NOT a cartesian plane." ); // update the calculated mDataDimensions before using them updateData( context->coordinatePlane() ); // this, in turn, calls our calculateGrid(). Q_ASSERT_X ( mDataDimensions.count() == 2, "CartesianGrid::drawGrid", "Error: updateData did not return exactly two dimensions." ); if ( !isBoundariesValid( mDataDimensions ) ) { return; } const DataDimension dimX = mDataDimensions.first(); const DataDimension dimY = mDataDimensions.last(); const bool isLogarithmicX = dimX.calcMode == AbstractCoordinatePlane::Logarithmic; const bool isLogarithmicY = dimY.calcMode == AbstractCoordinatePlane::Logarithmic; qreal minValueX = qMin( dimX.start, dimX.end ); qreal maxValueX = qMax( dimX.start, dimX.end ); qreal minValueY = qMin( dimY.start, dimY.end ); qreal maxValueY = qMax( dimY.start, dimY.end ); { bool adjustXLower = !isLogarithmicX && gridAttrsX.adjustLowerBoundToGrid(); bool adjustXUpper = !isLogarithmicX && gridAttrsX.adjustUpperBoundToGrid(); bool adjustYLower = !isLogarithmicY && gridAttrsY.adjustLowerBoundToGrid(); bool adjustYUpper = !isLogarithmicY && gridAttrsY.adjustUpperBoundToGrid(); AbstractGrid::adjustLowerUpperRange( minValueX, maxValueX, dimX.stepWidth, adjustXLower, adjustXUpper ); AbstractGrid::adjustLowerUpperRange( minValueY, maxValueY, dimY.stepWidth, adjustYLower, adjustYUpper ); } if ( plane->frameAttributes().isVisible() ) { const qreal radius = plane->frameAttributes().cornerRadius(); QPainterPath path; path.addRoundedRect( QRectF( plane->translate( QPointF( minValueX, minValueY ) ), plane->translate( QPointF( maxValueX, maxValueY ) ) ), radius, radius ); context->painter()->setClipPath( path ); } /* TODO features from old code: - MAYBE coarsen the main grid when it gets crowded (do it in calculateGrid or here?) if ( ! dimX.isCalculated ) { while ( screenRangeX / numberOfUnitLinesX <= MinimumPixelsBetweenLines ) { dimX.stepWidth *= 10.0; dimX.subStepWidth *= 10.0; numberOfUnitLinesX = qAbs( dimX.distance() / dimX.stepWidth ); } } - MAYBE deactivate the sub-grid when it gets crowded if ( dimX.subStepWidth && (screenRangeX / (dimX.distance() / dimX.subStepWidth) <= MinimumPixelsBetweenLines) ) { // de-activating grid sub steps: not enough space dimX.subStepWidth = 0.0; } */ for ( int i = 0; i < 2; i++ ) { XySwitch xy( i == 1 ); // first iteration paints the X grid lines, second paints the Y grid lines const GridAttributes& gridAttrs = xy( gridAttrsX, gridAttrsY ); bool hasMajorLines = gridAttrs.isGridVisible(); bool hasMinorLines = hasMajorLines && gridAttrs.isSubGridVisible(); if ( !hasMajorLines && !hasMinorLines ) { continue; } const DataDimension& dimension = xy( dimX, dimY ); const bool drawZeroLine = dimension.isCalculated && gridAttrs.zeroLinePen().style() != Qt::NoPen; QPointF lineStart = QPointF( minValueX, minValueY ); // still need transformation to screen space QPointF lineEnd = QPointF( maxValueX, maxValueY ); TickIterator it( xy.isY, dimension, gridAttrs.linesOnAnnotations(), hasMajorLines, hasMinorLines, plane ); for ( ; !it.isAtEnd(); ++it ) { if ( !gridAttrs.isOuterLinesVisible() && ( it.areAlmostEqual( it.position(), xy( minValueX, minValueY ) ) || it.areAlmostEqual( it.position(), xy( maxValueX, maxValueY ) ) ) ) { continue; } xy.lvalue( lineStart.rx(), lineStart.ry() ) = it.position(); xy.lvalue( lineEnd.rx(), lineEnd.ry() ) = it.position(); QPointF transLineStart = plane->translate( lineStart ); QPointF transLineEnd = plane->translate( lineEnd ); if ( ISNAN( transLineStart.x() ) || ISNAN( transLineStart.y() ) || ISNAN( transLineEnd.x() ) || ISNAN( transLineEnd.y() ) ) { // ### can we catch NaN problems earlier, wasting fewer cycles? continue; } if ( it.position() == 0.0 && drawZeroLine ) { p->setPen( PrintingParameters::scalePen( gridAttrsX.zeroLinePen() ) ); } else if ( it.type() == TickIterator::MinorTick ) { p->setPen( PrintingParameters::scalePen( gridAttrs.subGridPen() ) ); } else { p->setPen( PrintingParameters::scalePen( gridAttrs.gridPen() ) ); } p->drawLine( transLineStart, transLineEnd ); } } } DataDimensionsList CartesianGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const { Q_ASSERT_X ( rawDataDimensions.count() == 2, "CartesianGrid::calculateGrid", "Error: calculateGrid() expects a list with exactly two entries." ); CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( mPlane ); Q_ASSERT_X ( plane, "CartesianGrid::calculateGrid", "Error: PaintContext::calculatePlane() called, but no cartesian plane set." ); DataDimensionsList l( rawDataDimensions ); #if 0 qDebug() << Q_FUNC_INFO << "initial grid X-range:" << l.first().start << "->" << l.first().end << " substep width:" << l.first().subStepWidth; qDebug() << Q_FUNC_INFO << "initial grid Y-range:" << l.last().start << "->" << l.last().end << " substep width:" << l.last().subStepWidth; #endif // rule: Returned list is either empty, or it is providing two // valid dimensions, complete with two non-Zero step widths. if ( isBoundariesValid( l ) ) { const QPointF translatedBottomLeft( plane->translateBack( plane->geometry().bottomLeft() ) ); const QPointF translatedTopRight( plane->translateBack( plane->geometry().topRight() ) ); const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) ); const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) ); const DataDimension dimX = calculateGridXY( l.first(), Qt::Horizontal, gridAttrsX.adjustLowerBoundToGrid(), gridAttrsX.adjustUpperBoundToGrid() ); if ( dimX.stepWidth ) { //qDebug("CartesianGrid::calculateGrid() l.last().start: %f l.last().end: %f", l.last().start, l.last().end); //qDebug(" l.first().start: %f l.first().end: %f", l.first().start, l.first().end); // one time for the min/max value const DataDimension minMaxY = calculateGridXY( l.last(), Qt::Vertical, gridAttrsY.adjustLowerBoundToGrid(), gridAttrsY.adjustUpperBoundToGrid() ); if ( plane->autoAdjustGridToZoom() && plane->axesCalcModeY() == CartesianCoordinatePlane::Linear && plane->zoomFactorY() > 1.0 ) { l.last().start = translatedBottomLeft.y(); l.last().end = translatedTopRight.y(); } // and one other time for the step width const DataDimension dimY = calculateGridXY( l.last(), Qt::Vertical, gridAttrsY.adjustLowerBoundToGrid(), gridAttrsY.adjustUpperBoundToGrid() ); if ( dimY.stepWidth ) { l.first().start = dimX.start; l.first().end = dimX.end; l.first().stepWidth = dimX.stepWidth; l.first().subStepWidth = dimX.subStepWidth; l.last().start = minMaxY.start; l.last().end = minMaxY.end; l.last().stepWidth = dimY.stepWidth; l.last().subStepWidth = dimY.subStepWidth; //qDebug() << "CartesianGrid::calculateGrid() final grid y-range:" << l.last().end - l.last().start << " step width:" << l.last().stepWidth << endl; // calculate some reasonable subSteps if the // user did not set the sub grid but did set // the stepWidth. // FIXME (Johannes) // the last (y) dimension is not always the dimension for the ordinate! // since there's no way to check for the orientation of this dimension here, // we cannot automatically assume substep values //if ( dimY.subStepWidth == 0 ) // l.last().subStepWidth = dimY.stepWidth/2; //else // l.last().subStepWidth = dimY.subStepWidth; } } } #if 0 qDebug() << Q_FUNC_INFO << "final grid X-range:" << l.first().start << "->" << l.first().end << " substep width:" << l.first().subStepWidth; qDebug() << Q_FUNC_INFO << "final grid Y-range:" << l.last().start << "->" << l.last().end << " substep width:" << l.last().subStepWidth; #endif return l; } qreal fastPow10( int x ) { qreal res = 1.0; if ( 0 <= x ) { for ( int i = 1; i <= x; ++i ) res *= 10.0; } else { for ( int i = -1; i >= x; --i ) res *= 0.1; } return res; } #ifdef Q_OS_WIN #define trunc(x) ((int)(x)) #endif DataDimension CartesianGrid::calculateGridXY( const DataDimension& rawDataDimension, Qt::Orientation orientation, bool adjustLower, bool adjustUpper ) const { CartesianCoordinatePlane* const plane = dynamic_cast( mPlane ); if ( ( orientation == Qt::Vertical && plane->autoAdjustVerticalRangeToData() >= 100 ) || ( orientation == Qt::Horizontal && plane->autoAdjustHorizontalRangeToData() >= 100 ) ) { adjustLower = false; adjustUpper = false; } DataDimension dim( rawDataDimension ); if ( dim.isCalculated && dim.start != dim.end ) { if ( dim.calcMode == AbstractCoordinatePlane::Linear ) { // linear ( == not-logarithmic) calculation if ( dim.stepWidth == 0.0 ) { QList granularities; switch ( dim.sequence ) { case KChartEnums::GranularitySequence_10_20: granularities << 1.0 << 2.0; break; case KChartEnums::GranularitySequence_10_50: granularities << 1.0 << 5.0; break; case KChartEnums::GranularitySequence_25_50: granularities << 2.5 << 5.0; break; case KChartEnums::GranularitySequence_125_25: granularities << 1.25 << 2.5; break; case KChartEnums::GranularitySequenceIrregular: granularities << 1.0 << 1.25 << 2.0 << 2.5 << 5.0; break; } //qDebug("CartesianGrid::calculateGridXY() dim.start: %f dim.end: %f", dim.start, dim.end); calculateStepWidth( dim.start, dim.end, granularities, orientation, dim.stepWidth, dim.subStepWidth, adjustLower, adjustUpper ); } // if needed, adjust start/end to match the step width: //qDebug() << "CartesianGrid::calculateGridXY() has 1st linear range: min " << dim.start << " and max" << dim.end; AbstractGrid::adjustLowerUpperRange( dim.start, dim.end, dim.stepWidth, adjustLower, adjustUpper ); //qDebug() << "CartesianGrid::calculateGridXY() returns linear range: min " << dim.start << " and max" << dim.end; } else { // logarithmic calculation with negative values if ( dim.end <= 0 ) { qreal min; const qreal minRaw = qMin( dim.start, dim.end ); const int minLog = -static_cast(trunc( log10( -minRaw ) ) ); if ( minLog >= 0 ) min = qMin( minRaw, -std::numeric_limits< qreal >::epsilon() ); else min = -fastPow10( -(minLog-1) ); qreal max; const qreal maxRaw = qMin( -std::numeric_limits< qreal >::epsilon(), qMax( dim.start, dim.end ) ); const int maxLog = -static_cast(ceil( log10( -maxRaw ) ) ); if ( maxLog >= 0 ) max = -1; else if ( fastPow10( -maxLog ) < maxRaw ) max = -fastPow10( -(maxLog+1) ); else max = -fastPow10( -maxLog ); if ( adjustLower ) dim.start = min; if ( adjustUpper ) dim.end = max; dim.stepWidth = -pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) ); } // logarithmic calculation (ignoring all negative values) else { qreal min; const qreal minRaw = qMax( qMin( dim.start, dim.end ), qreal( 0.0 ) ); const int minLog = static_cast(trunc( log10( minRaw ) ) ); if ( minLog <= 0 && dim.end < 1.0 ) min = qMax( minRaw, std::numeric_limits< qreal >::epsilon() ); else if ( minLog <= 0 ) min = qMax( qreal(0.00001), dim.start ); else min = fastPow10( minLog-1 ); // Uh oh. Logarithmic scaling doesn't work with a lower or upper // bound being 0. const bool zeroBound = dim.start == 0.0 || dim.end == 0.0; qreal max; const qreal maxRaw = qMax( qMax( dim.start, dim.end ), qreal( 0.0 ) ); const int maxLog = static_cast(ceil( log10( maxRaw ) ) ); if ( maxLog <= 0 ) max = 1; else if ( fastPow10( maxLog ) < maxRaw ) max = fastPow10( maxLog+1 ); else max = fastPow10( maxLog ); if ( adjustLower || zeroBound ) dim.start = min; if ( adjustUpper || zeroBound ) dim.end = max; dim.stepWidth = pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) ); } } } else { //qDebug() << "CartesianGrid::calculateGridXY() returns stepWidth 1.0 !!"; // Do not ignore the user configuration dim.stepWidth = dim.stepWidth ? dim.stepWidth : 1.0; } return dim; } static void calculateSteps( qreal start_, qreal end_, const QList& list, int minSteps, int maxSteps, int power, qreal& steps, qreal& stepWidth, bool adjustLower, bool adjustUpper ) { //qDebug("-----------------------------------\nstart: %f end: %f power-of-ten: %i", start_, end_, power); qreal distance = 0.0; steps = 0.0; const int lastIdx = list.count()-1; for ( int i = 0; i <= lastIdx; ++i ) { const qreal testStepWidth = list.at(lastIdx - i) * fastPow10( power ); //qDebug( "testing step width: %f", testStepWidth); qreal start = qMin( start_, end_ ); qreal end = qMax( start_, end_ ); //qDebug("pre adjusting start: %f end: %f", start, end); AbstractGrid::adjustLowerUpperRange( start, end, testStepWidth, adjustLower, adjustUpper ); //qDebug("post adjusting start: %f end: %f", start, end); const qreal testDistance = qAbs(end - start); const qreal testSteps = testDistance / testStepWidth; //qDebug() << "testDistance:" << testDistance << " distance:" << distance; if ( (minSteps <= testSteps) && (testSteps <= maxSteps) && ( (steps == 0.0) || (testDistance <= distance) ) ) { steps = testSteps; stepWidth = testStepWidth; distance = testDistance; //qDebug( "start: %f end: %f step width: %f steps: %f distance: %f", start, end, stepWidth, steps, distance); } } } void CartesianGrid::calculateStepWidth( qreal start_, qreal end_, const QList& granularities, Qt::Orientation orientation, qreal& stepWidth, qreal& subStepWidth, bool adjustLower, bool adjustUpper ) const { Q_UNUSED( orientation ); Q_ASSERT_X ( granularities.count(), "CartesianGrid::calculateStepWidth", "Error: The list of GranularitySequence values is empty." ); QList list( granularities ); qSort( list ); const qreal start = qMin( start_, end_); const qreal end = qMax( start_, end_); const qreal distance = end - start; //qDebug( "raw data start: %f end: %f", start, end); qreal steps; int power = 0; while ( list.last() * fastPow10( power ) < distance ) { ++power; }; // We have the sequence *two* times in the calculation test list, // so we will be sure to find the best match: const int count = list.count(); QList testList; for ( int dec = -1; dec == -1 || fastPow10( dec + 1 ) >= distance; --dec ) for ( int i = 0; i < count; ++i ) testList << list.at(i) * fastPow10( dec ); testList << list; do { calculateSteps( start, end, testList, m_minsteps, m_maxsteps, power, steps, stepWidth, adjustLower, adjustUpper ); --power; } while ( steps == 0.0 ); ++power; //qDebug( "steps calculated: stepWidth: %f steps: %f", stepWidth, steps); // find the matching sub-grid line width in case it is // not set by the user if ( subStepWidth == 0.0 ) { if ( stepWidth == list.first() * fastPow10( power ) ) { subStepWidth = list.last() * fastPow10( power-1 ); //qDebug("A"); } else if ( stepWidth == list.first() * fastPow10( power-1 ) ) { subStepWidth = list.last() * fastPow10( power-2 ); //qDebug("B"); } else { qreal smallerStepWidth = list.first(); for ( int i = 1; i < list.count(); ++i ) { if ( stepWidth == list.at( i ) * fastPow10( power ) ) { subStepWidth = smallerStepWidth * fastPow10( power ); break; } if ( stepWidth == list.at( i ) * fastPow10( power-1 ) ) { subStepWidth = smallerStepWidth * fastPow10( power-1 ); break; } smallerStepWidth = list.at( i ); } } } //qDebug("CartesianGrid::calculateStepWidth() found stepWidth %f (%f steps) and sub-stepWidth %f", stepWidth, steps, subStepWidth); } diff --git a/src/KChart/KChartAbstractAreaBase.cpp b/src/KChart/KChartAbstractAreaBase.cpp index 1576443..3619d06 100644 --- a/src/KChart/KChartAbstractAreaBase.cpp +++ b/src/KChart/KChartAbstractAreaBase.cpp @@ -1,235 +1,235 @@ /* * 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 "KChartAbstractAreaBase.h" #include "KChartAbstractAreaBase_p.h" #include #include #include #include "KChartPainterSaver_p.h" #include "KChartPrintingParameters.h" #include "KChartMath_p.h" #include - +#include using namespace KChart; AbstractAreaBase::Private::Private() : visible( true ) { init(); } AbstractAreaBase::Private::~Private() {} void AbstractAreaBase::Private::init() { } AbstractAreaBase::AbstractAreaBase() : _d( new Private() ) { } AbstractAreaBase::~AbstractAreaBase() { delete _d; _d = 0; } void AbstractAreaBase::init() { } #define d d_func() bool AbstractAreaBase::compare( const AbstractAreaBase* other ) const { if ( other == this ) return true; if ( !other ) { return false; } return (frameAttributes() == other->frameAttributes()) && (backgroundAttributes() == other->backgroundAttributes()); } void AbstractAreaBase::alignToReferencePoint( const RelativePosition& position ) { Q_UNUSED( position ); // PENDING(kalle) FIXME qWarning( "Sorry, not implemented: void AbstractAreaBase::alignToReferencePoint( const RelativePosition& position )" ); } void AbstractAreaBase::setFrameAttributes( const FrameAttributes &a ) { if ( d->frameAttributes == a ) return; d->frameAttributes = a; positionHasChanged(); } FrameAttributes AbstractAreaBase::frameAttributes() const { return d->frameAttributes; } void AbstractAreaBase::setBackgroundAttributes( const BackgroundAttributes &a ) { if ( d->backgroundAttributes == a ) return; d->backgroundAttributes = a; positionHasChanged(); } BackgroundAttributes AbstractAreaBase::backgroundAttributes() const { return d->backgroundAttributes; } /* static */ void AbstractAreaBase::paintBackgroundAttributes( QPainter& painter, const QRect& rect, const KChart::BackgroundAttributes& attributes ) { if ( !attributes.isVisible() ) return; /* first draw the brush (may contain a pixmap)*/ if ( Qt::NoBrush != attributes.brush().style() ) { KChart::PainterSaver painterSaver( &painter ); painter.setPen( Qt::NoPen ); const QPointF newTopLeft( painter.deviceMatrix().map( rect.topLeft() ) ); painter.setBrushOrigin( newTopLeft ); painter.setBrush( attributes.brush() ); painter.drawRect( rect.adjusted( 0, 0, -1, -1 ) ); } /* next draw the backPixmap over the brush */ if ( !attributes.pixmap().isNull() && attributes.pixmapMode() != BackgroundAttributes::BackgroundPixmapModeNone ) { QPointF ol = rect.topLeft(); if ( BackgroundAttributes::BackgroundPixmapModeCentered == attributes.pixmapMode() ) { ol.setX( rect.center().x() - attributes.pixmap().width() / 2 ); ol.setY( rect.center().y() - attributes.pixmap().height()/ 2 ); painter.drawPixmap( ol, attributes.pixmap() ); } else { QMatrix m; qreal zW = (qreal)rect.width() / (qreal)attributes.pixmap().width(); qreal zH = (qreal)rect.height() / (qreal)attributes.pixmap().height(); switch ( attributes.pixmapMode() ) { case BackgroundAttributes::BackgroundPixmapModeScaled: { qreal z; z = qMin( zW, zH ); m.scale( z, z ); } break; case BackgroundAttributes::BackgroundPixmapModeStretched: m.scale( zW, zH ); break; default: ; // Cannot happen, previously checked } QPixmap pm = attributes.pixmap().transformed( m ); ol.setX( rect.center().x() - pm.width() / 2 ); ol.setY( rect.center().y() - pm.height()/ 2 ); painter.drawPixmap( ol, pm ); } } } /* static */ void AbstractAreaBase::paintFrameAttributes( QPainter& painter, const QRect& rect, const KChart::FrameAttributes& attributes ) { if ( !attributes.isVisible() ) return; // Note: We set the brush to NoBrush explicitly here. // Otherwise we might get a filled rectangle, so any // previously drawn background would be overwritten by that area. const QPen oldPen( painter.pen() ); const QBrush oldBrush( painter.brush() ); painter.setPen( PrintingParameters::scalePen( attributes.pen() ) ); painter.setBrush( Qt::NoBrush ); painter.drawRoundedRect( rect.adjusted( 0, 0, -1, -1 ), attributes.cornerRadius(), attributes.cornerRadius() ); painter.setBrush( oldBrush ); painter.setPen( oldPen ); } void AbstractAreaBase::paintBackground( QPainter& painter, const QRect& rect ) { Q_ASSERT_X ( d != 0, "AbstractAreaBase::paintBackground()", "Private class was not initialized!" ); PainterSaver painterSaver( &painter ); const qreal radius = d->frameAttributes.cornerRadius(); QPainterPath path; path.addRoundedRect( rect.adjusted( 0, 0, -1, -1 ), radius, radius ); painter.setClipPath(path); paintBackgroundAttributes( painter, rect, d->backgroundAttributes ); } void AbstractAreaBase::paintFrame( QPainter& painter, const QRect& rect ) { Q_ASSERT_X ( d != 0, "AbstractAreaBase::paintFrame()", "Private class was not initialized!" ); paintFrameAttributes( painter, rect, d->frameAttributes ); } void AbstractAreaBase::getFrameLeadings(int& left, int& top, int& right, int& bottom ) const { int padding = 0; if ( d && d->frameAttributes.isVisible() ) { padding = qMax( d->frameAttributes.padding(), 0 ); } left = padding; top = padding; right = padding; bottom = padding; } QRect AbstractAreaBase::innerRect() const { int left; int top; int right; int bottom; getFrameLeadings( left, top, right, bottom ); return QRect ( QPoint( 0, 0 ), areaGeometry().size() ).adjusted( left, top, -right, -bottom ); } void AbstractAreaBase::positionHasChanged() { // this bloc left empty intentionally } diff --git a/src/KChart/KChartAbstractDiagram.cpp b/src/KChart/KChartAbstractDiagram.cpp index df54cbd..ff9c276 100644 --- a/src/KChart/KChartAbstractDiagram.cpp +++ b/src/KChart/KChartAbstractDiagram.cpp @@ -1,1197 +1,1198 @@ /* * 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 "KChartAbstractDiagram.h" #include "KChartAbstractDiagram_p.h" #include +#include #include #include #include #include #include "KChartAbstractCoordinatePlane.h" #include "KChartChart.h" #include "KChartDataValueAttributes.h" #include "KChartTextAttributes.h" #include "KChartMarkerAttributes.h" #include "KChartAbstractThreeDAttributes.h" #include "KChartThreeDLineAttributes.h" #include "KChartPainterSaver_p.h" #include using namespace KChart; #define d d_func() AbstractDiagram::AbstractDiagram ( QWidget* parent, AbstractCoordinatePlane* plane ) : QAbstractItemView ( parent ), _d( new Private() ) { _d->init( plane ); init(); } AbstractDiagram::~AbstractDiagram() { emit aboutToBeDestroyed(); delete _d; } void AbstractDiagram::init() { _d->diagram = this; d->reverseMapper.setDiagram( this ); } bool AbstractDiagram::compare( const AbstractDiagram* other ) const { if ( other == this ) return true; if ( !other ) { return false; } return // compare QAbstractScrollArea properties (horizontalScrollBarPolicy() == other->horizontalScrollBarPolicy()) && (verticalScrollBarPolicy() == other->verticalScrollBarPolicy()) && // compare QFrame properties (frameShadow() == other->frameShadow()) && (frameShape() == other->frameShape()) && // frameWidth is a read-only property defined by the style, it should not be in here: // (frameWidth() == other->frameWidth()) && (lineWidth() == other->lineWidth()) && (midLineWidth() == other->midLineWidth()) && // compare QAbstractItemView properties (alternatingRowColors() == other->alternatingRowColors()) && (hasAutoScroll() == other->hasAutoScroll()) && (dragDropMode() == other->dragDropMode()) && (dragDropOverwriteMode() == other->dragDropOverwriteMode()) && (horizontalScrollMode() == other->horizontalScrollMode ()) && (verticalScrollMode() == other->verticalScrollMode()) && (dragEnabled() == other->dragEnabled()) && (editTriggers() == other->editTriggers()) && (iconSize() == other->iconSize()) && (selectionBehavior() == other->selectionBehavior()) && (selectionMode() == other->selectionMode()) && (showDropIndicator() == other->showDropIndicator()) && (tabKeyNavigation() == other->tabKeyNavigation()) && (textElideMode() == other->textElideMode()) && // compare all of the properties stored in the attributes model attributesModel()->compare( other->attributesModel() ) && // compare own properties (rootIndex().column() == other->rootIndex().column()) && (rootIndex().row() == other->rootIndex().row()) && (allowOverlappingDataValueTexts() == other->allowOverlappingDataValueTexts()) && (antiAliasing() == other->antiAliasing()) && (percentMode() == other->percentMode()) && (datasetDimension() == other->datasetDimension()); } AbstractCoordinatePlane* AbstractDiagram::coordinatePlane() const { return d->plane; } const QPair AbstractDiagram::dataBoundaries () const { if ( d->databoundariesDirty ) { d->databoundaries = calculateDataBoundaries (); d->databoundariesDirty = false; } return d->databoundaries; } void AbstractDiagram::setDataBoundariesDirty() const { d->databoundariesDirty = true; update(); } void AbstractDiagram::resize(const QSizeF& size) { d->diagramSize = size; QAbstractItemView::resize( size.toSize() ); } void AbstractDiagram::setModel( QAbstractItemModel * newModel ) { if ( newModel == model() ) { return; } AttributesModel* amodel = new PrivateAttributesModel( newModel, this ); amodel->initFrom( d->attributesModel ); d->setAttributesModel(amodel); QAbstractItemView::setModel( newModel ); scheduleDelayedItemsLayout(); setDataBoundariesDirty(); emit modelsChanged(); } void AbstractDiagram::setSelectionModel( QItemSelectionModel* newSelectionModel ) { if ( selectionModel() ) { disconnect( selectionModel(), SIGNAL( currentChanged( QModelIndex, QModelIndex ) ), this, SIGNAL( modelsChanged() ) ); disconnect( selectionModel(), SIGNAL( selectionChanged( QItemSelection, QItemSelection ) ), this, SIGNAL( modelsChanged() ) ); } QAbstractItemView::setSelectionModel( newSelectionModel ); if ( selectionModel() ) { connect( selectionModel(), SIGNAL( currentChanged( QModelIndex, QModelIndex ) ), this, SIGNAL( modelsChanged() ) ); connect( selectionModel(), SIGNAL( selectionChanged( QItemSelection, QItemSelection ) ), this, SIGNAL( modelsChanged() ) ); } emit modelsChanged(); } /*! Sets an external AttributesModel on this diagram. By default, a diagram has it's own internal set of attributes, but an external one can be set. This can be used to share attributes between several diagrams. The diagram does not take ownership of the attributesmodel. @param amodel The AttributesModel to use for this diagram. */ void AbstractDiagram::setAttributesModel( AttributesModel* amodel ) { if ( amodel->sourceModel() != model() ) { qWarning("KChart::AbstractDiagram::setAttributesModel() failed: " "Trying to set an attributesmodel which works on a different " "model than the diagram."); return; } if ( qobject_cast(amodel) ) { qWarning("KChart::AbstractDiagram::setAttributesModel() failed: " "Trying to set an attributesmodel that is private to another diagram."); return; } d->setAttributesModel( amodel ); scheduleDelayedItemsLayout(); setDataBoundariesDirty(); emit modelsChanged(); } bool AbstractDiagram::usesExternalAttributesModel() const { return d->usesExternalAttributesModel(); } AttributesModel* AbstractDiagram::attributesModel() const { return d->attributesModel; } QModelIndex AbstractDiagram::conditionallyMapFromSource( const QModelIndex & index ) const { Q_ASSERT( !index.isValid() || index.model() == attributesModel() || index.model() == attributesModel()->sourceModel() ); return index.model() == attributesModel() ? index : attributesModel()->mapFromSource( index ); } /*! \reimpl */ void AbstractDiagram::setRootIndex ( const QModelIndex& idx ) { QAbstractItemView::setRootIndex( idx ); setAttributesModelRootIndex( d->attributesModel->mapFromSource( idx ) ); } /*! \internal */ void AbstractDiagram::setAttributesModelRootIndex( const QModelIndex& idx ) { d->attributesModelRootIndex = idx; setDataBoundariesDirty(); scheduleDelayedItemsLayout(); } /*! returns a QModelIndex pointing into the AttributesModel that corresponds to the root index of the diagram. */ QModelIndex AbstractDiagram::attributesModelRootIndex() const { if ( !d->attributesModelRootIndex.isValid() ) d->attributesModelRootIndex = d->attributesModel->mapFromSource( rootIndex() ); return d->attributesModelRootIndex; } void AbstractDiagram::setCoordinatePlane( AbstractCoordinatePlane* parent ) { d->plane = parent; } void AbstractDiagram::doItemsLayout() { if ( d->plane ) { d->plane->layoutDiagrams(); update(); } QAbstractItemView::doItemsLayout(); } void AbstractDiagram::dataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector & ) { Q_UNUSED( topLeft ); Q_UNUSED( bottomRight ); // We are still too dumb to do intelligent updates... setDataBoundariesDirty(); scheduleDelayedItemsLayout(); } void AbstractDiagram::setHidden( const QModelIndex & index, bool hidden ) { d->attributesModel->setData( conditionallyMapFromSource( index ), qVariantFromValue( hidden ), DataHiddenRole ); emit dataHidden(); } void AbstractDiagram::setHidden( int dataset, bool hidden ) { d->setDatasetAttrs( dataset, qVariantFromValue( hidden ), DataHiddenRole ); emit dataHidden(); } void AbstractDiagram::setHidden( bool hidden ) { d->attributesModel->setModelData( qVariantFromValue( hidden ), DataHiddenRole ); emit dataHidden(); } bool AbstractDiagram::isHidden() const { return attributesModel()->modelData( DataHiddenRole ).value< bool >(); } bool AbstractDiagram::isHidden( int dataset ) const { const QVariant boolFlag( d->datasetAttrs( dataset, DataHiddenRole ) ); if ( boolFlag.isValid() ) return boolFlag.value< bool >(); return isHidden(); } bool AbstractDiagram::isHidden( const QModelIndex & index ) const { const QVariant boolFlag( attributesModel()->data( conditionallyMapFromSource( index ), DataHiddenRole ) ); if ( boolFlag.isValid() ) { return boolFlag.value< bool >(); } int dataset = index.column() / d->datasetDimension; return isHidden( dataset ); } void AbstractDiagram::setDataValueAttributes( const QModelIndex & index, const DataValueAttributes & a ) { d->attributesModel->setData( conditionallyMapFromSource( index ), qVariantFromValue( a ), DataValueLabelAttributesRole ); emit propertiesChanged(); } void AbstractDiagram::setDataValueAttributes( int dataset, const DataValueAttributes & a ) { d->setDatasetAttrs( dataset, qVariantFromValue( a ), DataValueLabelAttributesRole ); emit propertiesChanged(); } DataValueAttributes AbstractDiagram::dataValueAttributes() const { return attributesModel()->modelData( KChart::DataValueLabelAttributesRole ).value< DataValueAttributes >(); } DataValueAttributes AbstractDiagram::dataValueAttributes( int dataset ) const { /* The following did not work! (khz, 2008-01-25) If there was some attrs specified for the 0-th cells of a dataset, then this logic would return the cell's settings instead of the header settings: return qVariantValue( attributesModel()->data( attributesModel()->mapFromSource(columnToIndex( column )), KChart::DataValueLabelAttributesRole ) ); */ const QVariant headerAttrs( d->datasetAttrs( dataset, KChart::DataValueLabelAttributesRole ) ); if ( headerAttrs.isValid() ) return headerAttrs.value< DataValueAttributes >(); return dataValueAttributes(); } DataValueAttributes AbstractDiagram::dataValueAttributes( const QModelIndex & index ) const { return attributesModel()->data( conditionallyMapFromSource( index ), KChart::DataValueLabelAttributesRole ).value< DataValueAttributes >(); } void AbstractDiagram::setDataValueAttributes( const DataValueAttributes & a ) { d->attributesModel->setModelData( qVariantFromValue( a ), DataValueLabelAttributesRole ); emit propertiesChanged(); } void AbstractDiagram::setAllowOverlappingDataValueTexts( bool allow ) { DataValueAttributes attrs = dataValueAttributes(); attrs.setShowOverlappingDataLabels( allow ); setDataValueAttributes( attrs ); d->allowOverlappingDataValueTexts = allow; emit propertiesChanged(); } bool AbstractDiagram::allowOverlappingDataValueTexts() const { return d->allowOverlappingDataValueTexts; } void AbstractDiagram::setAntiAliasing( bool enabled ) { d->antiAliasing = enabled; emit propertiesChanged(); } bool AbstractDiagram::antiAliasing() const { return d->antiAliasing; } void AbstractDiagram::setPercentMode ( bool percent ) { d->percent = percent; emit propertiesChanged(); } bool AbstractDiagram::percentMode() const { return d->percent; } void AbstractDiagram::paintDataValueText( QPainter* painter, const QModelIndex& index, const QPointF& pos, qreal value ) { d->paintDataValueText( painter, index, pos, value ); } void AbstractDiagram::paintDataValueTexts( QPainter* painter ) { if ( !checkInvariants() ) { return; } d->forgetAlreadyPaintedDataValues(); const int rowCount = model()->rowCount( rootIndex() ); const int columnCount = model()->columnCount( rootIndex() ); for ( int column = 0; column < columnCount; column += datasetDimension() ) { for ( int row = 0; row < rowCount; ++row ) { QModelIndex index = model()->index( row, column, rootIndex() ); // checked qreal x; qreal y; if ( datasetDimension() == 1 ) { x = row; y = index.data().toReal(); } else { x = index.data().toReal(); y = model()->index( row, column + 1, rootIndex() ).data().toReal(); } paintDataValueText( painter, index, coordinatePlane()->translate( QPointF( x, y ) ), y ); } } } void AbstractDiagram::paintMarker( QPainter* painter, const DataValueAttributes& a, const QModelIndex& index, const QPointF& pos ) { if ( !checkInvariants() || !a.isVisible() ) return; const MarkerAttributes ma = a.markerAttributes(); if ( !ma.isVisible() ) return; const PainterSaver painterSaver( painter ); QSizeF maSize = ma.markerSize(); const qreal diagramWidth = d->diagramSize.width(); const qreal diagramHeight = d->diagramSize.height(); switch( ma.markerSizeMode() ) { case MarkerAttributes::AbsoluteSize: // Unscaled, i.e. without the painter's "zoom" maSize.rwidth() /= painter->matrix().m11(); maSize.rheight() /= painter->matrix().m22(); break; case MarkerAttributes::AbsoluteSizeScaled: // Keep maSize as is. It is specified directly in pixels and desired // to be effected by the painter's "zoom". break; case MarkerAttributes::RelativeToDiagramWidthHeightMin: maSize *= qMin( diagramWidth, diagramHeight ); break; } QBrush indexBrush( brush( index ) ); QPen indexPen( ma.pen() ); if ( ma.markerColor().isValid() ) indexBrush.setColor( ma.markerColor() ); paintMarker( painter, ma, indexBrush, indexPen, pos, maSize ); // workaround: BC cannot be changed, otherwise we would pass the // index down to next-lower paintMarker function. So far, we // basically save a circle of radius maSize at pos in the // reverseMapper. This means that ^^^ this version of paintMarker // needs to be called to reverse-map the marker. d->reverseMapper.addCircle( index.row(), index.column(), pos, 2 * maSize ); } void AbstractDiagram::paintMarker( QPainter* painter, const QModelIndex& index, const QPointF& pos ) { if ( !checkInvariants() ) return; paintMarker( painter, dataValueAttributes( index ), index, pos ); } void AbstractDiagram::paintMarker( QPainter* painter, const MarkerAttributes& markerAttributes, const QBrush& brush, const QPen& pen, const QPointF& pos, const QSizeF& maSize ) { const QPen oldPen( painter->pen() ); // Pen is used to paint 4Pixels - 1 Pixel - Ring and FastCross types. // make sure to use the brush color - see above in those cases. const bool isFourPixels = (markerAttributes.markerStyle() == MarkerAttributes::Marker4Pixels); if ( isFourPixels || (markerAttributes.markerStyle() == MarkerAttributes::Marker1Pixel) ) { // for high-performance point charts with tiny point markers: painter->setPen( PrintingParameters::scalePen( QPen( brush.color().light() ) ) ); if ( isFourPixels ) { const qreal x = pos.x(); const qreal y = pos.y(); painter->drawLine( QPointF(x-1.0,y-1.0), QPointF(x+1.0,y-1.0) ); painter->drawLine( QPointF(x-1.0,y), QPointF(x+1.0,y) ); painter->drawLine( QPointF(x-1.0,y+1.0), QPointF(x+1.0,y+1.0) ); } painter->drawPoint( pos ); } else { const PainterSaver painterSaver( painter ); QPen painterPen( pen ); painter->setPen( PrintingParameters::scalePen( painterPen ) ); painter->setBrush( brush ); painter->setRenderHint ( QPainter::Antialiasing ); painter->translate( pos ); switch ( markerAttributes.markerStyle() ) { case MarkerAttributes::MarkerCircle: { if ( markerAttributes.threeD() ) { QRadialGradient grad; grad.setCoordinateMode( QGradient::ObjectBoundingMode ); QColor drawColor = brush.color(); grad.setCenter( 0.5, 0.5 ); grad.setRadius( 1.0 ); grad.setFocalPoint( 0.35, 0.35 ); grad.setColorAt( 0.00, drawColor.lighter( 150 ) ); grad.setColorAt( 0.20, drawColor ); grad.setColorAt( 0.50, drawColor.darker( 150 ) ); grad.setColorAt( 0.75, drawColor.darker( 200 ) ); grad.setColorAt( 0.95, drawColor.darker( 250 ) ); grad.setColorAt( 1.00, drawColor.darker( 200 ) ); QBrush newBrush( grad ); newBrush.setMatrix( brush.matrix() ); painter->setBrush( newBrush ); } painter->drawEllipse( QRectF( 0 - maSize.height()/2, 0 - maSize.width()/2, maSize.height(), maSize.width()) ); } break; case MarkerAttributes::MarkerSquare: { QRectF rect( 0 - maSize.width()/2, 0 - maSize.height()/2, maSize.width(), maSize.height() ); painter->drawRect( rect ); break; } case MarkerAttributes::MarkerDiamond: { QVector diamondPoints; QPointF top, left, bottom, right; top = QPointF( 0, 0 - maSize.height()/2 ); left = QPointF( 0 - maSize.width()/2, 0 ); bottom = QPointF( 0, maSize.height()/2 ); right = QPointF( maSize.width()/2, 0 ); diamondPoints << top << left << bottom << right; painter->drawPolygon( diamondPoints ); break; } // both handled on top of the method: case MarkerAttributes::Marker1Pixel: case MarkerAttributes::Marker4Pixels: break; case MarkerAttributes::MarkerRing: { painter->setBrush( Qt::NoBrush ); painter->setPen( PrintingParameters::scalePen( QPen( brush.color() ) ) ); painter->drawEllipse( QRectF( 0 - maSize.height()/2, 0 - maSize.width()/2, maSize.height(), maSize.width()) ); break; } case MarkerAttributes::MarkerCross: { // Note: Markers can have outline, // so just drawing two rects is NOT the solution here! const qreal w02 = maSize.width() * 0.2; const qreal w05 = maSize.width() * 0.5; const qreal h02 = maSize.height()* 0.2; const qreal h05 = maSize.height()* 0.5; QVector crossPoints; QPointF p[12]; p[ 0] = QPointF( -w02, -h05 ); p[ 1] = QPointF( w02, -h05 ); p[ 2] = QPointF( w02, -h02 ); p[ 3] = QPointF( w05, -h02 ); p[ 4] = QPointF( w05, h02 ); p[ 5] = QPointF( w02, h02 ); p[ 6] = QPointF( w02, h05 ); p[ 7] = QPointF( -w02, h05 ); p[ 8] = QPointF( -w02, h02 ); p[ 9] = QPointF( -w05, h02 ); p[10] = QPointF( -w05, -h02 ); p[11] = QPointF( -w02, -h02 ); for ( int i=0; i<12; ++i ) crossPoints << p[i]; crossPoints << p[0]; painter->drawPolygon( crossPoints ); break; } case MarkerAttributes::MarkerFastCross: { QPointF left, right, top, bottom; left = QPointF( -maSize.width()/2, 0 ); right = QPointF( maSize.width()/2, 0 ); top = QPointF( 0, -maSize.height()/2 ); bottom= QPointF( 0, maSize.height()/2 ); painter->setPen( PrintingParameters::scalePen( QPen( brush.color() ) ) ); painter->drawLine( left, right ); painter->drawLine( top, bottom ); break; } case MarkerAttributes::MarkerArrowDown: { QVector arrowPoints; QPointF topLeft, topRight, bottom; topLeft = QPointF( 0 - maSize.width()/2, 0 - maSize.height()/2 ); topRight = QPointF( maSize.width()/2, 0 - maSize.height()/2 ); bottom = QPointF( 0, maSize.height()/2 ); arrowPoints << topLeft << bottom << topRight; painter->drawPolygon( arrowPoints ); break; } case MarkerAttributes::MarkerArrowUp: { QVector arrowPoints; QPointF top, bottomLeft, bottomRight; top = QPointF( 0, 0 - maSize.height()/2 ); bottomLeft = QPointF( 0 - maSize.width()/2, maSize.height()/2 ); bottomRight = QPointF( maSize.width()/2, maSize.height()/2 ); arrowPoints << top << bottomLeft << bottomRight; painter->drawPolygon( arrowPoints ); break; } case MarkerAttributes::MarkerArrowRight: { QVector arrowPoints; QPointF right, topLeft, bottomLeft; right = QPointF( maSize.width()/2, 0 ); topLeft = QPointF( 0 - maSize.width()/2, 0 - maSize.height()/2 ); bottomLeft = QPointF( 0 - maSize.width()/2, maSize.height()/2 ); arrowPoints << topLeft << bottomLeft << right; painter->drawPolygon( arrowPoints ); break; } case MarkerAttributes::MarkerArrowLeft: { QVector arrowPoints; QPointF left, topRight, bottomRight; left = QPointF( 0 - maSize.width()/2, 0 ); topRight = QPointF( maSize.width()/2, 0 - maSize.height()/2 ); bottomRight = QPointF( maSize.width()/2, maSize.height()/2 ); arrowPoints << left << bottomRight << topRight; painter->drawPolygon( arrowPoints ); break; } case MarkerAttributes::MarkerBowTie: case MarkerAttributes::MarkerHourGlass: { QVector points; QPointF topLeft, topRight, bottomLeft, bottomRight; topLeft = QPointF( 0 - maSize.width()/2, 0 - maSize.height()/2); topRight = QPointF( maSize.width()/2, 0 - maSize.height()/2 ); bottomLeft = QPointF( 0 - maSize.width()/2, maSize.height()/2 ); bottomRight = QPointF( maSize.width()/2, maSize.height()/2 ); if ( markerAttributes.markerStyle() == MarkerAttributes::MarkerBowTie) points << topLeft << bottomLeft << topRight << bottomRight; else points << topLeft << bottomRight << bottomLeft << topRight; painter->drawPolygon( points ); break; } case MarkerAttributes::MarkerStar: { const qreal w01 = maSize.width() * 0.1; const qreal w05 = maSize.width() * 0.5; const qreal h01 = maSize.height() * 0.1; const qreal h05 = maSize.height() * 0.5; QVector points; QPointF p1 = QPointF( 0, -h05 ); QPointF p2 = QPointF( -w01, -h01 ); QPointF p3 = QPointF( -w05, 0 ); QPointF p4 = QPointF( -w01, h01 ); QPointF p5 = QPointF( 0, h05 ); QPointF p6 = QPointF( w01, h01 ); QPointF p7 = QPointF( w05, 0 ); QPointF p8 = QPointF( w01, -h01 ); points << p1 << p2 << p3 << p4 << p5 << p6 << p7 << p8; painter->drawPolygon( points ); break; } case MarkerAttributes::MarkerX: { const qreal w01 = maSize.width() * 0.1; const qreal w04 = maSize.width() * 0.4; const qreal w05 = maSize.width() * 0.5; const qreal h01 = maSize.height() * 0.1; const qreal h04 = maSize.height() * 0.4; const qreal h05 = maSize.height() * 0.5; QVector crossPoints; QPointF p1 = QPointF( -w04, -h05 ); QPointF p2 = QPointF( -w05, -h04 ); QPointF p3 = QPointF( -w01, 0 ); QPointF p4 = QPointF( -w05, h04 ); QPointF p5 = QPointF( -w04, h05 ); QPointF p6 = QPointF( 0, h01 ); QPointF p7 = QPointF( w04, h05 ); QPointF p8 = QPointF( w05, h04 ); QPointF p9 = QPointF( w01, 0 ); QPointF p10 = QPointF( w05, -h04 ); QPointF p11 = QPointF( w04, -h05 ); QPointF p12 = QPointF( 0, -h01 ); crossPoints << p1 << p2 << p3 << p4 << p5 << p6 << p7 << p8 << p9 << p10 << p11 << p12; painter->drawPolygon( crossPoints ); break; } case MarkerAttributes::MarkerAsterisk: { // Note: Markers can have outline, // so just drawing three lines is NOT the solution here! // The idea that we use is to draw 3 lines anyway, but convert their // outlines to QPainterPaths which are then united and filled. const qreal w04 = maSize.width() * 0.4; const qreal h02 = maSize.height() * 0.2; const qreal h05 = maSize.height() * 0.5; //QVector crossPoints; QPointF p1 = QPointF( 0, -h05 ); QPointF p2 = QPointF( -w04, -h02 ); QPointF p3 = QPointF( -w04, h02 ); QPointF p4 = QPointF( 0, h05 ); QPointF p5 = QPointF( w04, h02 ); QPointF p6 = QPointF( w04, -h02 ); QPen pen = painter->pen(); QPainterPathStroker stroker; stroker.setWidth( pen.widthF() ); stroker.setCapStyle( pen.capStyle() ); QPainterPath path; QPainterPath dummyPath; dummyPath.moveTo( p1 ); dummyPath.lineTo( p4 ); path = stroker.createStroke( dummyPath ); dummyPath = QPainterPath(); dummyPath.moveTo( p2 ); dummyPath.lineTo( p5 ); path = path.united( stroker.createStroke( dummyPath ) ); dummyPath = QPainterPath(); dummyPath.moveTo( p3 ); dummyPath.lineTo( p6 ); path = path.united( stroker.createStroke( dummyPath ) ); painter->drawPath( path ); break; } case MarkerAttributes::MarkerHorizontalBar: { const qreal w05 = maSize.width() * 0.5; const qreal h02 = maSize.height()* 0.2; QVector points; QPointF p1 = QPointF( -w05, -h02 ); QPointF p2 = QPointF( -w05, h02 ); QPointF p3 = QPointF( w05, h02 ); QPointF p4 = QPointF( w05, -h02 ); points << p1 << p2 << p3 << p4; painter->drawPolygon( points ); break; } case MarkerAttributes::MarkerVerticalBar: { const qreal w02 = maSize.width() * 0.2; const qreal h05 = maSize.height()* 0.5; QVector points; QPointF p1 = QPointF( -w02, -h05 ); QPointF p2 = QPointF( -w02, h05 ); QPointF p3 = QPointF( w02, h05 ); QPointF p4 = QPointF( w02, -h05 ); points << p1 << p2 << p3 << p4; painter->drawPolygon( points ); break; } case MarkerAttributes::NoMarker: break; case MarkerAttributes::PainterPathMarker: { QPainterPath path = markerAttributes.customMarkerPath(); const QRectF pathBoundingRect = path.boundingRect(); const qreal xScaling = maSize.height() / pathBoundingRect.height(); const qreal yScaling = maSize.width() / pathBoundingRect.width(); const qreal scaling = qMin( xScaling, yScaling ); painter->scale( scaling, scaling ); painter->setPen( PrintingParameters::scalePen( QPen( brush.color() ) ) ); painter->drawPath(path); break; } default: Q_ASSERT_X ( false, "paintMarkers()", "Type item does not match a defined Marker Type." ); } } painter->setPen( oldPen ); } void AbstractDiagram::paintMarkers( QPainter* painter ) { if ( !checkInvariants() ) { return; } const int rowCount = model()->rowCount( rootIndex() ); const int columnCount = model()->columnCount( rootIndex() ); for ( int column = 0; column < columnCount; column += datasetDimension() ) { for ( int row = 0; row < rowCount; ++row ) { QModelIndex index = model()->index( row, column, rootIndex() ); // checked qreal x; qreal y; if ( datasetDimension() == 1 ) { x = row; y = index.data().toReal(); } else { x = index.data().toReal(); y = model()->index( row, column + 1, rootIndex() ).data().toReal(); } paintMarker( painter, index, coordinatePlane()->translate( QPointF( x, y ) ) ); } } } void AbstractDiagram::setPen( const QModelIndex& index, const QPen& pen ) { attributesModel()->setData( conditionallyMapFromSource( index ), qVariantFromValue( pen ), DatasetPenRole ); emit propertiesChanged(); } void AbstractDiagram::setPen( const QPen& pen ) { attributesModel()->setModelData( qVariantFromValue( pen ), DatasetPenRole ); emit propertiesChanged(); } void AbstractDiagram::setPen( int dataset, const QPen& pen ) { d->setDatasetAttrs( dataset, qVariantFromValue( pen ), DatasetPenRole ); emit propertiesChanged(); } QPen AbstractDiagram::pen() const { return attributesModel()->data( DatasetPenRole ).value< QPen >(); } QPen AbstractDiagram::pen( int dataset ) const { const QVariant penSettings( d->datasetAttrs( dataset, DatasetPenRole ) ); if ( penSettings.isValid() ) return penSettings.value< QPen >(); return pen(); } QPen AbstractDiagram::pen( const QModelIndex& index ) const { return attributesModel()->data( conditionallyMapFromSource( index ), DatasetPenRole ).value< QPen >(); } void AbstractDiagram::setBrush( const QModelIndex& index, const QBrush& brush ) { attributesModel()->setData( conditionallyMapFromSource( index ), qVariantFromValue( brush ), DatasetBrushRole ); emit propertiesChanged(); } void AbstractDiagram::setBrush( const QBrush& brush ) { attributesModel()->setModelData( qVariantFromValue( brush ), DatasetBrushRole ); emit propertiesChanged(); } void AbstractDiagram::setBrush( int dataset, const QBrush& brush ) { d->setDatasetAttrs( dataset, qVariantFromValue( brush ), DatasetBrushRole ); emit propertiesChanged(); } QBrush AbstractDiagram::brush() const { return attributesModel()->data( DatasetBrushRole ).value< QBrush >(); } QBrush AbstractDiagram::brush( int dataset ) const { const QVariant brushSettings( d->datasetAttrs( dataset, DatasetBrushRole ) ); if ( brushSettings.isValid() ) return brushSettings.value< QBrush >(); return brush(); } QBrush AbstractDiagram::brush( const QModelIndex& index ) const { return attributesModel()->data( conditionallyMapFromSource( index ), DatasetBrushRole ).value< QBrush >(); } /** * Sets the unit prefix for one value * @param prefix the prefix to be set * @param column the value using that prefix * @param orientation the orientantion of the axis to set */ void AbstractDiagram::setUnitPrefix( const QString& prefix, int column, Qt::Orientation orientation ) { d->unitPrefixMap[ column ][ orientation ]= prefix; } /** * Sets the unit prefix for all values * @param prefix the prefix to be set * @param orientation the orientantion of the axis to set */ void AbstractDiagram::setUnitPrefix( const QString& prefix, Qt::Orientation orientation ) { d->unitPrefix[ orientation ] = prefix; } /** * Sets the unit suffix for one value * @param suffix the suffix to be set * @param column the value using that suffix * @param orientation the orientantion of the axis to set */ void AbstractDiagram::setUnitSuffix( const QString& suffix, int column, Qt::Orientation orientation ) { d->unitSuffixMap[ column ][ orientation ]= suffix; } /** * Sets the unit suffix for all values * @param suffix the suffix to be set * @param orientation the orientantion of the axis to set */ void AbstractDiagram::setUnitSuffix( const QString& suffix, Qt::Orientation orientation ) { d->unitSuffix[ orientation ] = suffix; } /** * Returns the unit prefix for a special value * @param column the value which's prefix is requested * @param orientation the orientation of the axis * @param fallback if true, the global prefix is return when no specific one is set for that value * @return the unit prefix */ QString AbstractDiagram::unitPrefix( int column, Qt::Orientation orientation, bool fallback ) const { if ( !fallback || d->unitPrefixMap[ column ].contains( orientation ) ) return d->unitPrefixMap[ column ][ orientation ]; return d->unitPrefix[ orientation ]; } /** Returns the global unit prefix * @param orientation the orientation of the axis * @return the unit prefix */ QString AbstractDiagram::unitPrefix( Qt::Orientation orientation ) const { return d->unitPrefix[ orientation ]; } /** * Returns the unit suffix for a special value * @param column the value which's suffix is requested * @param orientation the orientation of the axis * @param fallback if true, the global suffix is return when no specific one is set for that value * @return the unit suffix */ QString AbstractDiagram::unitSuffix( int column, Qt::Orientation orientation, bool fallback ) const { if ( !fallback || d->unitSuffixMap[ column ].contains( orientation ) ) return d->unitSuffixMap[ column ][ orientation ]; return d->unitSuffix[ orientation ]; } /** Returns the global unit suffix * @param orientation the orientation of the axis * @return the unit siffix */ QString AbstractDiagram::unitSuffix( Qt::Orientation orientation ) const { return d->unitSuffix[ orientation ]; } // implement QAbstractItemView: QRect AbstractDiagram::visualRect( const QModelIndex &index ) const { return d->reverseMapper.boundingRect( index.row(), index.column() ).toRect(); } void AbstractDiagram::scrollTo(const QModelIndex &, ScrollHint ) {} // indexAt ... down below QModelIndex AbstractDiagram::moveCursor(CursorAction, Qt::KeyboardModifiers ) { return QModelIndex(); } int AbstractDiagram::horizontalOffset() const { return 0; } int AbstractDiagram::verticalOffset() const { return 0; } bool AbstractDiagram::isIndexHidden(const QModelIndex &) const { return true; } void AbstractDiagram::setSelection(const QRect& rect , QItemSelectionModel::SelectionFlags command ) { const QModelIndexList indexes = d->indexesIn( rect ); QItemSelection selection; Q_FOREACH( const QModelIndex& index, indexes ) { selection.append( QItemSelectionRange( index ) ); } selectionModel()->select( selection, command ); } QRegion AbstractDiagram::visualRegionForSelection(const QItemSelection &selection) const { QPolygonF polygon; Q_FOREACH( const QModelIndex& index, selection.indexes() ) { polygon << d->reverseMapper.polygon(index.row(), index.column()); } return polygon.isEmpty() ? QRegion() : QRegion( polygon.toPolygon() ); } QRegion AbstractDiagram::visualRegion(const QModelIndex &index) const { QPolygonF polygon = d->reverseMapper.polygon(index.row(), index.column()); return polygon.isEmpty() ? QRegion() : QRegion( polygon.toPolygon() ); } void KChart::AbstractDiagram::useDefaultColors( ) { d->attributesModel->setPaletteType( AttributesModel::PaletteTypeDefault ); } void KChart::AbstractDiagram::useSubduedColors( ) { d->attributesModel->setPaletteType( AttributesModel::PaletteTypeSubdued ); } void KChart::AbstractDiagram::useRainbowColors( ) { d->attributesModel->setPaletteType( AttributesModel::PaletteTypeRainbow ); } QStringList AbstractDiagram::itemRowLabels() const { QStringList ret; if ( model() ) { //qDebug() << "AbstractDiagram::itemRowLabels(): " << attributesModel()->rowCount(attributesModelRootIndex()) << "entries"; const int rowCount = attributesModel()->rowCount(attributesModelRootIndex()); for ( int i = 0; i < rowCount; ++i ) { //qDebug() << "item row label: " << attributesModel()->headerData( i, Qt::Vertical, Qt::DisplayRole ).toString(); ret << unitPrefix( i, Qt::Horizontal, true ) + attributesModel()->headerData( i, Qt::Vertical, Qt::DisplayRole ).toString() + unitSuffix( i, Qt::Horizontal, true ); } } return ret; } QStringList AbstractDiagram::datasetLabels() const { QStringList ret; if ( !model() ) { return ret; } const int datasetCount = d->datasetCount(); for ( int i = 0; i < datasetCount; ++i ) { ret << d->datasetAttrs( i, Qt::DisplayRole ).toString(); } return ret; } QList AbstractDiagram::datasetBrushes() const { QList ret; if ( !model() ) { return ret; } const int datasetCount = d->datasetCount(); for ( int i = 0; i < datasetCount; ++i ) { ret << brush( i ); } return ret; } QList AbstractDiagram::datasetPens() const { QList ret; if ( !model() ) { return ret; } const int datasetCount = d->datasetCount(); for ( int i = 0; i < datasetCount; ++i ) { ret << pen( i ); } return ret; } QList AbstractDiagram::datasetMarkers() const { QList ret; if ( !model() ) { return ret; } const int datasetCount = d->datasetCount(); for ( int i = 0; i < datasetCount; ++i ) { ret << dataValueAttributes( i ).markerAttributes(); } return ret; } bool AbstractDiagram::checkInvariants( bool justReturnTheStatus ) const { if ( ! justReturnTheStatus ) { Q_ASSERT_X ( model(), "AbstractDiagram::checkInvariants()", "There is no usable model set, for the diagram." ); Q_ASSERT_X ( coordinatePlane(), "AbstractDiagram::checkInvariants()", "There is no usable coordinate plane set, for the diagram." ); } return model() && coordinatePlane(); } int AbstractDiagram::datasetDimension( ) const { return d->datasetDimension; } void AbstractDiagram::setDatasetDimension( int dimension ) { Q_UNUSED( dimension ); qDebug() << "Setting the dataset dimension using AbstractDiagram::setDatasetDimension is " "obsolete. Use the specific diagram types instead."; } void AbstractDiagram::setDatasetDimensionInternal( int dimension ) { Q_ASSERT( dimension != 0 ); if ( d->datasetDimension == dimension ) { return; } d->datasetDimension = dimension; d->attributesModel->setDatasetDimension( dimension ); setDataBoundariesDirty(); emit layoutChanged( this ); } qreal AbstractDiagram::valueForCell( int row, int column ) const { if ( !d->attributesModel->hasIndex( row, column, attributesModelRootIndex() ) ) { qWarning() << "AbstractDiagram::valueForCell(): Requesting value for invalid index!"; return std::numeric_limits::quiet_NaN(); } return d->attributesModel->data( d->attributesModel->index( row, column, attributesModelRootIndex() ) ).toReal(); // checked } void AbstractDiagram::update() const { if ( d->plane ) { d->plane->update(); } } QModelIndex AbstractDiagram::indexAt( const QPoint& point ) const { return d->indexAt( point ); } QModelIndexList AbstractDiagram::indexesAt( const QPoint& point ) const { return d->indexesAt( point ); } QModelIndexList AbstractDiagram::indexesIn( const QRect& rect ) const { return d->indexesIn( rect ); } diff --git a/src/KChart/KChartAbstractDiagram_p.h b/src/KChart/KChartAbstractDiagram_p.h index c746278..08d9f4c 100644 --- a/src/KChart/KChartAbstractDiagram_p.h +++ b/src/KChart/KChartAbstractDiagram_p.h @@ -1,249 +1,250 @@ /* * 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 . */ #ifndef KCHARTABSTRACTDIAGRAM_P_H #define KCHARTABSTRACTDIAGRAM_P_H // // 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.h" #include "KChartAbstractCoordinatePlane.h" #include "KChartDataValueAttributes.h" #include "KChartBackgroundAttributes.h" #include "KChartRelativePosition.h" #include "KChartPosition.h" #include "KChartPaintContext.h" #include "KChartPrintingParameters.h" #include "KChartChart.h" #include #include "ReverseMapper.h" #include #include #include #include #include #include +#include #include namespace KChart { class LabelPaintInfo { public: LabelPaintInfo(); LabelPaintInfo( const QModelIndex& _index, const DataValueAttributes& _attrs, const QPainterPath& _labelArea, const QPointF& _markerPos, bool _isValuePositive, const QString& _value ); LabelPaintInfo( const LabelPaintInfo& other ); QModelIndex index; DataValueAttributes attrs; QPainterPath labelArea; QPointF markerPos; bool isValuePositive; // could (ab)use attrs.dataLabel() instead QString value; }; class LabelPaintCache { public: LabelPaintCache() {} ~LabelPaintCache() { clear(); } void clear() { paintReplay.clear(); } QVector paintReplay; private: LabelPaintCache( LabelPaintCache& other ); // no copies }; /** * \internal */ class AttributesModel; class Q_DECL_HIDDEN KChart::AbstractDiagram::Private { friend class AbstractDiagram; public: explicit Private(); virtual ~Private(); Private( const Private& rhs ); void setAttributesModel( AttributesModel* ); bool usesExternalAttributesModel() const; // FIXME: Optimize if necessary virtual qreal calcPercentValue( const QModelIndex & index ) const; // this should possibly be virtual so it can be overridden void 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 ); const QFontMetrics* cachedFontMetrics( const QFont& font, const QPaintDevice* paintDevice) const; const QFontMetrics cachedFontMetrics() const; QString formatNumber( qreal value, int decimalDigits ) const; QString formatDataValueText( const DataValueAttributes &dva, const QModelIndex& index, qreal value ) const; void forgetAlreadyPaintedDataValues(); void paintDataValueTextsAndMarkers( PaintContext* ctx, const LabelPaintCache& cache, bool paintMarkers, bool justCalculateRect=false, QRectF* cumulatedBoundingRect=0 ); void paintDataValueText( QPainter* painter, const QModelIndex& index, const QPointF& pos, qreal value, bool justCalculateRect=false, QRectF* cumulatedBoundingRect=0 ); void paintDataValueText( QPainter* painter, const DataValueAttributes& attrs, const QPointF& pos, bool valueIsPositive, const QString& text, bool justCalculateRect=false, QRectF* cumulatedBoundingRect=0 ); inline int datasetCount() const { return attributesModel->columnCount( attributesModelRootIndex ) / datasetDimension; } virtual QModelIndex indexAt( const QPoint& point ) const; QModelIndexList indexesAt( const QPoint& point ) const; QModelIndexList indexesIn( const QRect& rect ) const; virtual CartesianDiagramDataCompressor::AggregatedDataValueAttributes aggregatedAttrs( const QModelIndex & index, const CartesianDiagramDataCompressor::CachePosition * position ) const; /** * Sets arbitrary attributes of a data set. */ void setDatasetAttrs( int dataset, const QVariant& data, int role ); /** * Retrieves arbitrary attributes of a data set. */ QVariant datasetAttrs( int dataset, int role ) const; /** * Resets an attribute of a dataset back to its default. */ void resetDatasetAttrs( int dataset, int role ); /** * Whether the diagram is transposed (X and Y swapped), which has the same effect as rotating * the diagram 90° clockwise and inverting the (then vertical) X coordinate. */ bool isTransposed() const; static Private* get( AbstractDiagram *diagram ) { return diagram->_d; } AbstractDiagram* diagram; ReverseMapper reverseMapper; /// The size of the diagram set by AbstractDiagram::resize() QSizeF diagramSize; bool doDumpPaintTime; // for use in performance testing code protected: void init(); void init( AbstractCoordinatePlane* plane ); QPointer plane; mutable QModelIndex attributesModelRootIndex; QPointer attributesModel; bool allowOverlappingDataValueTexts; bool antiAliasing; bool percent; int datasetDimension; mutable QPair databoundaries; mutable bool databoundariesDirty; QMap< Qt::Orientation, QString > unitSuffix; QMap< Qt::Orientation, QString > unitPrefix; QMap< int, QMap< Qt::Orientation, QString > > unitSuffixMap; QMap< int, QMap< Qt::Orientation, QString > > unitPrefixMap; QList< QPainterPath > alreadyDrawnDataValueTexts; private: QString prevPaintedDataValueText; mutable QFontMetrics mCachedFontMetrics; mutable QFont mCachedFont; mutable QPaintDevice* mCachedPaintDevice; }; inline AbstractDiagram::AbstractDiagram( Private * p ) : _d( p ) { init(); } inline AbstractDiagram::AbstractDiagram( Private * p, QWidget* parent, AbstractCoordinatePlane* plane ) : QAbstractItemView( parent ), _d( p ) { _d->init( plane ); init(); } class LineAttributesInfo { public: LineAttributesInfo(); LineAttributesInfo( const QModelIndex& _index, const QPointF& _value, const QPointF& _nextValue ); QModelIndex index; QPointF value; QPointF nextValue; }; typedef QVector LineAttributesInfoList; typedef QVectorIterator LineAttributesInfoListIterator; } #endif /* KCHARTDIAGRAM_P_H */ diff --git a/src/KGantt/kganttdatetimegrid.cpp b/src/KGantt/kganttdatetimegrid.cpp index 1adf17b..89c6b3e 100644 --- a/src/KGantt/kganttdatetimegrid.cpp +++ b/src/KGantt/kganttdatetimegrid.cpp @@ -1,1375 +1,1376 @@ /* * 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 "kganttdatetimegrid.h" #include "kganttdatetimegrid_p.h" #include "kganttabstractrowcontroller.h" #include #include #include +#include #include #include #include #include #include #include #include using namespace KGantt; QDebug operator<<( QDebug dbg, KGantt::DateTimeScaleFormatter::Range range ) { switch ( range ) { case KGantt::DateTimeScaleFormatter::Second: dbg << "KGantt::DateTimeScaleFormatter::Second"; break; case KGantt::DateTimeScaleFormatter::Minute: dbg << "KGantt::DateTimeScaleFormatter::Minute"; break; case KGantt::DateTimeScaleFormatter::Hour: dbg << "KGantt::DateTimeScaleFormatter::Hour"; break; case KGantt::DateTimeScaleFormatter::Day: dbg << "KGantt::DateTimeScaleFormatter::Day"; break; case KGantt::DateTimeScaleFormatter::Week: dbg << "KGantt::DateTimeScaleFormatter::Week"; break; case KGantt::DateTimeScaleFormatter::Month: dbg << "KGantt::DateTimeScaleFormatter::Month"; break; case KGantt::DateTimeScaleFormatter::Year: dbg << "KGantt::DateTimeScaleFormatter::Year"; break; } return dbg; } /*!\class KGantt::DateTimeGrid * \ingroup KGantt * * This implementation of AbstractGrid works with QDateTime * and shows days and week numbers in the header */ qreal DateTimeGrid::Private::dateTimeToChartX( const QDateTime& dt ) const { assert( startDateTime.isValid() ); qreal result = startDateTime.date().daysTo(dt.date())*24.*60.*60.; result += startDateTime.time().msecsTo(dt.time())/1000.; result *= dayWidth/( 24.*60.*60. ); return result; } QDateTime DateTimeGrid::Private::chartXtoDateTime( qreal x ) const { assert( startDateTime.isValid() ); int days = static_cast( x/dayWidth ); qreal secs = x*( 24.*60.*60. )/dayWidth; QDateTime dt = startDateTime; QDateTime result = dt.addDays( days ) .addSecs( static_cast(secs-(days*24.*60.*60.) ) ) .addMSecs( qRound( ( secs-static_cast( secs ) )*1000. ) ); return result; } #define d d_func() /*!\class KGantt::DateTimeScaleFormatter * \ingroup KGantt * * This class formats dates and times used in DateTimeGrid follawing a given format. * * The format follows the format of QDateTime::toString(), with one addition: * "w" is replaced with the week number of the date as number without a leading zero (1-53) * "ww" is replaced with the week number of the date as number with a leading zero (01-53) * * For example: * * \code * // formatter to print the complete date over the current week * // This leads to the first day of the week being printed * DateTimeScaleFormatter formatter = DateTimeScaleFormatter( DateTimeScaleFormatter::Week, "yyyy-MM-dd" ); * \endcode * * Optionally, you can set an user defined text alignment flag. The default value is Qt::AlignCenter. * \sa DateTimeScaleFormatter::DateTimeScaleFormatter * * This class even controls the range of the grid sections. * \sa KGanttDateTimeScaleFormatter::Range */ /*! Creates a DateTimeScaleFormatter using \a range and \a format. * The text on the header is aligned following \a alignment. */ DateTimeScaleFormatter::DateTimeScaleFormatter( Range range, const QString& format, const QString& templ, Qt::Alignment alignment ) : _d( new Private( range, format, templ, alignment ) ) { } DateTimeScaleFormatter::DateTimeScaleFormatter( Range range, const QString& format, Qt::Alignment alignment ) : _d( new Private( range, format, QString::fromLatin1( "%1" ), alignment ) ) { } DateTimeScaleFormatter::DateTimeScaleFormatter( const DateTimeScaleFormatter& other ) : _d( new Private( other.range(), other.format(), other.d->templ, other.alignment() ) ) { } DateTimeScaleFormatter::~DateTimeScaleFormatter() { delete _d; } DateTimeScaleFormatter& DateTimeScaleFormatter::operator=( const DateTimeScaleFormatter& other ) { if ( this == &other ) return *this; delete _d; _d = new Private( other.range(), other.format(), other.d->templ, other.alignment() ); return *this; } /*! \returns The format being used for formatting dates and times. */ QString DateTimeScaleFormatter::format() const { return d->format; } /*! \returns The \a datetime as string respecting the format. */ QString DateTimeScaleFormatter::format( const QDateTime& datetime ) const { QString result = d->format; // additional feature: Weeknumber const QString shortWeekNumber = QString::number( datetime.date().weekNumber()) + QLatin1String("/") + QString::number( datetime.date().year()); const QString longWeekNumber = ( shortWeekNumber.length() == 1 ? QString::fromLatin1( "0" ) : QString() ) + shortWeekNumber; result.replace( QString::fromLatin1( "ww" ), longWeekNumber ); result.replace( QString::fromLatin1( "w" ), shortWeekNumber ); result = datetime.toLocalTime().toString( result ); return result; } QString DateTimeScaleFormatter::text( const QDateTime& datetime ) const { return d->templ.arg( format( datetime ) ); } /*! \returns The range of each item on a DateTimeGrid header. * \sa DateTimeScaleFormatter::Range */ DateTimeScaleFormatter::Range DateTimeScaleFormatter::range() const { return d->range; } Qt::Alignment DateTimeScaleFormatter::alignment() const { return d->alignment; } /*! \returns the QDateTime being the begin of the range after the one containing \a datetime * \sa currentRangeBegin */ QDateTime DateTimeScaleFormatter::nextRangeBegin( const QDateTime& datetime ) const { QDateTime result = datetime; switch ( d->range ) { case Second: result = result.addSecs( 60 ); break; case Minute: // set it to the begin of the next minute result.setTime( QTime( result.time().hour(), result.time().minute() ) ); result = result.addSecs( 60 ); break; case Hour: // set it to the begin of the next hour result.setTime( QTime( result.time().hour(), 0 ) ); result = result.addSecs( 60 * 60 ); break; case Day: // set it to midnight the next day result.setTime( QTime( 0, 0 ) ); result = result.addDays( 1 ); break; case Week: // set it to midnight result.setTime( QTime( 0, 0 ) ); // iterate day-wise, until weekNumber changes { const int weekNumber = result.date().weekNumber(); while ( weekNumber == result.date().weekNumber() ) result = result.addDays( 1 ); } break; case Month: // set it to midnight result.setTime( QTime( 0, 0 ) ); // set it to the first of the next month result.setDate( QDate( result.date().year(), result.date().month(), 1 ).addMonths( 1 ) ); break; case Year: // set it to midnight result.setTime( QTime( 0, 0 ) ); // set it to the first of the next year result.setDate( QDate( result.date().year(), 1, 1 ).addYears( 1 ) ); break; } //result = result.toLocalTime(); assert( result != datetime ); //qDebug() << "DateTimeScaleFormatter::nextRangeBegin("<range<range ) { case Second: break; // nothing case Minute: // set it to the begin of the current minute result.setTime( QTime( result.time().hour(), result.time().minute() ) ); break; case Hour: // set it to the begin of the current hour result.setTime( QTime( result.time().hour(), 0 ) ); break; case Day: // set it to midnight the current day result.setTime( QTime( 0, 0 ) ); break; case Week: // set it to midnight result.setTime( QTime( 0, 0 ) ); // iterate day-wise, as long weekNumber is the same { const int weekNumber = result.date().weekNumber(); while ( weekNumber == result.date().addDays( -1 ).weekNumber() ) result = result.addDays( -1 ); } break; case Month: // set it to midnight result.setTime( QTime( 0, 0 ) ); // set it to the first of the current month result.setDate( QDate( result.date().year(), result.date().month(), 1 ) ); break; case Year: // set it to midnight result.setTime( QTime( 0, 0 ) ); // set it to the first of the current year result.setDate( QDate( result.date().year(), 1, 1 ) ); break; } return result; } DateTimeGrid::DateTimeGrid() : AbstractGrid( new Private ) { } DateTimeGrid::~DateTimeGrid() { } /*! \returns The QDateTime used as start date for the grid. * * The default is three days before the current date. */ QDateTime DateTimeGrid::startDateTime() const { return d->startDateTime; } /*! \param dt The start date of the grid. It is used as the beginning of the * horizontal scrollbar in the view. * * Emits gridChanged() after the start date has changed. */ void DateTimeGrid::setStartDateTime( const QDateTime& dt ) { d->startDateTime = dt; emit gridChanged(); } /*! \returns The width in pixels for each day in the grid. * * The default is 100 pixels. */ qreal DateTimeGrid::dayWidth() const { return d->dayWidth; } /*! Maps a given point in time \a dt to an X value in the scene. */ qreal DateTimeGrid::mapFromDateTime( const QDateTime& dt) const { return d->dateTimeToChartX( dt ); } /*! Maps a given X value \a x in scene coordinates to a point in time. */ QDateTime DateTimeGrid::mapToDateTime( qreal x ) const { return d->chartXtoDateTime( x ); } /*! \param w The width in pixels for each day in the grid. * * The signal gridChanged() is emitted after the day width is changed. */ void DateTimeGrid::setDayWidth( qreal w ) { assert( w>0 ); d->dayWidth = w; emit gridChanged(); } /*! \param s The scale to be used to paint the grid. * * The signal gridChanged() is emitted after the scale has changed. * \sa Scale * * Following example demonstrates how to change the format of the header to use * a date-scaling with the header-label displayed with the ISO date-notation. * \code * DateTimeScaleFormatter* formatter = new DateTimeScaleFormatter(DateTimeScaleFormatter::Day, QString::fromLatin1("yyyy-MMMM-dddd")); * grid->setUserDefinedUpperScale( formatter ); * grid->setUserDefinedLowerScale( formatter ); * grid->setScale( DateTimeGrid::ScaleUserDefined ); * \endcode */ void DateTimeGrid::setScale( Scale s ) { d->scale = s; emit gridChanged(); } /*! \returns The scale used to paint the grid. * * The default is ScaleAuto, which means the day scale will be used * as long as the day width is less or equal to 500. * \sa Scale */ DateTimeGrid::Scale DateTimeGrid::scale() const { return d->scale; } /*! Sets the scale formatter for the lower part of the header to the * user defined formatter to \a lower. The DateTimeGrid object takes * ownership of the formatter, which has to be allocated with new. * * You have to set the scale to ScaleUserDefined for this setting to take effect. * \sa DateTimeScaleFormatter */ void DateTimeGrid::setUserDefinedLowerScale( DateTimeScaleFormatter* lower ) { delete d->lower; d->lower = lower; emit gridChanged(); } /*! Sets the scale formatter for the upper part of the header to the * user defined formatter to \a upper. The DateTimeGrid object takes * ownership of the formatter, which has to be allocated with new. * * You have to set the scale to ScaleUserDefined for this setting to take effect. * \sa DateTimeScaleFormatter */ void DateTimeGrid::setUserDefinedUpperScale( DateTimeScaleFormatter* upper ) { delete d->upper; d->upper = upper; emit gridChanged(); } /*! \return The DateTimeScaleFormatter being used to render the lower scale. */ DateTimeScaleFormatter* DateTimeGrid::userDefinedLowerScale() const { return d->lower; } /*! \return The DateTimeScaleFormatter being used to render the upper scale. */ DateTimeScaleFormatter* DateTimeGrid::userDefinedUpperScale() const { return d->upper; } /*! \param ws The start day of the week. * * A solid line is drawn on the grid to mark the beginning of a new week. * Emits gridChanged() after the start day has changed. */ void DateTimeGrid::setWeekStart( Qt::DayOfWeek ws ) { d->weekStart = ws; emit gridChanged(); } /*! \returns The start day of the week */ Qt::DayOfWeek DateTimeGrid::weekStart() const { return d->weekStart; } /*! \param fd A set of days to mark as free in the grid. * * Free days are filled with the alternate base brush of the * palette used by the view. * The signal gridChanged() is emitted after the free days are changed. */ void DateTimeGrid::setFreeDays( const QSet& fd ) { d->freeDays = fd; emit gridChanged(); } /*! \returns The days marked as free in the grid. */ QSet DateTimeGrid::freeDays() const { return d->freeDays; } /*! Sets the brush to use to paint free days. */ void DateTimeGrid::setFreeDaysBrush(const QBrush brush) { d->freeDaysBrush = brush; } /*! \returns The brush used to paint free days. */ QBrush DateTimeGrid::freeDaysBrush() const { return d->freeDaysBrush; } /*! \returns true if row separators are used. */ bool DateTimeGrid::rowSeparators() const { return d->rowSeparators; } /*! \param enable Whether to use row separators or not. */ void DateTimeGrid::setRowSeparators( bool enable ) { d->rowSeparators = enable; } /*! Sets the brush used to display rows where no data is found. * Default is a red pattern. If set to QBrush() rows with no * information will not be marked. */ void DateTimeGrid::setNoInformationBrush( const QBrush& brush ) { d->noInformationBrush = brush; emit gridChanged(); } /*! \returns the brush used to mark rows with no information. */ QBrush DateTimeGrid::noInformationBrush() const { return d->noInformationBrush; } /*! * \param value The datetime to get the x value for. * \returns The x value corresponding to \a value or -1.0 if \a value is not a datetime variant. */ qreal DateTimeGrid::mapToChart( const QVariant& value ) const { if ( ! value.canConvert( QVariant::DateTime ) || ( value.type() == QVariant::String && value.toString().isEmpty() ) ) { return -1.0; } return d->dateTimeToChartX( value.toDateTime() ); } /*! * \param x The x value get the datetime for. * \returns The datetime corresponding to \a x or an invalid datetime if x cannot be mapped. */ QVariant DateTimeGrid::mapFromChart( qreal x ) const { return d->chartXtoDateTime( x ); } /*! \param idx The index to get the Span for. * \returns The start and end pixels, in a Span, of the specified index. */ Span DateTimeGrid::mapToChart( const QModelIndex& idx ) const { assert( model() ); if ( !idx.isValid() ) return Span(); assert( idx.model()==model() ); const QVariant sv = model()->data( idx, StartTimeRole ); const QVariant ev = model()->data( idx, EndTimeRole ); if ( sv.canConvert( QVariant::DateTime ) && ev.canConvert( QVariant::DateTime ) && !(sv.type() == QVariant::String && sv.toString().isEmpty()) && !(ev.type() == QVariant::String && ev.toString().isEmpty()) ) { QDateTime st = sv.toDateTime(); QDateTime et = ev.toDateTime(); if ( et.isValid() && st.isValid() ) { qreal sx = d->dateTimeToChartX( st ); qreal ex = d->dateTimeToChartX( et )-sx; //qDebug() << "DateTimeGrid::mapToChart("< "<< Span( sx, ex ); return Span( sx, ex); } } // Special case for Events with only a start date if ( sv.canConvert( QVariant::DateTime ) && !(sv.type() == QVariant::String && sv.toString().isEmpty()) ) { QDateTime st = sv.toDateTime(); if ( st.isValid() ) { qreal sx = d->dateTimeToChartX( st ); return Span( sx, 0 ); } } return Span(); } #if 0 static void debug_print_idx( const QModelIndex& idx ) { if ( !idx.isValid() ) { qDebug() << "[Invalid]"; return; } QDateTime st = idx.data( StartTimeRole ).toDateTime(); QDateTime et = idx.data( EndTimeRole ).toDateTime(); qDebug() << idx << "["<& constraints ) const { assert( model() ); if ( !idx.isValid() ) return false; assert( idx.model()==model() ); QDateTime st = d->chartXtoDateTime(span.start()); QDateTime et = d->chartXtoDateTime(span.start()+span.length()); //qDebug() << "DateTimeGrid::mapFromChart("< "<< st << et; Q_FOREACH( const Constraint& c, constraints ) { if ( c.type() != Constraint::TypeHard || !isSatisfiedConstraint( c )) continue; if ( c.startIndex() == idx ) { QDateTime tmpst = model()->data( c.endIndex(), StartTimeRole ).toDateTime(); //qDebug() << tmpst << "<" << et <<"?"; if ( tmpstdata( c.startIndex(), EndTimeRole ).toDateTime(); //qDebug() << tmpet << ">" << st <<"?"; if ( tmpet>st ) return false; } } return model()->setData( idx, qVariantFromValue(st), StartTimeRole ) && model()->setData( idx, qVariantFromValue(et), EndTimeRole ); } Qt::PenStyle DateTimeGrid::Private::gridLinePenStyle( QDateTime dt, Private::HeaderType headerType ) const { switch ( headerType ) { case Private::HeaderHour: // Midnight if ( dt.time().hour() == 0 ) return Qt::SolidLine; return Qt::DashLine; case Private::HeaderDay: // First day of the week if ( dt.date().dayOfWeek() == weekStart ) return Qt::SolidLine; return Qt::DashLine; case Private::HeaderWeek: // First day of the month if ( dt.date().day() == 1 ) return Qt::SolidLine; // First day of the week if ( dt.date().dayOfWeek() == weekStart ) return Qt::DashLine; return Qt::NoPen; case Private::HeaderMonth: // First day of the year if ( dt.date().dayOfYear() == 1 ) return Qt::SolidLine; // First day of the month if ( dt.date().day() == 1 ) return Qt::DashLine; return Qt::NoPen; default: // Nothing to do here break; } // Default return Qt::NoPen; } QDateTime DateTimeGrid::Private::adjustDateTimeForHeader( QDateTime dt, Private::HeaderType headerType ) const { // In any case, set time to 00:00:00:00 dt.setTime( QTime( 0, 0, 0, 0 ) ); switch ( headerType ) { case Private::HeaderWeek: // Set day to beginning of the week while ( dt.date().dayOfWeek() != weekStart ) dt = dt.addDays( -1 ); break; case Private::HeaderMonth: // Set day to beginning of the month dt = dt.addDays( 1 - dt.date().day() ); break; case Private::HeaderYear: // Set day to first day of the year dt = dt.addDays( 1 - dt.date().dayOfYear() ); break; default: // In any other case, we don't need to adjust the date time break; } return dt; } void DateTimeGrid::Private::paintVerticalLines( QPainter* painter, const QRectF& sceneRect, const QRectF& exposedRect, QWidget* widget, Private::HeaderType headerType ) { QDateTime dt = chartXtoDateTime( exposedRect.left() ); dt = adjustDateTimeForHeader( dt, headerType ); int offsetSeconds = 0; int offsetDays = 0; // Determine the time step per grid line if ( headerType == Private::HeaderHour ) offsetSeconds = 60*60; else offsetDays = 1; for ( qreal x = dateTimeToChartX( dt ); x < exposedRect.right(); dt = dt.addSecs( offsetSeconds ), dt = dt.addDays( offsetDays ), x = dateTimeToChartX( dt ) ) { //TODO not the best solution as it might be one paint too much, but i don't know what //causes the test to fail yet, i think it might be a rounding error //if ( x >= exposedRect.left() ) { QPen pen = painter->pen(); pen.setBrush( QApplication::palette().dark() ); pen.setStyle( gridLinePenStyle( dt, headerType ) ); painter->setPen( pen ); if ( freeDays.contains( static_cast( dt.date().dayOfWeek() ) ) ) { if (freeDaysBrush.style() == Qt::NoBrush) painter->setBrush( widget?widget->palette().midlight() :QApplication::palette().midlight() ); else painter->setBrush(freeDaysBrush); painter->fillRect( QRectF( x, exposedRect.top(), dayWidth, exposedRect.height() ), painter->brush() ); } painter->drawLine( QPointF( x, sceneRect.top() ), QPointF( x, sceneRect.bottom() ) ); //} } } void DateTimeGrid::Private::paintVerticalUserDefinedLines( QPainter* painter, const QRectF& sceneRect, const QRectF& exposedRect, const DateTimeScaleFormatter* formatter, QWidget* widget ) { Q_UNUSED( widget ); QDateTime dt = chartXtoDateTime( exposedRect.left() ); dt = formatter->currentRangeBegin( dt ); QPen pen = painter->pen(); pen.setBrush( QApplication::palette().dark() ); pen.setStyle( Qt::DashLine ); painter->setPen( pen ); for ( qreal x = dateTimeToChartX( dt ); x < exposedRect.right(); dt = formatter->nextRangeBegin( dt ),x=dateTimeToChartX( dt ) ) { if ( freeDays.contains( static_cast( dt.date().dayOfWeek() ) ) ) { QBrush oldBrush = painter->brush(); if (freeDaysBrush.style() == Qt::NoBrush) painter->setBrush( widget?widget->palette().midlight() :QApplication::palette().midlight() ); else painter->setBrush(freeDaysBrush); painter->fillRect( QRectF( x, exposedRect.top(), dayWidth, exposedRect.height() ), painter->brush() ); painter->setBrush( oldBrush ); } //TODO not the best solution as it might be one paint too much, but i don't know what //causes the test to fail yet, i think it might be a rounding error //if ( x >= exposedRect.left() ) { // FIXME: Also fill area between this and the next vertical line to indicate free days? (Johannes) painter->drawLine( QPointF( x, sceneRect.top() ), QPointF( x, sceneRect.bottom() ) ); //} } } DateTimeGrid::Private::HeaderType DateTimeGrid::Private::headerTypeForScale( DateTimeGrid::Scale scale ) { switch ( scale ) { case ScaleHour: return Private::HeaderHour; case ScaleDay: return Private::HeaderDay; case ScaleWeek: return Private::HeaderWeek; case ScaleMonth: return Private::HeaderMonth; default: // There are no specific header types for any other scale! assert( false ); break; } return Private::HeaderDay; } void DateTimeGrid::paintGrid( QPainter* painter, const QRectF& sceneRect, const QRectF& exposedRect, AbstractRowController* rowController, QWidget* widget ) { // TODO: Support hours and weeks switch ( scale() ) { case ScaleHour: case ScaleDay: case ScaleWeek: case ScaleMonth: d->paintVerticalLines( painter, sceneRect, exposedRect, widget, d->headerTypeForScale( scale() ) ); break; case ScaleAuto: { const qreal tabw = QApplication::fontMetrics().width( QLatin1String( "XXXXX" ) ); const qreal dayw = dayWidth(); if ( dayw > 24*60*60*tabw ) { d->paintVerticalUserDefinedLines( painter, sceneRect, exposedRect, &d->minute_lower, widget ); } else if ( dayw > 24*60*tabw ) { d->paintVerticalLines( painter, sceneRect, exposedRect, widget, Private::HeaderHour ); } else if ( dayw > 24*tabw ) { d->paintVerticalLines( painter, sceneRect, exposedRect, widget, Private::HeaderDay ); } else if ( dayw > tabw ) { d->paintVerticalUserDefinedLines( painter, sceneRect, exposedRect, &d->week_lower, widget ); } else if ( 4*dayw > tabw ) { d->paintVerticalUserDefinedLines( painter, sceneRect, exposedRect, &d->month_lower, widget ); } else { d->paintVerticalUserDefinedLines( painter, sceneRect, exposedRect, &d->year_lower, widget ); } break; } case ScaleUserDefined: d->paintVerticalUserDefinedLines( painter, sceneRect, exposedRect, d->lower, widget ); break; } if ( rowController ) { // First draw the rows QPen pen = painter->pen(); pen.setBrush( QApplication::palette().dark() ); pen.setStyle( Qt::DashLine ); painter->setPen( pen ); QModelIndex idx = rowController->indexAt( qRound( exposedRect.top() ) ); if ( rowController->indexAbove( idx ).isValid() ) idx = rowController->indexAbove( idx ); qreal y = 0; while ( y < exposedRect.bottom() && idx.isValid() ) { const Span s = rowController->rowGeometry( idx ); y = s.start()+s.length(); if ( d->rowSeparators ) { painter->drawLine( QPointF( sceneRect.left(), y ), QPointF( sceneRect.right(), y ) ); } if ( !idx.data( ItemTypeRole ).isValid() && d->noInformationBrush.style() != Qt::NoBrush ) { painter->fillRect( QRectF( exposedRect.left(), s.start(), exposedRect.width(), s.length() ), d->noInformationBrush ); } // Is alternating background better? //if ( idx.row()%2 ) painter->fillRect( QRectF( exposedRect.x(), s.start(), exposedRect.width(), s.length() ), QApplication::palette().alternateBase() ); idx = rowController->indexBelow( idx ); } } } int DateTimeGrid::Private::tabHeight( const QString& txt, QWidget* widget ) const { QStyleOptionHeader opt; if ( widget ) opt.initFrom( widget ); opt.text = txt; QStyle* style; if ( widget ) style = widget->style(); else style = QApplication::style(); QSize s = style->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(), widget); return s.height(); } void DateTimeGrid::Private::getAutomaticFormatters( DateTimeScaleFormatter** lower, DateTimeScaleFormatter** upper) { const qreal tabw = QApplication::fontMetrics().width( QLatin1String( "XXXXX" ) ); const qreal dayw = dayWidth; if ( dayw > 24*60*60*tabw ) { *lower = &minute_lower; *upper = &minute_upper; } else if ( dayw > 24*60*tabw ) { *lower = &hour_lower; *upper = &hour_upper; } else if ( dayw > 24*tabw ) { *lower = &day_lower; *upper = &day_upper; } else if ( dayw > tabw ) { *lower = &week_lower; *upper = &week_upper; } else if ( 4*dayw > tabw ) { *lower = &month_lower; *upper = &month_upper; } else { *lower = &year_lower; *upper = &year_upper; } } void DateTimeGrid::paintHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, QWidget* widget ) { painter->save(); QPainterPath clipPath; clipPath.addRect( headerRect ); painter->setClipPath( clipPath, Qt::IntersectClip ); switch ( scale() ) { case ScaleHour: paintHourScaleHeader( painter, headerRect, exposedRect, offset, widget ); break; case ScaleDay: paintDayScaleHeader( painter, headerRect, exposedRect, offset, widget ); break; case ScaleWeek: paintWeekScaleHeader( painter, headerRect, exposedRect, offset, widget ); break; case ScaleMonth: paintMonthScaleHeader( painter, headerRect, exposedRect, offset, widget ); break; case ScaleAuto: { DateTimeScaleFormatter *lower, *upper; d->getAutomaticFormatters( &lower, &upper ); const qreal lowerHeight = d->tabHeight( lower->text( startDateTime() ) ); const qreal upperHeight = d->tabHeight( upper->text( startDateTime() ) ); const qreal upperRatio = upperHeight/( lowerHeight+upperHeight ); const QRectF upperHeaderRect( headerRect.x(), headerRect.top(), headerRect.width()-1, headerRect.height() * upperRatio ); const QRectF lowerHeaderRect( headerRect.x(), upperHeaderRect.bottom()+1, headerRect.width()-1, headerRect.height()-upperHeaderRect.height()-1 ); paintUserDefinedHeader( painter, lowerHeaderRect, exposedRect, offset, lower, widget ); paintUserDefinedHeader( painter, upperHeaderRect, exposedRect, offset, upper, widget ); break; } case ScaleUserDefined: { const qreal lowerHeight = d->tabHeight( d->lower->text( startDateTime() ) ); const qreal upperHeight = d->tabHeight( d->upper->text( startDateTime() ) ); const qreal upperRatio = upperHeight/( lowerHeight+upperHeight ); const QRectF upperHeaderRect( headerRect.x(), headerRect.top(), headerRect.width()-1, headerRect.height() * upperRatio ); const QRectF lowerHeaderRect( headerRect.x(), upperHeaderRect.bottom()+1, headerRect.width()-1, headerRect.height()-upperHeaderRect.height()-1 ); paintUserDefinedHeader( painter, lowerHeaderRect, exposedRect, offset, d->lower, widget ); paintUserDefinedHeader( painter, upperHeaderRect, exposedRect, offset, d->upper, widget ); } break; } painter->restore(); } void DateTimeGrid::paintUserDefinedHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, const DateTimeScaleFormatter* formatter, QWidget* widget ) { const QStyle* const style = widget ? widget->style() : QApplication::style(); QDateTime dt = formatter->currentRangeBegin( d->chartXtoDateTime( offset + exposedRect.left() )); qreal x = d->dateTimeToChartX( dt ); while ( x < exposedRect.right() + offset ) { const QDateTime next = formatter->nextRangeBegin( dt ); const qreal nextx = d->dateTimeToChartX( next ); QStyleOptionHeader opt; if ( widget ) opt.init( widget ); opt.rect = QRectF( x - offset+1, headerRect.top(), qMax( 1., nextx-x-1 ), headerRect.height() ).toAlignedRect(); opt.textAlignment = formatter->alignment(); opt.text = formatter->text( dt ); style->drawControl( QStyle::CE_Header, &opt, painter, widget ); dt = next; x = nextx; } } void DateTimeGrid::Private::paintHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, QWidget* widget, Private::HeaderType headerType, DateTextFormatter *formatter ) { QStyle* style = widget?widget->style():QApplication::style(); const qreal left = exposedRect.left() + offset; const qreal right = exposedRect.right() + offset; // Paint a section for each hour QDateTime dt = chartXtoDateTime( left ); dt = adjustDateTimeForHeader( dt, headerType ); // Determine the time step per grid line int offsetSeconds = 0; int offsetDays = 0; int offsetMonths = 0; switch ( headerType ) { case Private::HeaderHour: offsetSeconds = 60*60; break; case Private::HeaderDay: offsetDays = 1; break; case Private::HeaderWeek: offsetDays = 7; break; case Private::HeaderMonth: offsetMonths = 1; break; case Private::HeaderYear: offsetMonths = 12; break; default: // Other scales cannot be painted with this method! assert( false ); break; } for ( qreal x = dateTimeToChartX( dt ); x < right; dt = dt.addSecs( offsetSeconds ), dt = dt.addDays( offsetDays ), dt = dt.addMonths( offsetMonths ), x = dateTimeToChartX( dt ) ) { QStyleOptionHeader opt; if ( widget ) opt.init( widget ); opt.rect = formatter->textRect( x, offset, dayWidth, headerRect, dt ); opt.text = formatter->format( dt ); opt.textAlignment = Qt::AlignCenter; style->drawControl(QStyle::CE_Header, &opt, painter, widget); } } /*! Paints the hour scale header. * \sa paintHeader() */ void DateTimeGrid::paintHourScaleHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, QWidget* widget ) { class HourFormatter : public Private::DateTextFormatter { public: virtual ~HourFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return dt.time().toString( QString::fromLatin1( "hh" ) ); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { Q_UNUSED(dt); return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset + 1.0, headerRect.height() / 2.0 ), QSizeF( dayWidth / 24.0, headerRect.height() / 2.0 ) ).toAlignedRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderHour, new HourFormatter ); // Custom parameters class DayFormatter : public Private::DateTextFormatter { public: virtual ~DayFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return dt.date().toString(); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { Q_UNUSED(dt); return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset, 0.0 ), QSizeF( dayWidth, headerRect.height() / 2.0 ) ).toRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderDay, new DayFormatter ); // Custom parameters } /*! Paints the day scale header. * \sa paintHeader() */ void DateTimeGrid::paintDayScaleHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, QWidget* widget ) { class DayFormatter : public Private::DateTextFormatter { public: virtual ~DayFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return dt.toString( QString::fromLatin1( "ddd" ) ).left( 1 ); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { Q_UNUSED(dt); return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset + 1.0, headerRect.height() / 2.0 ), QSizeF( dayWidth, headerRect.height() / 2.0 ) ).toAlignedRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderDay, new DayFormatter ); // Custom parameters class WeekFormatter : public Private::DateTextFormatter { public: virtual ~WeekFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return QString::number(dt.date().weekNumber()) + QLatin1String("/") + QString::number(dt.date().year()); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { Q_UNUSED(dt); return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset, 0.0 ), QSizeF( dayWidth * 7, headerRect.height() / 2.0 ) ).toRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderWeek, new WeekFormatter ); // Custom parameters } /*! Paints the week scale header. * \sa paintHeader() */ void DateTimeGrid::paintWeekScaleHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, QWidget* widget ) { class WeekFormatter : public Private::DateTextFormatter { public: virtual ~WeekFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return QString::number( dt.date().weekNumber() ); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { Q_UNUSED(dt); return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset, headerRect.height() / 2.0 ), QSizeF( dayWidth * 7, headerRect.height() / 2.0 ) ).toRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderWeek, new WeekFormatter ); // Custom parameters class MonthFormatter : public Private::DateTextFormatter { public: virtual ~MonthFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return QLocale().monthName(dt.date().month(), QLocale::LongFormat) + QLatin1String("/") + QString::number(dt.date().year()); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset, 0.0 ), QSizeF( dayWidth * dt.date().daysInMonth(), headerRect.height() / 2.0 ) ).toRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderMonth, new MonthFormatter ); // Custom parameters } /*! Paints the week scale header. * \sa paintHeader() */ void DateTimeGrid::paintMonthScaleHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, QWidget* widget ) { class MonthFormatter : public Private::DateTextFormatter { public: virtual ~MonthFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return QLocale().monthName(dt.date().month(), QLocale::ShortFormat) + QLatin1String("/") + QString::number(dt.date().year()); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset, headerRect.height() / 2.0 ), QSizeF( dayWidth * dt.date().daysInMonth(), headerRect.height() / 2.0 ) ).toRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderMonth, new MonthFormatter ); // Custom parameters class YearFormatter : public Private::DateTextFormatter { public: virtual ~YearFormatter() {} QString format( const QDateTime& dt ) Q_DECL_OVERRIDE { return QString::number( dt.date().year() ); } QRect textRect( qreal x, qreal offset, qreal dayWidth, const QRectF& headerRect, const QDateTime& dt ) Q_DECL_OVERRIDE { return QRectF( QPointF( x, headerRect.top() ) + QPointF( -offset, 0.0 ), QSizeF( dayWidth * dt.date().daysInYear(), headerRect.height() / 2.0 ) ).toRect(); } }; d->paintHeader( painter, headerRect, exposedRect, offset, widget, // General parameters Private::HeaderYear, new YearFormatter ); // Custom parameters } /*! Draw the background for a day. */ void DateTimeGrid::drawDayBackground(QPainter* painter, const QRectF& rect, const QDate& date) { Q_UNUSED(painter); Q_UNUSED(rect); Q_UNUSED(date); } /*! Draw the foreground for a day. */ void DateTimeGrid::drawDayForeground(QPainter* painter, const QRectF& rect, const QDate& date) { Q_UNUSED(painter); Q_UNUSED(rect); Q_UNUSED(date); } /** Return the rectangle that represents the date-range. */ QRectF DateTimeGrid::computeRect(const QDateTime& from, const QDateTime& to, const QRectF& rect) const { qreal topLeft = d->dateTimeToChartX(from); qreal topRight = d->dateTimeToChartX(to); return QRectF(topLeft, rect.top(), topRight - topLeft, rect.height()); } /** Return a date-range represented by the rectangle. */ QPair DateTimeGrid::dateTimeRange(const QRectF& rect) const { QDateTime start; QDateTime end; start = d->chartXtoDateTime(rect.left()); end = d->chartXtoDateTime(rect.right()); return qMakePair(start, end); } void DateTimeGrid::drawBackground(QPainter* paint, const QRectF& rect) { int offset = (int)dayWidth(); assert( offset>0 ); // Figure out the date at the extreme left QDate date = d->chartXtoDateTime(rect.left()).date(); // We need to paint from one end to the other int startx = rect.left(); int endx = rect.right(); // Save the painter state paint->save(); // Paint the first date column while (1) { QDate nextDate = d->chartXtoDateTime(startx+1).date(); if (date != nextDate) { QRectF dayRect(startx-dayWidth(), rect.top(), dayWidth(), rect.height()); dayRect = dayRect.adjusted(1, 0, 0, 0); drawDayBackground(paint, dayRect, date); break; } ++startx; } // Paint the remaining dates for (int i=startx; ichartXtoDateTime(i+1).date(); QRectF dayRect(i, rect.top(), dayWidth(), rect.height()); dayRect = dayRect.adjusted(1, 0, 0, 0); drawDayBackground(paint, dayRect, date); } // Restore the painter state paint->restore(); } void DateTimeGrid::drawForeground(QPainter* paint, const QRectF& rect) { int offset = (int)dayWidth(); // Figure out the date at the extreme left QDate date = d->chartXtoDateTime(rect.left()).date(); // We need to paint from one end to the other int startx = rect.left(); int endx = rect.right(); // Save the painter state paint->save(); // Paint the first date column while (1) { QDate nextDate = d->chartXtoDateTime(startx+1).date(); if (date != nextDate) { QRectF dayRect(startx-dayWidth(), rect.top(), dayWidth(), rect.height()); dayRect = dayRect.adjusted(1, 0, 0, 0); drawDayForeground(paint, dayRect, date); break; } ++startx; } // Paint the remaining dates for (int i=startx; ichartXtoDateTime(i+1).date(); QRectF dayRect(i, rect.top(), dayWidth(), rect.height()); dayRect = dayRect.adjusted(1, 0, 0, 0); drawDayForeground(paint, dayRect, date); } // Restore the painter state paint->restore(); } #undef d #ifndef KDAB_NO_UNIT_TESTS #include #include "unittest/test.h" static std::ostream& operator<<( std::ostream& os, const QDateTime& dt ) { #ifdef QT_NO_STL os << dt.toString().toLatin1().constData(); #else os << dt.toString().toStdString(); #endif return os; } KDAB_SCOPED_UNITTEST_SIMPLE( KGantt, DateTimeGrid, "test" ) { QStandardItemModel model( 3, 2 ); DateTimeGrid grid; QDateTime dt = QDateTime::currentDateTime(); grid.setModel( &model ); QDateTime startdt = dt.addDays( -10 ); grid.setStartDateTime( startdt ); model.setData( model.index( 0, 0 ), dt, StartTimeRole ); model.setData( model.index( 0, 0 ), dt.addDays( 17 ), EndTimeRole ); model.setData( model.index( 2, 0 ), dt.addDays( 18 ), StartTimeRole ); model.setData( model.index( 2, 0 ), dt.addDays( 19 ), EndTimeRole ); Span s = grid.mapToChart( model.index( 0, 0 ) ); //qDebug() << "span="<0 ); assertTrue( s.length()>0 ); assertTrue( startdt == grid.mapToDateTime( grid.mapFromDateTime( startdt ) ) ); grid.mapFromChart( s, model.index( 1, 0 ) ); QDateTime s1 = model.data( model.index( 0, 0 ), StartTimeRole ).toDateTime(); QDateTime e1 = model.data( model.index( 0, 0 ), EndTimeRole ).toDateTime(); QDateTime s2 = model.data( model.index( 1, 0 ), StartTimeRole ).toDateTime(); QDateTime e2 = model.data( model.index( 1, 0 ), EndTimeRole ).toDateTime(); assertTrue( s1.isValid() ); assertTrue( e1.isValid() ); assertTrue( s2.isValid() ); assertTrue( e2.isValid() ); assertEqual( s1, s2 ); assertEqual( e1, e2 ); assertTrue( grid.isSatisfiedConstraint( Constraint( model.index( 0, 0 ), model.index( 2, 0 ) ) ) ); assertFalse( grid.isSatisfiedConstraint( Constraint( model.index( 2, 0 ), model.index( 0, 0 ) ) ) ); s = grid.mapToChart( model.index( 0, 0 ) ); s.setEnd( s.end()+100000. ); bool rc = grid.mapFromChart( s, model.index( 0, 0 ) ); assertTrue( rc ); assertEqual( s1, model.data( model.index( 0, 0 ), StartTimeRole ).toDateTime() ); Span newspan = grid.mapToChart( model.index( 0, 0 ) ); assertEqual( newspan.start(), s.start() ); assertEqual( newspan.length(), s.length() ); { QDateTime startDateTime = QDateTime::currentDateTime(); qreal dayWidth = 100; QDate currentDate = QDate::currentDate(); QDateTime dt( QDate(currentDate.year(), 1, 1), QTime( 0, 0, 0, 0 ) ); assert( dt.isValid() ); qreal result = startDateTime.date().daysTo(dt.date())*24.*60.*60.; result += startDateTime.time().msecsTo(dt.time())/1000.; result *= dayWidth/( 24.*60.*60. ); int days = static_cast( result/dayWidth ); qreal secs = result*( 24.*60.*60. )/dayWidth; QDateTime dt2 = startDateTime; QDateTime result2 = dt2.addDays( days ).addSecs( static_cast(secs-(days*24.*60.*60.) ) ).addMSecs( qRound( ( secs-static_cast( secs ) )*1000. ) ); assertEqual( dt, result2 ); } } #endif /* KDAB_NO_UNIT_TESTS */ #include "moc_kganttdatetimegrid.cpp"