diff --git a/examples/Gantt/project/mainwindow.cpp b/examples/Gantt/project/mainwindow.cpp index c2277b2..ebb9d25 100644 --- a/examples/Gantt/project/mainwindow.cpp +++ b/examples/Gantt/project/mainwindow.cpp @@ -1,485 +1,485 @@ /** * 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 "mainwindow.h" #include "projectmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class ItemTypeComboBox : public QComboBox { Q_OBJECT Q_PROPERTY( KGantt::ItemType itemType READ itemType WRITE setItemType ) public: explicit ItemTypeComboBox( QWidget* parent = nullptr ); KGantt::ItemType itemType() const; public slots: void setItemType( KGantt::ItemType typ ); }; ItemTypeComboBox::ItemTypeComboBox( QWidget* parent ) : QComboBox( parent ) { addItem( tr( "Task" ), QVariant( KGantt::TypeTask ) ); addItem( tr( "Event" ), QVariant( KGantt::TypeEvent ) ); addItem( tr( "Summary" ), QVariant( KGantt::TypeSummary ) ); } KGantt::ItemType ItemTypeComboBox::itemType() const { return static_cast( itemData( currentIndex() ).toInt() ); } void ItemTypeComboBox::setItemType( KGantt::ItemType typ ) { setCurrentIndex( typ-1 ); } class MyItemDelegate : public KGantt::ItemDelegate { public: explicit MyItemDelegate( QObject* parent = nullptr ); /*reimp*/ QWidget* createEditor( QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& idx ) const Q_DECL_OVERRIDE; /*reimp*/ void setEditorData( QWidget* editor, const QModelIndex& index ) const Q_DECL_OVERRIDE; /*reimp*/ void setModelData( QWidget* editor, QAbstractItemModel* model, const QModelIndex & index ) const Q_DECL_OVERRIDE; protected: /*reimp*/void drawDisplay( QPainter* painter, const QStyleOptionViewItem & option, const QRect& rect, const QString& text ) const Q_DECL_OVERRIDE; }; MyItemDelegate::MyItemDelegate( QObject* parent ) : KGantt::ItemDelegate( parent ) { } QWidget* MyItemDelegate::createEditor( QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& idx ) const { qDebug() << "MyItemDelegate::createEditor("<(editor)) && index.isValid() ) { c->setItemType(static_cast(index.data(Qt::EditRole).toInt())); } else { ItemDelegate::setEditorData(editor,index); } } void MyItemDelegate::setModelData ( QWidget* editor, QAbstractItemModel* model, const QModelIndex & index ) const { ItemTypeComboBox* c; if ( (c = qobject_cast(editor)) && index.isValid() ) { model->setData(index,c->itemType()); } else { ItemDelegate::setModelData(editor,model,index); } } void MyItemDelegate::drawDisplay( QPainter* painter, const QStyleOptionViewItem& option, const QRect& rect, const QString& text ) const { //qDebug() << "MyItemDelegate::drawDisplay(" <(text.toInt()); QString str; switch (typ) { case KGantt::TypeTask: str = tr("Task"); break; case KGantt::TypeEvent: str = tr("Event"); break; case KGantt::TypeSummary: str = tr("Summary"); break; default: str = tr("None"); break; } ItemDelegate::drawDisplay(painter,option,rect,str); } /////////////////////////////////////////////////////////////////////////////// // Provide custom background and foreground /////////////////////////////////////////////////////////////////////////////// class DateTimeGrid : public KGantt::DateTimeGrid { public: DateTimeGrid(QObject* parent = nullptr) { setParent(parent); setFreeDays( QSet() ); setFreeDaysBrush( QBrush( Qt::NoBrush ) ); } ~DateTimeGrid() { } //virtual void paintUserDefinedHeader(QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, const KGantt::DateTimeScaleFormatter* formatter, QWidget* widget = 0); void drawBackground(QPainter* painter, const QRectF& rect) Q_DECL_OVERRIDE; void drawForeground(QPainter* painter, const QRectF& rect) Q_DECL_OVERRIDE; }; void DateTimeGrid::drawBackground(QPainter* painter, const QRectF& rect) { QLinearGradient grad; grad.setCoordinateMode( QGradient::ObjectBoundingMode ); grad.setStart( 0.5, 0.5 ); grad.setFinalStop( 0.5, 0.0 ); grad.setSpread( QGradient::ReflectSpread ); // grad.setCenter( 0.5, 0.5 ); // grad.setFocalPoint( 0.5, 0.5 ); // grad.setRadius( 0.5 ); QColor currentColor = Qt::blue; for ( qreal i = 0; i <= 1.0; i += 0.1 ) { currentColor = currentColor.lighter( 100 + 20 * i ); grad.setColorAt( i, currentColor ); } QBrush brush( grad); //brush.setColor(Qt::lightGray); QRectF r = computeRect(QDateTime::currentDateTime(), QDateTime::currentDateTime().addDays(2), rect); painter->fillRect(r, brush); KGantt::DateTimeGrid::drawBackground(painter, rect); } void DateTimeGrid::drawForeground(QPainter* painter, const QRectF& rect) { painter->save(); QRectF r = computeRect(QDateTime::currentDateTime(), QDateTime::currentDateTime().addDays(2), rect); static QString text("Holiday"); QFont font = painter->font(); font.setPixelSize(r.width()/5); QFontMetrics fm(font); - int width = fm.width(text); + int width = fm.boundingRect(text).width(); int height = fm.boundingRect(text).height(); painter->translate(r.center()); painter->translate(-width/2, height/2); painter->setFont(font); painter->drawText(0, 0, text); painter->restore(); KGantt::DateTimeGrid::drawForeground(painter, rect); } /* void DateTimeGrid::paintUserDefinedHeader( QPainter* painter, const QRectF& headerRect, const QRectF& exposedRect, qreal offset, const KGantt::DateTimeScaleFormatter* formatter, QWidget* widget) { const QStyle* const style = widget ? widget->style() : QApplication::style(); QDateTime dt = formatter->currentRangeBegin( mapToDateTime( offset + exposedRect.left() ) ).toUTC(); qreal x = mapFromDateTime( dt ); while ( x < exposedRect.right() + offset ) { const QDateTime next = formatter->nextRangeBegin( dt ); const qreal nextx = mapFromDateTime( 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.state = QStyle::State_Raised | QStyle::State_Enabled; opt.textAlignment = formatter->alignment(); opt.text = formatter->text( dt ); // use white text on black background opt.palette.setColor(QPalette::Window, QColor("black")); opt.palette.setColor(QPalette::ButtonText, QColor("white")); style->drawControl( QStyle::CE_Header, &opt, painter, widget ); dt = next; x = nextx; } } */ MainWindow::MainWindow( QWidget* parent ) : QMainWindow( parent ), m_model( new ProjectModel( this ) ), m_view( new KGantt::View ) { m_view->setModel( m_model ); m_view->setSelectionModel( new QItemSelectionModel(m_model)); // slotToolsNewItem(); m_view->leftView()->setItemDelegateForColumn( 1, new MyItemDelegate( this ) ); m_view->leftView()->setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded ); m_view->graphicsView()->setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded ); DateTimeGrid *grid = new DateTimeGrid(this); grid->timeLine()->setPen(QPen(Qt::red)); grid->timeLine()->setOptions(KGantt::DateTimeTimeLine::UseCustomPen); grid->timeLine()->setInterval(5000); m_view->setGrid(grid); //QItemEditorCreatorBase *creator = new QItemEditorCreator("itemType"); //QItemEditorFactory* factory = new QItemEditorFactory; //factory->registerEditor( QVariant( KGantt::TypeTask ).type(), creator ); //m_view->itemDelegate()->setItemEditorFactory( factory ); setCentralWidget( m_view ); QMenuBar* mb = menuBar(); QMenu* fileMenu = new QMenu( tr( "&File" ) ); #ifndef QT_NO_PRINTER fileMenu->addAction( tr( "&Save as PDF..." ), this, SLOT(slotFileSavePdf()) ); fileMenu->addAction( tr( "&Print..." ), this, SLOT(slotFilePrint()) ); #endif fileMenu->addSeparator(); fileMenu->addAction( tr( "&Quit" ), this, SLOT(slotFileQuit()) ); mb->addMenu( fileMenu ); QMenu* toolsMenu = new QMenu( tr( "&Tools" ) ); toolsMenu->addAction( tr( "&New Item" ), this, SLOT(slotToolsNewItem()) ); toolsMenu->addAction( tr( "&Add Item" ), this, SLOT(slotToolsAppendItem()) ); toolsMenu->addSeparator(); QMenu *alignMenu = toolsMenu->addMenu( tr( "Ali&gn" ) ); alignMenu->addAction( tr( "&Left" ), this, SLOT(slotAlignLeft()) ); alignMenu->addAction( tr( "&Center" ), this, SLOT(slotAlignCenter()) ); alignMenu->addAction( tr( "&Right" ), this, SLOT(slotAlignRight()) ); alignMenu->addAction( tr( "&Hidden" ), this, SLOT(slotAlignHidden()) ); toolsMenu->addSeparator(); toolsMenu->addAction( tr( "&Collapse All" ), this, SLOT(slotCollapseAll()) ); toolsMenu->addAction( tr( "&Expand All" ), this, SLOT(slotExpandAll()) ); mb->addMenu( toolsMenu ); /* slotToolsNewItem(); slotToolsNewItem(); slotToolsNewItem(); for (int i = 0; i < 3; ++i) { m_model->setData(m_model->index(i,2,QModelIndex()), QVariant::fromValue(QDateTime::currentDateTime().addDays(i)), KGantt::StartTimeRole); m_model->setData(m_model->index(i,3,QModelIndex()), QVariant::fromValue(QDateTime::currentDateTime().addDays(i+1)), KGantt::EndTimeRole); } m_view->setConstraintModel(new KGantt::ConstraintModel(m_view)); m_view->constraintModel()->addConstraint(KGantt::Constraint(m_model->index(0,0,QModelIndex()),m_model->index(1,0,QModelIndex()))); m_view->constraintModel()->addConstraint(KGantt::Constraint(m_model->index(1,0,QModelIndex()),m_model->index(2,0,QModelIndex()))); */ } SavePdfDialog::SavePdfDialog(QWidget *parent) : QDialog(parent) { setModal(true); setWindowTitle(tr("Save as PDF")); QVBoxLayout *l = new QVBoxLayout(this); setLayout(l); QHBoxLayout *fileLayout = new QHBoxLayout(this); l->addLayout(fileLayout); QLabel *fileLabel = new QLabel(tr("File:"), this); fileLayout->addWidget(fileLabel); m_fileEdit = new QLineEdit(this); fileLabel->setBuddy(m_fileEdit); m_fileEdit->setText(QFileInfo(QDir::homePath(), "gantt.pdf").absoluteFilePath()); fileLayout->addWidget(m_fileEdit); QPushButton *m_fileButton = new QPushButton("...", this); connect(m_fileButton, SIGNAL(clicked()), this, SLOT(fileButtonClicked())); fileLayout->addWidget(m_fileButton); m_rowLabels = new QCheckBox(tr("Row Header"), this); m_rowLabels->setChecked(true); l->addWidget(m_rowLabels); m_columnLabels = new QCheckBox(tr("Column Header"), this); m_columnLabels->setChecked(true); l->addWidget(m_columnLabels); QDialogButtonBox *btnBox = new QDialogButtonBox(this); btnBox->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Cancel); connect(btnBox, SIGNAL(accepted()), this, SLOT(accept())); connect(btnBox, SIGNAL(rejected()), this, SLOT(reject())); l->addWidget(btnBox); resize(QSize(400, 100).expandedTo(minimumSizeHint())); } void SavePdfDialog::fileButtonClicked() { const QString file = QFileDialog::getSaveFileName(this, tr("Choose PDF File..."), QString(), tr("PDF files (*.pdf)")); if (!file.isEmpty()) m_fileEdit->setText(file); } void MainWindow::slotFileSavePdf() { #ifndef QT_NO_PRINTER SavePdfDialog dialog(this); if (dialog.exec() != QDialog::Accepted) return; const QString file = dialog.m_fileEdit->text(); if (file.isEmpty()) return; const bool drawRowLabels = dialog.m_rowLabels->isChecked(); const bool drawColumnLabels = dialog.m_columnLabels->isChecked(); QPrinter printer(QPrinter::HighResolution); printer.setOrientation(QPrinter::Landscape); printer.setColorMode(QPrinter::Color); printer.setPageMargins(0.2, 0.2, 0.2, 0.2, QPrinter::Point); printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(file); m_view->print(&printer, drawRowLabels, drawColumnLabels); #endif } void MainWindow::slotFilePrint() { #ifndef QT_NO_PRINTER QPrinter printer(QPrinter::HighResolution); printer.setOrientation(QPrinter::Landscape); printer.setColorMode(QPrinter::Color); QPrintDialog dialog(&printer, this); if (dialog.exec() != QDialog::Accepted) return; m_view->print(&printer); #endif } void MainWindow::slotFileQuit() { // TODO QApplication::instance()->quit(); } void MainWindow::slotToolsNewItem() { QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) { qDebug() << "MainWindow::slotToolsNewItem" << idx; m_model->insertRows( 0, 1, m_model->index( idx.row(),0,idx.parent() ) ); } else { m_model->insertRows( 0, 1, m_view->rootIndex() ); } } void MainWindow::slotToolsAppendItem() { QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) { qDebug() << "MainWindow::slotToolsAppendItem" << idx; m_model->insertRows( m_model->rowCount( idx ), 1, m_model->index( idx.row(),0,idx.parent() ) ); } else { m_model->insertRows( m_model->rowCount( m_view->rootIndex() ), 1, m_view->rootIndex() ); } } void MainWindow::slotCollapseAll() { // don't use the treeview's collapseAll/expandAll methods but use the one provided by the // view cause that one will take care to update everyt6hing as needed. //QTreeView* view = qobject_cast( m_view->leftView() ); //view->collapseAll(); QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) m_view->collapseAll(); } void MainWindow::slotExpandAll() { // don't use the treeview's collapseAll/expandAll methods but use the one provided by the // view cause that one will take care to update everyt6hing as needed. //QTreeView* view = qobject_cast( m_view->leftView() ); //view->expandAll(); QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) m_view->expandAll(); } void MainWindow::slotAlignLeft() { QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) { m_model->setData( idx, KGantt::StyleOptionGanttItem::Left, KGantt::TextPositionRole ); } } void MainWindow::slotAlignCenter() { QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) { m_model->setData( idx, KGantt::StyleOptionGanttItem::Center, KGantt::TextPositionRole ); } } void MainWindow::slotAlignRight() { QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) { m_model->setData( idx, KGantt::StyleOptionGanttItem::Right, KGantt::TextPositionRole ); } } void MainWindow::slotAlignHidden() { QModelIndex idx = m_view->selectionModel()->currentIndex(); if ( idx.isValid() ) { m_model->setData( idx, KGantt::StyleOptionGanttItem::Hidden, KGantt::TextPositionRole ); } } #include "mainwindow.moc" diff --git a/examples/Plotter/BigDataset/Model.cpp b/examples/Plotter/BigDataset/Model.cpp index cae403c..dda9d16 100644 --- a/examples/Plotter/BigDataset/Model.cpp +++ b/examples/Plotter/BigDataset/Model.cpp @@ -1,156 +1,157 @@ /** * 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 "Model.h" #include "KChartMath_p.h" #include #include +#include static const qreal s_stepWidth = 0.1; Model::Model() : m_x( 0 ), m_function( SineFunction ) { m_appendTimer.setInterval( 3 ); connect( &m_appendTimer, SIGNAL(timeout()), SLOT(appendPoint()) ); // pre-fill some values appendPoints( 100 ); } int Model::columnCount( const QModelIndex& parent ) const { Q_UNUSED( parent ); return 2; // row 0: x, row 1: y } int Model::rowCount( const QModelIndex& parent ) const { Q_UNUSED( parent ); return m_data.count(); } QModelIndex Model::index( int row, int column, const QModelIndex& parent ) const { if ( column > 1 || row >= m_data.count() || parent.isValid() ) { return QModelIndex(); } return createIndex( row, column ); } QModelIndex Model::parent( const QModelIndex& index ) const { Q_UNUSED( index ); return QModelIndex(); } QVariant Model::data( const QModelIndex& index, int role ) const { if ( role != Qt::DisplayRole || index.parent().isValid() || index.column() > 1 || index.row() >= m_data.count() ) { return QVariant(); } if ( index.column() == 0 ) { return index.row() * s_stepWidth; } else { return m_data.at( index.row() ); } } // slot void Model::setFunction( Function f ) { m_function = f; } void Model::setRunning( bool running ) { if ( running ) { m_appendTimer.start(); } else { m_appendTimer.stop(); } } // slot void Model::appendPoint() { appendPoints( 1 ); } void Model::appendPoints( int numPoints ) { Q_ASSERT( numPoints >= 1); beginInsertRows( QModelIndex(), m_data.count(), m_data.count() + numPoints - 1 ); for ( int i = 0; i < numPoints; i++ ) { m_data.append( nextFunctionValue() ); } - QTime stopWatch; + QElapsedTimer stopWatch; stopWatch.start(); endInsertRows(); // this immediately triggers the signals that cause the diagram to update qDebug() << "Adding" << numPoints << "data points to the existing" << m_data.count() - numPoints << "took" << stopWatch.elapsed() << "milliseconds"; } qreal Model::nextFunctionValue() { qreal fx = 0.0; switch ( m_function ) { case SineFunction: fx = sin( m_x ); break; case TriangleFunction: { qreal x = fmod( m_x + 0.5 * M_PI, 2 * M_PI ); // make it look like sine, only less round if ( x < M_PI ) { fx = -1.0 + x * ( 2.0 / M_PI); } else { fx = 3.0 - x * ( 2.0 / M_PI); } break; } case SquareFunction: { qreal x = fmod( m_x, 2 * M_PI ); fx = x < M_PI ? 1 : -1; break; } case NoiseFunction: fx = -1.0 + qreal( qrand() ) * 2.0 / qreal( RAND_MAX ); break; case SineOneDivFunction: { // we want this repeating and we want negative arguments, too. qreal x = fmod( m_x + 10, 20 ) - 10; if ( qAbs( x ) < 1e-6 ) { break; } fx = sin( 1.0 / x ); break; } case OneDivSineFunction: { qreal s = sin( m_x ); fx = qAbs( s ) > 1e-6 ? 1.0 / s : 0.0; break; } default: Q_ASSERT( false ); } m_x += s_stepWidth; return fx; } diff --git a/qtests/DrawIntoPainter/mainwindow.cpp b/qtests/DrawIntoPainter/mainwindow.cpp index 8522924..af036d2 100644 --- a/qtests/DrawIntoPainter/mainwindow.cpp +++ b/qtests/DrawIntoPainter/mainwindow.cpp @@ -1,432 +1,433 @@ /** * 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 "mainwindow.h" #include "framewidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include using namespace KChart; static QPixmap drawIntoPixmap( const QSize& size, KChart::Chart* chart ) { QPixmap pix( size ); pix.fill( Qt::white ); QPainter painter( &pix ); chart->paint( &painter, QRect( 0, 0, size.width(), size.height() ) ); return pix; } // When set, this example uses FrameWidget which uses Chart::paint to paint itself. // When not set, this example uses a Chart widget directly. #define USE_FRAME_WIDGET 1 MainWindow::MainWindow( QWidget* parent ) : QWidget( parent ) { setupUi( this ); connect( lineTypeCB, SIGNAL(currentIndexChanged(QString)), this, SLOT(setLineType(QString)) ); connect( paintLegendCB, SIGNAL(toggled(bool)), this, SLOT(setLegendVisible(bool)) ); connect( paintValuesCB, SIGNAL(toggled(bool)), this, SLOT(setValuesVisible(bool)) ); connect( paintMarkersCB, SIGNAL(toggled(bool)), this, SLOT(setMarkersVisible(bool)) ); connect( markersStyleCB, SIGNAL(currentIndexChanged(int)), this, SLOT(updateMarkers()) ); connect( markersWidthSB, SIGNAL(valueChanged(int)), this, SLOT(updateMarkersHeight()) ); connect( markersHeightSB, SIGNAL(valueChanged(int)), this, SLOT(updateMarkersWidth()) ); connect( displayAreasCB, SIGNAL(toggled(bool)), this, SLOT(updateAreas(bool)) ); connect( transparencySB, SIGNAL(valueChanged(int)), this, SLOT(updateAreasTransparency()) ); connect( zoomFactorSB, SIGNAL(valueChanged(double)), this, SLOT(setZoomFactor(qreal)) ); connect( hSBar, SIGNAL(valueChanged(int)), this, SLOT(setHPos(int)) ); connect( vSBar, SIGNAL(valueChanged(int)), this, SLOT(setVPos(int)) ); connect( savePB, SIGNAL(clicked()), this, SLOT(saveChart()) ); QHBoxLayout* chartLayout = new QHBoxLayout( chartFrame ); #ifdef USE_FRAME_WIDGET FrameWidget* chartFrameWidget = new FrameWidget(); chartLayout->addWidget( chartFrameWidget ); #endif hSBar->setVisible( false ); vSBar->setVisible( false ); m_model.loadFromCSV( ":/empty" ); // Set up the diagram m_lines = new LineDiagram(); m_lines->setModel( &m_model ); CartesianAxis *xAxis = new CartesianAxis( m_lines ); CartesianAxis *yAxis = new CartesianAxis ( m_lines ); CartesianAxis *axisTop = new CartesianAxis ( m_lines ); CartesianAxis *axisRight = new CartesianAxis ( m_lines ); xAxis->setPosition ( KChart::CartesianAxis::Bottom ); yAxis->setPosition ( KChart::CartesianAxis::Left ); axisTop->setPosition( KChart::CartesianAxis::Top ); axisRight->setPosition( KChart::CartesianAxis::Right ); m_lines->addAxis( xAxis ); m_lines->addAxis( yAxis ); m_lines->addAxis( axisTop ); m_lines->addAxis( axisRight ); m_chart = new Chart(); //m_chart->setGlobalLeading(10,10,10,10); // by default there is no leading #ifdef USE_FRAME_WIDGET chartFrameWidget->setChart( m_chart ); // make sure, we re-draw after changing one of the chart's properties connect( m_chart, SIGNAL(propertiesChanged()), chartFrameWidget, SLOT(update()) ) ; #else chartLayout->addWidget( m_chart ); #endif m_chart->coordinatePlane()->replaceDiagram( m_lines ); for ( int iColumn = 0; iColumnmodel()->columnCount(); ++iColumn ) { QPen pen(m_lines->pen( iColumn )); pen.setWidth(4); m_lines->setPen( iColumn, pen ); } FrameAttributes faChart( m_chart->frameAttributes() ); faChart.setVisible( true ); faChart.setPen( QPen(QColor(0x60,0x60,0xb0), 8) ); m_chart->setFrameAttributes( faChart ); BackgroundAttributes baChart( m_chart->backgroundAttributes() ); baChart.setVisible( true ); baChart.setBrush( QColor(0xd0,0xd0,0xff) ); m_chart->setBackgroundAttributes( baChart ); // Set up the legend m_legend = new Legend( m_lines, m_chart ); m_legend->setPosition( Position::South ); m_legend->setAlignment( Qt::AlignRight ); m_legend->setShowLines( false ); m_legend->setTitleText( tr( "Legend" ) ); m_legend->setOrientation( Qt::Horizontal ); // setting the legend frame and background to the same color: const QColor legendColor(0xff,0xe0,0x80); FrameAttributes faLegend( m_legend->frameAttributes() ); faLegend.setVisible( true ); faLegend.setPen( QPen(legendColor, 1) ); m_legend->setFrameAttributes( faLegend ); BackgroundAttributes baLegend( m_legend->backgroundAttributes() ); baLegend.setVisible( true ); baLegend.setBrush( legendColor ); m_legend->setBackgroundAttributes( baLegend ); m_chart->addLegend( m_legend ); // for illustration we paint the same chart at different sizes: QSize size1 = QSize( 200, 200 ); QSize size2 = QSize( 800, 800 ); m_pix1 = drawIntoPixmap( size1, m_chart ); m_pix2 = drawIntoPixmap( size2, m_chart ); m_pix2 = m_pix2.scaled( size1 ); m_smallChart1 = new QLabel( this ); m_smallChart1->setWindowTitle( "200x200" ); m_smallChart1->setPixmap( m_pix1 ); m_smallChart1->setFixedSize( m_pix1.size() ); m_smallChart1->move( width() - m_pix1.width()*2, height()/2 - m_pix1.height()-5 ); m_smallChart1->show(); m_smallChart2 = new QLabel( this ); m_smallChart2->setWindowTitle( "800x800 scaled down" ); m_smallChart2->setPixmap( m_pix2 ); m_smallChart2->setFixedSize( m_pix2.size() ); m_smallChart2->move( width() - m_pix2.width()*2, height()/2 + 5 ); m_smallChart2->show(); faChart.setPen( QPen(QColor(0xb0,0xb0,0xff), 8) ); m_chart->setFrameAttributes( faChart ); // initialize attributes; this is necessary because we need to enable data value attributes before // any of them (e.g. only markers) can be displayed. but if we enable data value attributs, a default // data value text is included, even if we only wanted to set markers. so we enable DVA and then // individually disable the parts we don't want. setValuesVisible( false ); setMarkersVisible( false ); } void MainWindow::updateData(QString data) { - QTime t; + QElapsedTimer t; t.start(); m_model.loadFromCSV( data ); - qDebug("Time for loading data %s: %d ms", data.toLatin1().constData(), t.elapsed()); + qDebug("Time for loading data %s: %lld ms", data.toLatin1().constData(), t.elapsed()); t.restart(); QSize size1 = QSize( 200, 200 ); QSize size2 = QSize( 800, 800 ); m_pix1 = drawIntoPixmap( size1, m_chart ); m_pix2 = drawIntoPixmap( size2, m_chart ); - qDebug("Time for drawing pixmap %s: %d ms", data.toLatin1().constData(), t.elapsed()); + qDebug("Time for drawing pixmap %s: %lld ms", data.toLatin1().constData(), t.elapsed()); t.restart(); m_lines->setModel( &m_model ); - qDebug("Time for setting model %s: %d ms", data.toLatin1().constData(), t.elapsed()); + qDebug("Time for setting model %s: %lld ms", data.toLatin1().constData(), t.elapsed()); t.restart(); m_smallChart1->setPixmap( m_pix1 ); m_smallChart2->setPixmap( m_pix2 ); m_smallChart1->show(); m_smallChart2->show(); - qDebug("Time for setting pixmap %s: %d ms", data.toLatin1().constData(), t.elapsed()); + qDebug("Time for setting pixmap %s: %lld ms", data.toLatin1().constData(), t.elapsed()); t.restart(); } void MainWindow::setLineType( const QString & text ) { if ( text == "Normal" ) m_lines->setType( LineDiagram::Normal ); else if ( text == "Stacked" ) m_lines->setType( LineDiagram::Stacked ); else if ( text == "Percent" ) m_lines->setType( LineDiagram::Percent ); else qWarning (" Does not match any type"); } void MainWindow::setLegendVisible(bool visible ) { KChart::Legend* legend = m_chart->legend(); if ( visible != ( legend != nullptr ) ) { if ( visible ) m_chart->addLegend( m_legend ); else m_chart->takeLegend( legend ); } } void MainWindow::setValuesVisible(bool visible ) { const int colCount = m_lines->model()->columnCount(); for ( int iColumn = 0; iColumndataValueAttributes( iColumn ); a.setVisible( true ); TextAttributes ta = a.textAttributes(); ta.setRotation( 0 ); ta.setFont( QFont( "Comic", 10 ) ); ta.setPen( m_lines->brush( iColumn ).color() ); ta.setVisible( visible ); a.setTextAttributes( ta ); m_lines->setDataValueAttributes( iColumn, a); } } void MainWindow::setMarkersVisible( bool visible ) { paintMarkers( visible, QSize() ); } void MainWindow::updateMarkers() { setMarkersVisible( paintMarkersCB->isChecked() ); } void MainWindow::updateMarkersHeight() { markersHeightSB->setValue( markersWidthSB->value() ); updateMarkers(); } void MainWindow::updateMarkersWidth() { markersWidthSB->setValue( markersHeightSB->value() ); updateMarkers(); } void MainWindow::updateAreas( bool visible ) { const int colCount = m_lines->model()->columnCount(); for ( int iColumn = 0; iColumnlineAttributes( iColumn ) ); la.setDisplayArea( visible ); if ( visible ) la.setTransparency( transparencySB->value() ); m_lines->setLineAttributes( iColumn, la ); } } void MainWindow::updateAreasTransparency() { if ( !displayAreasCB->isChecked() ) displayAreasCB->setChecked( true ); else updateAreas( true ); } void MainWindow::setZoomFactor( qreal factor ) { const bool isZoomedIn = factor > 1.0f; hSBar->setVisible( isZoomedIn ); vSBar->setVisible( isZoomedIn ); if ( !isZoomedIn ) { hSBar->setValue( 500 ); vSBar->setValue( 500 ); } m_chart->coordinatePlane()->setZoomFactorX( factor ); m_chart->coordinatePlane()->setZoomFactorY( factor ); } void MainWindow::setHPos( int hPos ) { m_chart->coordinatePlane()->setZoomCenter( QPointF(hPos/1000.0, vSBar->value()/1000.0) ); m_chart->update(); } void MainWindow::setVPos( int vPos ) { m_chart->coordinatePlane()->setZoomCenter( QPointF( hSBar->value()/1000.0, vPos/1000.0) ); } // since DataValue markers have no relative sizing mode we need to scale them for printing void MainWindow::paintMarkers( bool checked, const QSize& printSize ) { MarkerAttributes::MarkerStylesMap map; map.insert( 0, MarkerAttributes::MarkerSquare ); map.insert( 1, MarkerAttributes::MarkerCircle ); map.insert( 2, MarkerAttributes::MarkerRing ); map.insert( 3, MarkerAttributes::MarkerCross ); map.insert( 4, MarkerAttributes::MarkerDiamond ); // next: Specify column- / cell-specific attributes! const int colCount = m_lines->model()->columnCount(); for ( int iColumn = 0; iColumndataValueAttributes( iColumn ); dva.setVisible( true ); MarkerAttributes ma( dva.markerAttributes() ); switch ( markersStyleCB->currentIndex() ) { case 0: ma.setMarkerStyle( MarkerAttributes::MarkerSquare ); break; case 1: // Column-specific attributes ma.setMarkerStyle( map.value( iColumn ) ); break; case 2: ma.setMarkerStyle( MarkerAttributes::MarkerCircle ); break; case 3: ma.setMarkerStyle( MarkerAttributes::MarkerDiamond ); break; case 4: ma.setMarkerStyle( MarkerAttributes::Marker1Pixel ); break; case 5: ma.setMarkerStyle( MarkerAttributes::Marker4Pixels ); break; case 6: ma.setMarkerStyle( MarkerAttributes::MarkerRing ); break; case 7: ma.setMarkerStyle( MarkerAttributes::MarkerCross ); break; case 8: ma.setMarkerStyle( MarkerAttributes::MarkerFastCross ); break; default: Q_ASSERT( false ); } ma.setVisible( checked ); qreal factorWidth = printSize.isValid() ? ( printSize.width() / m_chart->rect().width() ) : 1.0f; qreal factorHeight = printSize.isValid() ? ( printSize.height() / m_chart->rect().height() ) : 1.0f; ma.setMarkerSize( QSize( markersWidthSB->value() * factorWidth, markersHeightSB->value() * factorHeight ) ); dva.setMarkerAttributes( ma ); m_lines->setDataValueAttributes( iColumn, dva ); // make a special one for certain values DataValueAttributes yellowAttributes( dva ); MarkerAttributes yellowMarker( yellowAttributes.markerAttributes() ); yellowMarker.setMarkerColor( Qt::yellow ); yellowAttributes.setMarkerAttributes( yellowMarker ); const int rowCount = m_lines->model()->rowCount(); for ( int j=0; j< rowCount; ++j ) { QModelIndex index = m_lines->model()->index( j, iColumn, QModelIndex() ); QBrush brush = m_lines->model()->headerData( iColumn, Qt::Vertical, DatasetBrushRole ).value(); qreal value = m_lines->model()->data( index ).toReal(); /* Set a specific color - marker for a specific value */ if ( value == 13 ) { m_lines->setDataValueAttributes( index, yellowAttributes ); } } } } void MainWindow::saveChart() { qDebug() << "Painting into PNG"; QPixmap qpix(600,600); QPainter painter(&qpix); painter.setBrush(Qt::white); painter.fillRect( 0, 0, 600, 600, Qt::white); m_chart->paint( &painter, QRect(100, 100, 400, 400) ); painter.end(); qpix.save("kchart-demo.png", "PNG"); qDebug() << "Painting into PNG - done"; } void MainWindow::resizeEvent ( QResizeEvent * ) { m_smallChart1->move( width() - m_pix1.width()*2, height()/2 - m_pix1.height()-5 ); m_smallChart2->move( width() - m_pix2.width()*2, height()/2 + 5 ); } diff --git a/qtests/ParamVsParam/main.cpp b/qtests/ParamVsParam/main.cpp index 3f5dce0..43125a2 100644 --- a/qtests/ParamVsParam/main.cpp +++ b/qtests/ParamVsParam/main.cpp @@ -1,49 +1,49 @@ /** * 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 "mainwindow.h" - +#include #include class TestParamVsParam: public QObject { Q_OBJECT private slots: void testMainWindow() { - QTime t; + QElapsedTimer t; t.start(); MainWindow mainWindow; - qDebug("time constructor: %d ms", t.elapsed()); + qDebug("time constructor: %lld ms", t.elapsed()); mainWindow.show(); QTimer::singleShot(0, qApp, SLOT(quit())); - qDebug("time show(): %d ms", t.elapsed()); + qDebug("time show(): %lld ms", t.elapsed()); // uncomment to see it blink: // QTest::qWait( 10000 ); } }; QTEST_MAIN(TestParamVsParam) #include "main.moc" diff --git a/src/KChart/Cartesian/DiagramFlavors/KChartPercentPlotter_p.cpp b/src/KChart/Cartesian/DiagramFlavors/KChartPercentPlotter_p.cpp index a2d070d..dd5bdbe 100644 --- a/src/KChart/Cartesian/DiagramFlavors/KChartPercentPlotter_p.cpp +++ b/src/KChart/Cartesian/DiagramFlavors/KChartPercentPlotter_p.cpp @@ -1,264 +1,264 @@ /* * 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 "KChartPercentPlotter_p.h" #include "KChartPlotter.h" #include "PaintingHelpers_p.h" #include using namespace KChart; using namespace std; PercentPlotter::PercentPlotter( Plotter* d ) : PlotterType( d ) { } Plotter::PlotType PercentPlotter::type() const { return Plotter::Percent; } const QPair< QPointF, QPointF > PercentPlotter::calculateDataBoundaries() const { const int rowCount = compressor().modelDataRows(); const int colCount = compressor().modelDataColumns(); qreal xMin = std::numeric_limits< qreal >::quiet_NaN(); qreal xMax = std::numeric_limits< qreal >::quiet_NaN(); const qreal yMin = 0.0; const qreal yMax = 100.0; for ( int column = 0; column < colCount; ++column ) { for ( int row = 0; row < rowCount; ++row ) { const CartesianDiagramDataCompressor::CachePosition position( row, column ); const CartesianDiagramDataCompressor::DataPoint point = compressor().data( position ); const qreal valueX = ISNAN( point.key ) ? 0.0 : point.key; if ( ISNAN( xMin ) ) { xMin = valueX; xMax = valueX; } else { xMin = qMin( xMin, valueX ); xMax = qMax( xMax, valueX ); } } } const QPointF bottomLeft( QPointF( xMin, yMin ) ); const QPointF topRight( QPointF( xMax, yMax ) ); return QPair< QPointF, QPointF >( bottomLeft, topRight ); } class Value { public: Value() : value( std::numeric_limits< qreal >::quiet_NaN() ) { } // allow implicit conversion Value( qreal value ) : value( value ) { } operator qreal() const { return value; } private: qreal value; }; void PercentPlotter::paint( PaintContext* ctx ) { reverseMapper().clear(); Q_ASSERT( dynamic_cast< CartesianCoordinatePlane* >( ctx->coordinatePlane() ) ); const CartesianCoordinatePlane* const plane = static_cast< CartesianCoordinatePlane* >( ctx->coordinatePlane() ); const int colCount = compressor().modelDataColumns(); const int rowCount = compressor().modelDataRows(); if ( colCount == 0 || rowCount == 0 ) return; LabelPaintCache lpc; // this map contains the y-values to each x-value QMap< qreal, QVector< QPair< Value, QModelIndex > > > diagramValues; for ( int col = 0; col < colCount; ++col ) { for ( int row = 0; row < rowCount; ++row ) { const CartesianDiagramDataCompressor::CachePosition position( row, col ); const CartesianDiagramDataCompressor::DataPoint point = compressor().data( position ); diagramValues[ point.key ].resize( colCount ); diagramValues[ point.key ][ col ].first = point.value; diagramValues[ point.key ][ col ].second = point.index; } } // the sums of the y-values per x-value QMap< qreal, qreal > yValueSums; // the x-values QList< qreal > xValues = diagramValues.keys(); // make sure it's sorted - qSort( xValues ); + std::sort(xValues.begin(), xValues.end()); Q_FOREACH( const qreal xValue, xValues ) { // the y-values to the current x-value QVector< QPair< Value, QModelIndex > >& yValues = diagramValues[ xValue ]; Q_ASSERT( yValues.count() == colCount ); for ( int column = 0; column < colCount; ++column ) { QPair< Value, QModelIndex >& data = yValues[ column ]; // if the index is invalid, there was no value. Let's interpolate. if ( !data.second.isValid() ) { QPair< QPair< qreal, Value >, QModelIndex > left; QPair< QPair< qreal, Value >, QModelIndex > right; int xIndex = 0; // let's find the next lower value for ( xIndex = xValues.indexOf( xValue ); xIndex >= 0; --xIndex ) { if ( diagramValues[ xValues[ xIndex ] ][ column ].second.isValid() ) { left.first.first = xValues[ xIndex ]; left.first.second = diagramValues[ left.first.first ][ column ].first; left.second = diagramValues[ xValues[ xIndex ] ][ column ].second; break; } } // let's find the next higher value for ( xIndex = xValues.indexOf( xValue ); xIndex < xValues.count(); ++xIndex ) { if ( diagramValues[ xValues[ xIndex ] ][ column ].second.isValid() ) { right.first.first = xValues[ xIndex ]; right.first.second = diagramValues[ right.first.first ][ column ].first; right.second = diagramValues[ xValues[ xIndex ] ][ column ].second; break; } } // interpolate out of them (left and/or right might be invalid, but this doesn't matter here) const qreal leftX = left.first.first; const qreal rightX = right.first.first; const qreal leftY = left.first.second; const qreal rightY = right.first.second; data.first = leftY + ( rightY - leftY ) * ( xValue - leftX ) / ( rightX - leftX ); // if the result is a valid value, let's assign the index, too if ( !ISNAN( data.first.operator qreal() ) ) data.second = left.second; } // sum it up if ( !ISNAN( yValues[ column ].first.operator qreal() ) ) yValueSums[ xValue ] += yValues[ column ].first; } } for ( int column = 0; column < colCount; ++column ) { LineAttributesInfoList lineList; LineAttributes laPreviousCell; CartesianDiagramDataCompressor::CachePosition previousCellPosition; CartesianDiagramDataCompressor::DataPoint lastPoint; qreal lastExtraY = 0.0; qreal lastValue = 0.0; QMapIterator< qreal, QVector< QPair< Value, QModelIndex > > > i( diagramValues ); while ( i.hasNext() ) { i.next(); CartesianDiagramDataCompressor::DataPoint point; point.key = i.key(); const QPair< Value, QModelIndex >& data = i.value().at( column ); point.value = data.first; point.index = data.second; if ( ISNAN( point.key ) || ISNAN( point.value ) ) { previousCellPosition = CartesianDiagramDataCompressor::CachePosition(); continue; } qreal extraY = 0.0; for ( int col = column - 1; col >= 0; --col ) { const qreal y = i.value().at( col ).first; if ( !ISNAN( y ) ) extraY += y; } LineAttributes laCell; const qreal value = ( point.value + extraY ) / yValueSums[ i.key() ] * 100; const QModelIndex sourceIndex = attributesModel()->mapToSource( point.index ); // area corners, a + b are the line ends: const QPointF a( plane->translate( QPointF( lastPoint.key, lastValue ) ) ); const QPointF b( plane->translate( QPointF( point.key, value ) ) ); const QPointF c( plane->translate( QPointF( lastPoint.key, lastExtraY / yValueSums[ i.key() ] * 100 ) ) ); const QPointF d( plane->translate( QPointF( point.key, extraY / yValueSums[ i.key() ] * 100 ) ) ); // add the line to the list: laCell = diagram()->lineAttributes( sourceIndex ); // add data point labels: const PositionPoints pts = PositionPoints( b, a, d, c ); // if necessary, add the area to the area list: QList areas; if ( laCell.displayArea() ) { QPolygonF polygon; polygon << a << b << d << c; areas << polygon; } // add the pieces to painting if this is not hidden: if ( !point.hidden /*&& !ISNAN( lastPoint.key ) && !ISNAN( lastPoint.value ) */) { m_private->addLabel( &lpc, sourceIndex, nullptr, pts, Position::NorthWest, Position::NorthWest, value ); if ( !ISNAN( lastPoint.key ) && !ISNAN( lastPoint.value ) ) { PaintingHelpers::paintAreas( m_private, ctx, attributesModel()->mapToSource( lastPoint.index ), areas, laCell.transparency() ); lineList.append( LineAttributesInfo( sourceIndex, a, b ) ); } } // wrap it up: laPreviousCell = laCell; lastPoint = point; lastExtraY = extraY; lastValue = value; } PaintingHelpers::paintElements( m_private, ctx, lpc, lineList ); } } diff --git a/src/KChart/Cartesian/KChartCartesianAxis.cpp b/src/KChart/Cartesian/KChartCartesianAxis.cpp index 96d0303..e48b25d 100644 --- a/src/KChart/Cartesian/KChartCartesianAxis.cpp +++ b/src/KChart/Cartesian/KChartCartesianAxis.cpp @@ -1,1173 +1,1173 @@ /* * 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 "KChartCartesianAxis.h" #include "KChartCartesianAxis_p.h" #include #include #include #include #include #include #include "KChartPaintContext.h" #include "KChartChart.h" #include "KChartAbstractCartesianDiagram.h" #include "KChartAbstractDiagram_p.h" #include "KChartAbstractGrid.h" #include "KChartPainterSaver_p.h" #include "KChartLayoutItems.h" #include "KChartBarDiagram.h" #include "KChartStockDiagram.h" #include "KChartLineDiagram.h" #include "KChartPrintingParameters.h" using namespace KChart; #define d (d_func()) static qreal slightlyLessThan( qreal r ) { if ( r == 0.0 ) { // scale down the epsilon somewhat arbitrarily return r - std::numeric_limits< qreal >::epsilon() * 1e-6; } // scale the epsilon so that it (hopefully) changes at least the least significant bit of r qreal diff = qAbs( r ) * std::numeric_limits< qreal >::epsilon() * 2.0; return r - diff; } static int numSignificantDecimalPlaces( qreal floatNumber ) { static const int maxPlaces = 15; QString sample = QString::number( floatNumber, 'f', maxPlaces ).section( QLatin1Char('.'), 1, 2 ); int ret = maxPlaces; for ( ; ret > 0; ret-- ) { if ( sample[ ret - 1 ] != QLatin1Char( '0' ) ) { break; } } return ret; } // Feature idea: In case of numeric labels, consider limiting the possible values of majorThinningFactor // to something like {1, 2, 5} * 10^n. Or even better, something that achieves round values in the // remaining labels. // ensure we take the const-overload of any following function, esp. required for strict iterators template static const T& constify(T &v) { return v; } TickIterator::TickIterator( CartesianAxis* a, CartesianCoordinatePlane* plane, uint majorThinningFactor, bool omitLastTick ) : m_axis( a ), m_majorThinningFactor( majorThinningFactor ), m_majorLabelCount( 0 ), m_type( NoTick ) { // deal with the things that are specific to axes (like annotations), before the generic init(). const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get( a ); XySwitch xy( axisPriv->isVertical() ); m_dimension = xy( plane->gridDimensionsList().first(), plane->gridDimensionsList().last() ); if ( omitLastTick ) { // In bar and stock charts the last X tick is a fencepost with no associated value, which is // convenient for grid painting. Here we have to manually exclude it to avoid overpainting. m_dimension.end -= m_dimension.stepWidth; } m_annotations = axisPriv->annotations; m_customTicks = axisPriv->customTicksPositions; const qreal inf = std::numeric_limits< qreal >::infinity(); if ( m_customTicks.count() ) { - qSort( m_customTicks.begin(), m_customTicks.end() ); + std::sort(m_customTicks.begin(), m_customTicks.end()); m_customTickIndex = 0; m_customTick = m_customTicks.at( m_customTickIndex ); } else { m_customTickIndex = -1; m_customTick = inf; } if ( m_majorThinningFactor > 1 && hasShorterLabels() ) { m_manualLabelTexts = m_axis->shortLabels(); } else { m_manualLabelTexts = m_axis->labels(); } m_manualLabelIndex = m_manualLabelTexts.isEmpty() ? -1 : 0; if ( !m_dimension.isCalculated ) { // ### depending on the data, it is difficult to impossible to choose anchors (where ticks // corresponding to the header labels are) on the ordinate or even the abscissa with // 2-dimensional data. this should be somewhat mitigated by isCalculated only being false // when header data labels should work, at least that seems to be what the code that sets up // the dimensions is trying to do. QStringList dataHeaderLabels; AbstractDiagram* const dia = plane->diagram(); dataHeaderLabels = dia->itemRowLabels(); if ( !dataHeaderLabels.isEmpty() ) { AttributesModel* model = dia->attributesModel(); const int anchorCount = model->rowCount( QModelIndex() ); if ( anchorCount == dataHeaderLabels.count() ) { for ( int i = 0; i < anchorCount; i++ ) { // ### ordinal number as anchor point generally only works for 1-dimensional data m_dataHeaderLabels.insert( qreal( i ), dataHeaderLabels.at( i ) ); } } } } bool hasMajorTicks = m_axis->rulerAttributes().showMajorTickMarks(); bool hasMinorTicks = m_axis->rulerAttributes().showMinorTickMarks(); init( xy.isY, hasMajorTicks, hasMinorTicks, plane ); } static QMap< qreal, QString > allAxisAnnotations( const AbstractCoordinatePlane *plane, bool isY ) { QMap< qreal, QString > annotations; Q_FOREACH( const AbstractDiagram *diagram, plane->diagrams() ) { const AbstractCartesianDiagram *cd = qobject_cast< const AbstractCartesianDiagram* >( diagram ); if ( !cd ) { continue; } Q_FOREACH( const CartesianAxis *axis, cd->axes() ) { const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get( axis ); if ( axisPriv->isVertical() == isY ) { annotations.unite( axisPriv->annotations ); } } } return annotations; } TickIterator::TickIterator( bool isY, const DataDimension& dimension, bool useAnnotationsForTicks, bool hasMajorTicks, bool hasMinorTicks, CartesianCoordinatePlane* plane ) : m_axis( nullptr ), m_dimension( dimension ), m_majorThinningFactor( 1 ), m_majorLabelCount( 0 ), m_customTickIndex( -1 ), m_manualLabelIndex( -1 ), m_type( NoTick ), m_customTick( std::numeric_limits< qreal >::infinity() ) { if ( useAnnotationsForTicks ) { m_annotations = allAxisAnnotations( plane, isY ); } init( isY, hasMajorTicks, hasMinorTicks, plane ); } void TickIterator::init( bool isY, bool hasMajorTicks, bool hasMinorTicks, CartesianCoordinatePlane* plane ) { Q_ASSERT( std::numeric_limits< qreal >::has_infinity ); m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic; // sanity check against infinite loops hasMajorTicks = hasMajorTicks && ( m_dimension.stepWidth > 0 || m_isLogarithmic ); hasMinorTicks = hasMinorTicks && ( m_dimension.subStepWidth > 0 || m_isLogarithmic ); XySwitch xy( isY ); GridAttributes gridAttributes = plane->gridAttributes( xy( Qt::Horizontal, Qt::Vertical ) ); m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic; if ( !m_isLogarithmic ) { // adjustedLowerUpperRange() is intended for use with linear scaling; specifically it would // round lower bounds < 1 to 0. const bool fixedRange = xy( plane->autoAdjustHorizontalRangeToData(), plane->autoAdjustVerticalRangeToData() ) >= 100; const bool adjustLower = gridAttributes.adjustLowerBoundToGrid() && !fixedRange; const bool adjustUpper = gridAttributes.adjustUpperBoundToGrid() && !fixedRange; m_dimension = AbstractGrid::adjustedLowerUpperRange( m_dimension, adjustLower, adjustUpper ); m_decimalPlaces = numSignificantDecimalPlaces( m_dimension.stepWidth ); } else { // the number of significant decimal places for each label naturally varies with logarithmic scaling m_decimalPlaces = -1; } const qreal inf = std::numeric_limits< qreal >::infinity(); // try to place m_position just in front of the first tick to be drawn so that operator++() // can be used to find the first tick if ( m_isLogarithmic ) { if ( ISNAN( m_dimension.start ) || ISNAN( m_dimension.end ) ) { // this can happen in a spurious paint operation before everything is set up; // just bail out to avoid an infinite loop in that case. m_dimension.start = 0.0; m_dimension.end = 0.0; m_position = inf; m_majorTick = inf; m_minorTick = inf; } else if ( m_dimension.start >= 0 ) { m_position = m_dimension.start ? pow( 10.0, floor( log10( m_dimension.start ) ) - 1.0 ) : 1e-6; m_majorTick = hasMajorTicks ? m_position : inf; m_minorTick = hasMinorTicks ? m_position * 20.0 : inf; } else { m_position = -pow( 10.0, ceil( log10( -m_dimension.start ) ) + 1.0 ); m_majorTick = hasMajorTicks ? m_position : inf; m_minorTick = hasMinorTicks ? m_position * 0.09 : inf; } } else { m_majorTick = hasMajorTicks ? m_dimension.start : inf; m_minorTick = hasMinorTicks ? m_dimension.start : inf; m_position = slightlyLessThan( m_dimension.start ); } ++( *this ); } bool TickIterator::areAlmostEqual( qreal r1, qreal r2 ) const { if ( !m_isLogarithmic ) { qreal span = m_dimension.end - m_dimension.start; if ( span == 0 ) { // When start == end, we still want to show one tick if possible, // which needs this function to perform a reasonable comparison. span = qFuzzyIsNull( m_dimension.start) ? 1 : qAbs( m_dimension.start ); } return qAbs( r2 - r1 ) < ( span ) * 1e-6; } else { return qAbs( r2 - r1 ) < qMax( qAbs( r1 ), qAbs( r2 ) ) * 0.01; } } bool TickIterator::isHigherPrecedence( qreal importantTick, qreal unimportantTick ) const { return importantTick != std::numeric_limits< qreal >::infinity() && ( importantTick <= unimportantTick || areAlmostEqual( importantTick, unimportantTick ) ); } void TickIterator::computeMajorTickLabel( int decimalPlaces ) { if ( m_manualLabelIndex >= 0 ) { m_text = m_manualLabelTexts[ m_manualLabelIndex++ ]; if ( m_manualLabelIndex >= m_manualLabelTexts.count() ) { // manual label texts repeat if there are less label texts than ticks on an axis m_manualLabelIndex = 0; } m_type = m_majorThinningFactor > 1 ? MajorTickManualShort : MajorTickManualLong; } else { // if m_axis is null, we are dealing with grid lines. grid lines never need labels. if ( m_axis && ( m_majorLabelCount++ % m_majorThinningFactor ) == 0 ) { QMap< qreal, QString >::ConstIterator it = constify(m_dataHeaderLabels).lowerBound( slightlyLessThan( m_position ) ); if ( it != m_dataHeaderLabels.constEnd() && areAlmostEqual( it.key(), m_position ) ) { m_text = it.value(); m_type = MajorTickHeaderDataLabel; } else { // 'f' to avoid exponential notation for large numbers, consistent with data value text if ( decimalPlaces < 0 ) { decimalPlaces = numSignificantDecimalPlaces( m_position ); } m_text = QString::number( m_position, 'f', decimalPlaces ); m_type = MajorTick; } } else { m_text.clear(); m_type = MajorTick; } } } void TickIterator::operator++() { if ( isAtEnd() ) { return; } const qreal inf = std::numeric_limits< qreal >::infinity(); // make sure to find the next tick at a value strictly greater than m_position if ( !m_annotations.isEmpty() ) { QMap< qreal, QString >::ConstIterator it = constify(m_annotations).upperBound( m_position ); if ( it != m_annotations.constEnd() ) { m_position = it.key(); m_text = it.value(); m_type = CustomTick; } else { m_position = inf; } } else if ( !m_isLogarithmic && m_dimension.stepWidth * 1e6 < qMax( qAbs( m_dimension.start ), qAbs( m_dimension.end ) ) ) { // If the step width is too small to increase m_position at all, we get an infinite loop. // This usually happens when m_dimension.start == m_dimension.end and both are very large. // When start == end, the step width defaults to 1, and it doesn't scale with start or end. // So currently, we bail and show no tick at all for empty ranges > 10^6, but at least we don't hang. m_position = inf; } else { // advance the calculated ticks if ( m_isLogarithmic ) { while ( m_majorTick <= m_position ) { m_majorTick *= m_position >= 0 ? 10 : 0.1; } while ( m_minorTick <= m_position ) { // the next major tick position should be greater than this m_minorTick += m_majorTick * ( m_position >= 0 ? 0.1 : 1.0 ); } } else { while ( m_majorTick <= m_position ) { m_majorTick += m_dimension.stepWidth; } while ( m_minorTick <= m_position ) { m_minorTick += m_dimension.subStepWidth; } } while ( m_customTickIndex >= 0 && m_customTick <= m_position ) { if ( ++m_customTickIndex >= m_customTicks.count() ) { m_customTickIndex = -1; m_customTick = inf; break; } m_customTick = m_customTicks.at( m_customTickIndex ); } // now see which kind of tick we'll have if ( isHigherPrecedence( m_customTick, m_majorTick ) && isHigherPrecedence( m_customTick, m_minorTick ) ) { m_position = m_customTick; computeMajorTickLabel( -1 ); // override the MajorTick type here because those tick's labels are collision-tested, which we don't want // for custom ticks. they may be arbitrarily close to other ticks, causing excessive label thinning. if ( m_type == MajorTick ) { m_type = CustomTick; } } else if ( isHigherPrecedence( m_majorTick, m_minorTick ) ) { m_position = m_majorTick; if ( m_minorTick != inf ) { // realign minor to major m_minorTick = m_majorTick; } computeMajorTickLabel( m_decimalPlaces ); } else if ( m_minorTick != inf ) { m_position = m_minorTick; m_text.clear(); m_type = MinorTick; } else { m_position = inf; } } if ( m_position > m_dimension.end || ISNAN( m_position ) ) { m_position = inf; // make isAtEnd() return true m_text.clear(); m_type = NoTick; } } CartesianAxis::CartesianAxis( AbstractCartesianDiagram* diagram ) : AbstractAxis ( new Private( diagram, this ), diagram ) { init(); } CartesianAxis::~CartesianAxis() { // when we remove the first axis it will unregister itself and // propagate the next one to the primary, thus the while loop while ( d->mDiagram ) { AbstractCartesianDiagram *cd = qobject_cast< AbstractCartesianDiagram* >( d->mDiagram ); cd->takeAxis( this ); } Q_FOREACH( AbstractDiagram *diagram, d->secondaryDiagrams ) { AbstractCartesianDiagram *cd = qobject_cast< AbstractCartesianDiagram* >( diagram ); cd->takeAxis( this ); } } void CartesianAxis::init() { d->customTickLength = 3; d->position = Bottom; setCachedSizeDirty(); connect( this, SIGNAL(coordinateSystemChanged()), SLOT(coordinateSystemChanged()) ); } bool CartesianAxis::compare( const CartesianAxis* other ) const { if ( other == this ) { return true; } if ( !other ) { return false; } return AbstractAxis::compare( other ) && ( position() == other->position() ) && ( titleText() == other->titleText() ) && ( titleTextAttributes() == other->titleTextAttributes() ); } void CartesianAxis::coordinateSystemChanged() { layoutPlanes(); } void CartesianAxis::setTitleText( const QString& text ) { d->titleText = text; setCachedSizeDirty(); layoutPlanes(); } QString CartesianAxis::titleText() const { return d->titleText; } void CartesianAxis::setTitleTextAttributes( const TextAttributes &a ) { d->titleTextAttributes = a; d->useDefaultTextAttributes = false; setCachedSizeDirty(); layoutPlanes(); } TextAttributes CartesianAxis::titleTextAttributes() const { if ( hasDefaultTitleTextAttributes() ) { TextAttributes ta( textAttributes() ); Measure me( ta.fontSize() ); me.setValue( me.value() * 1.5 ); ta.setFontSize( me ); return ta; } return d->titleTextAttributes; } void CartesianAxis::resetTitleTextAttributes() { d->useDefaultTextAttributes = true; setCachedSizeDirty(); layoutPlanes(); } bool CartesianAxis::hasDefaultTitleTextAttributes() const { return d->useDefaultTextAttributes; } void CartesianAxis::setPosition( Position p ) { if ( d->position == p ) { return; } d->position = p; // Invalidating size is not always necessary if both old and new positions are horizontal or both // vertical, but in practice there could be small differences due to who-knows-what, so always adapt // to the possibly new size. Changing position is expensive anyway. setCachedSizeDirty(); layoutPlanes(); } #if defined(Q_COMPILER_MANGLES_RETURN_TYPE) const #endif CartesianAxis::Position CartesianAxis::position() const { return d->position; } void CartesianAxis::layoutPlanes() { if ( ! d->diagram() || ! d->diagram()->coordinatePlane() ) { return; } AbstractCoordinatePlane* plane = d->diagram()->coordinatePlane(); if ( plane ) { plane->layoutPlanes(); } } static bool referenceDiagramIsBarDiagram( const AbstractDiagram * diagram ) { const AbstractCartesianDiagram * dia = qobject_cast< const AbstractCartesianDiagram * >( diagram ); if ( dia && dia->referenceDiagram() ) dia = dia->referenceDiagram(); return qobject_cast< const BarDiagram* >( dia ) != nullptr; } static bool referenceDiagramNeedsCenteredAbscissaTicks( const AbstractDiagram *diagram ) { const AbstractCartesianDiagram * dia = qobject_cast< const AbstractCartesianDiagram * >( diagram ); if ( dia && dia->referenceDiagram() ) dia = dia->referenceDiagram(); if ( qobject_cast< const BarDiagram* >( dia ) ) return true; if ( qobject_cast< const StockDiagram* >( dia ) ) return true; const LineDiagram * lineDiagram = qobject_cast< const LineDiagram* >( dia ); return lineDiagram && lineDiagram->centerDataPoints(); } bool CartesianAxis::isAbscissa() const { const Qt::Orientation diagramOrientation = referenceDiagramIsBarDiagram( d->diagram() ) ? ( ( BarDiagram* )( d->diagram() ) )->orientation() : Qt::Vertical; return diagramOrientation == Qt::Vertical ? position() == Bottom || position() == Top : position() == Left || position() == Right; } bool CartesianAxis::isOrdinate() const { return !isAbscissa(); } void CartesianAxis::paint( QPainter* painter ) { if ( !d->diagram() || !d->diagram()->coordinatePlane() ) { return; } PaintContext ctx; ctx.setPainter ( painter ); AbstractCoordinatePlane *const plane = d->diagram()->coordinatePlane(); ctx.setCoordinatePlane( plane ); ctx.setRectangle( QRectF( areaGeometry() ) ); PainterSaver painterSaver( painter ); // enable clipping only when required due to zoom, because it slows down painting // (the alternative to clipping when zoomed in requires much more work to paint just the right area) const qreal zoomFactor = d->isVertical() ? plane->zoomFactorY() : plane->zoomFactorX(); if ( zoomFactor > 1.0 ) { painter->setClipRegion( areaGeometry().adjusted( - d->amountOfLeftOverlap - 1, - d->amountOfTopOverlap - 1, d->amountOfRightOverlap + 1, d->amountOfBottomOverlap + 1 ) ); } paintCtx( &ctx ); } const TextAttributes CartesianAxis::Private::titleTextAttributesWithAdjustedRotation() const { TextAttributes titleTA( titleTextAttributes ); int rotation = titleTA.rotation(); if ( position == Left || position == Right ) { rotation += 270; } if ( rotation >= 360 ) { rotation -= 360; } // limit the allowed values to 0, 90, 180, 270 rotation = ( rotation / 90 ) * 90; titleTA.setRotation( rotation ); return titleTA; } QString CartesianAxis::Private::customizedLabelText( const QString& text, Qt::Orientation orientation, qreal value ) const { // ### like in the old code, using int( value ) as column number... QString withUnits = diagram()->unitPrefix( int( value ), orientation, true ) + text + diagram()->unitSuffix( int( value ), orientation, true ); return axis()->customizedLabel( withUnits ); } void CartesianAxis::setTitleSpace( qreal axisTitleSpace ) { d->axisTitleSpace = axisTitleSpace; } qreal CartesianAxis::titleSpace() const { return d->axisTitleSpace; } void CartesianAxis::setTitleSize( qreal value ) { Q_UNUSED( value ) // ### remove me } qreal CartesianAxis::titleSize() const { // ### remove me return 1.0; } void CartesianAxis::Private::drawTitleText( QPainter* painter, CartesianCoordinatePlane* plane, const QRect& geoRect ) const { const TextAttributes titleTA( titleTextAttributesWithAdjustedRotation() ); if ( titleTA.isVisible() ) { TextLayoutItem titleItem( titleText, titleTA, plane->parent(), KChartEnums::MeasureOrientationMinimum, Qt::AlignHCenter | Qt::AlignVCenter ); QPointF point; QSize size = titleItem.sizeHint(); switch ( position ) { case Top: point.setX( geoRect.left() + geoRect.width() / 2 ); point.setY( geoRect.top() + ( size.height() / 2 ) / axisTitleSpace ); size.setWidth( qMin( size.width(), axis()->geometry().width() ) ); break; case Bottom: point.setX( geoRect.left() + geoRect.width() / 2 ); point.setY( geoRect.bottom() - ( size.height() / 2 ) / axisTitleSpace ); size.setWidth( qMin( size.width(), axis()->geometry().width() ) ); break; case Left: point.setX( geoRect.left() + ( size.width() / 2 ) / axisTitleSpace ); point.setY( geoRect.top() + geoRect.height() / 2 ); size.setHeight( qMin( size.height(), axis()->geometry().height() ) ); break; case Right: point.setX( geoRect.right() - ( size.width() / 2 ) / axisTitleSpace ); point.setY( geoRect.top() + geoRect.height() / 2 ); size.setHeight( qMin( size.height(), axis()->geometry().height() ) ); break; } const PainterSaver painterSaver( painter ); painter->setClipping( false ); painter->translate( point ); titleItem.setGeometry( QRect( QPoint( -size.width() / 2, -size.height() / 2 ), size ) ); titleItem.paint( painter ); } } bool CartesianAxis::Private::isVertical() const { return axis()->isAbscissa() == AbstractDiagram::Private::get( diagram() )->isTransposed(); } void CartesianAxis::paintCtx( PaintContext* context ) { Q_ASSERT_X ( d->diagram(), "CartesianAxis::paint", "Function call not allowed: The axis is not assigned to any diagram." ); CartesianCoordinatePlane* plane = dynamic_cast( context->coordinatePlane() ); Q_ASSERT_X ( plane, "CartesianAxis::paint", "Bad function call: PaintContext::coordinatePlane() NOT a cartesian plane." ); // note: Not having any data model assigned is no bug // but we can not draw an axis then either. if ( !d->diagram()->model() ) { return; } const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks( d->diagram() ) && isAbscissa(); XySwitch geoXy( d->isVertical() ); QPainter* const painter = context->painter(); // determine the position of the axis (also required for labels) and paint it qreal transversePosition = signalingNaN; // in data space // the next one describes an additional shift in screen space; it is unfortunately required to // make axis sharing work, which uses the areaGeometry() to override the position of the axis. qreal transverseScreenSpaceShift = signalingNaN; { // determine the unadulterated position in screen space DataDimension dimX = plane->gridDimensionsList().first(); DataDimension dimY = plane->gridDimensionsList().last(); QPointF start( dimX.start, dimY.start ); QPointF end( dimX.end, dimY.end ); // consider this: you can turn a diagonal line into a horizontal or vertical line on any // edge by changing just one of its four coordinates. switch ( position() ) { case CartesianAxis::Bottom: end.setY( dimY.start ); break; case CartesianAxis::Top: start.setY( dimY.end ); break; case CartesianAxis::Left: end.setX( dimX.start ); break; case CartesianAxis::Right: start.setX( dimX.end ); break; } transversePosition = geoXy( start.y(), start.x() ); QPointF transStart = plane->translate( start ); QPointF transEnd = plane->translate( end ); // an externally set areaGeometry() moves the axis position transversally; the shift is // nonzero only when this is a shared axis const QRect geo = areaGeometry(); switch ( position() ) { case CartesianAxis::Bottom: transverseScreenSpaceShift = geo.top() - transStart.y(); break; case CartesianAxis::Top: transverseScreenSpaceShift = geo.bottom() - transStart.y(); break; case CartesianAxis::Left: transverseScreenSpaceShift = geo.right() - transStart.x(); break; case CartesianAxis::Right: transverseScreenSpaceShift = geo.left() - transStart.x(); break; } geoXy.lvalue( transStart.ry(), transStart.rx() ) += transverseScreenSpaceShift; geoXy.lvalue( transEnd.ry(), transEnd.rx() ) += transverseScreenSpaceShift; if ( rulerAttributes().showRulerLine() ) { bool clipSaved = context->painter()->hasClipping(); painter->setClipping( false ); painter->drawLine( transStart, transEnd ); painter->setClipping( clipSaved ); } } // paint ticks and labels TextAttributes labelTA = textAttributes(); RulerAttributes rulerAttr = rulerAttributes(); int labelThinningFactor = 1; // TODO: label thinning also when grid line distance < 4 pixels, not only when labels collide TextLayoutItem *tickLabel = new TextLayoutItem( QString(), labelTA, plane->parent(), KChartEnums::MeasureOrientationMinimum, Qt::AlignLeft ); TextLayoutItem *prevTickLabel = new TextLayoutItem( QString(), labelTA, plane->parent(), KChartEnums::MeasureOrientationMinimum, Qt::AlignLeft ); QPointF prevTickLabelPos; enum { Layout = 0, Painting, Done }; for ( int step = labelTA.isVisible() ? Layout : Painting; step < Done; step++ ) { bool skipFirstTick = !rulerAttr.showFirstTick(); bool isFirstLabel = true; for ( TickIterator it( this, plane, labelThinningFactor, centerTicks ); !it.isAtEnd(); ++it ) { if ( skipFirstTick ) { skipFirstTick = false; continue; } const qreal drawPos = it.position() + ( centerTicks ? 0.5 : 0. ); QPointF onAxis = plane->translate( geoXy( QPointF( drawPos, transversePosition ) , QPointF( transversePosition, drawPos ) ) ); geoXy.lvalue( onAxis.ry(), onAxis.rx() ) += transverseScreenSpaceShift; const bool isOutwardsPositive = position() == Bottom || position() == Right; // paint the tick mark QPointF tickEnd = onAxis; qreal tickLen = it.type() == TickIterator::CustomTick ? d->customTickLength : tickLength( it.type() == TickIterator::MinorTick ); geoXy.lvalue( tickEnd.ry(), tickEnd.rx() ) += isOutwardsPositive ? tickLen : -tickLen; // those adjustments are required to paint the ticks exactly on the axis and of the right length if ( position() == Top ) { onAxis.ry() += 1; tickEnd.ry() += 1; } else if ( position() == Left ) { tickEnd.rx() += 1; } if ( step == Painting ) { painter->save(); if ( rulerAttr.hasTickMarkPenAt( it.position() ) ) { painter->setPen( rulerAttr.tickMarkPen( it.position() ) ); } else { painter->setPen( it.type() == TickIterator::MinorTick ? rulerAttr.minorTickMarkPen() : rulerAttr.majorTickMarkPen() ); } painter->drawLine( onAxis, tickEnd ); painter->restore(); } if ( it.text().isEmpty() || !labelTA.isVisible() ) { // the following code in the loop is only label painting, so skip it continue; } // paint the label QString text = it.text(); if ( it.type() == TickIterator::MajorTick ) { // add unit prefixes and suffixes, then customize text = d->customizedLabelText( text, geoXy( Qt::Horizontal, Qt::Vertical ), it.position() ); } else if ( it.type() == TickIterator::MajorTickHeaderDataLabel ) { // unit prefixes and suffixes have already been added in this case - only customize text = customizedLabel( text ); } tickLabel->setText( text ); QSizeF size = QSizeF( tickLabel->sizeHint() ); QPolygon labelPoly = tickLabel->boundingPolygon(); Q_ASSERT( labelPoly.count() == 4 ); // for alignment, find the label polygon edge "most parallel" and closest to the axis int axisAngle = 0; switch ( position() ) { case Bottom: axisAngle = 0; break; case Top: axisAngle = 180; break; case Right: axisAngle = 270; break; case Left: axisAngle = 90; break; default: Q_ASSERT( false ); } // the left axis is not actually pointing down and the top axis not actually pointing // left, but their corresponding closest edges of a rectangular unrotated label polygon are. int relAngle = axisAngle - labelTA.rotation() + 45; if ( relAngle < 0 ) { relAngle += 360; } int polyCorner1 = relAngle / 90; QPoint p1 = labelPoly.at( polyCorner1 ); QPoint p2 = labelPoly.at( polyCorner1 == 3 ? 0 : ( polyCorner1 + 1 ) ); QPointF labelPos = tickEnd; qreal labelMargin = rulerAttr.labelMargin(); if ( labelMargin < 0 ) { labelMargin = QFontMetricsF( tickLabel->realFont() ).height() * 0.5; } labelMargin -= tickLabel->marginWidth(); // make up for the margin that's already there switch ( position() ) { case Left: labelPos += QPointF( -size.width() - labelMargin, -0.45 * size.height() - 0.5 * ( p1.y() + p2.y() ) ); break; case Right: labelPos += QPointF( labelMargin, -0.45 * size.height() - 0.5 * ( p1.y() + p2.y() ) ); break; case Top: labelPos += QPointF( -0.45 * size.width() - 0.5 * ( p1.x() + p2.x() ), -size.height() - labelMargin ); break; case Bottom: labelPos += QPointF( -0.45 * size.width() - 0.5 * ( p1.x() + p2.x() ), labelMargin ); break; } tickLabel->setGeometry( QRect( labelPos.toPoint(), size.toSize() ) ); if ( step == Painting ) { tickLabel->paint( painter ); } // collision check the current label against the previous one // like in the old code, we don't shorten or decimate labels if they are already the // manual short type, or if they are the manual long type and on the vertical axis // ### they can still collide though, especially when they're rotated! if ( step == Layout ) { int spaceSavingRotation = geoXy( 270, 0 ); bool canRotate = labelTA.autoRotate() && labelTA.rotation() != spaceSavingRotation; const bool canShortenLabels = !geoXy.isY && it.type() == TickIterator::MajorTickManualLong && it.hasShorterLabels(); bool collides = false; if ( it.type() == TickIterator::MajorTick || it.type() == TickIterator::MajorTickHeaderDataLabel || canShortenLabels || canRotate ) { if ( isFirstLabel ) { isFirstLabel = false; } else { collides = tickLabel->intersects( *prevTickLabel, labelPos, prevTickLabelPos ); qSwap( prevTickLabel, tickLabel ); } prevTickLabelPos = labelPos; } if ( collides ) { // to make room, we try in order: shorten, rotate, decimate if ( canRotate && !canShortenLabels ) { labelTA.setRotation( spaceSavingRotation ); // tickLabel will be reused in the next round tickLabel->setTextAttributes( labelTA ); } else { labelThinningFactor++; } step--; // relayout break; } } } } delete tickLabel; tickLabel = nullptr; delete prevTickLabel; prevTickLabel = nullptr; if ( ! titleText().isEmpty() ) { d->drawTitleText( painter, plane, geometry() ); } } /* pure virtual in QLayoutItem */ bool CartesianAxis::isEmpty() const { return false; // if the axis exists, it has some (perhaps default) content } /* pure virtual in QLayoutItem */ Qt::Orientations CartesianAxis::expandingDirections() const { Qt::Orientations ret; switch ( position() ) { case Bottom: case Top: ret = Qt::Horizontal; break; case Left: case Right: ret = Qt::Vertical; break; default: Q_ASSERT( false ); break; }; return ret; } void CartesianAxis::setCachedSizeDirty() const { d->cachedMaximumSize = QSize(); } /* pure virtual in QLayoutItem */ QSize CartesianAxis::maximumSize() const { if ( ! d->cachedMaximumSize.isValid() ) d->cachedMaximumSize = d->calculateMaximumSize(); return d->cachedMaximumSize; } QSize CartesianAxis::Private::calculateMaximumSize() const { if ( !diagram() ) { return QSize(); } CartesianCoordinatePlane* plane = dynamic_cast< CartesianCoordinatePlane* >( diagram()->coordinatePlane() ); Q_ASSERT( plane ); QObject* refArea = plane->parent(); const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks( diagram() ) && axis()->isAbscissa(); // we ignore: // - label thinning (expensive, not worst case and we want worst case) // - label autorotation (expensive, obscure feature(?)) // - axis length (it is determined by the plane / diagram / chart anyway) // - the title's influence on axis length; this one might be TODO. See KDCH-863. XySwitch geoXy( isVertical() ); qreal size = 0; // this is the size transverse to the axis direction // the following variables describe how much the first and last label stick out over the axis // area, so that the geometry of surrounding layout items can be adjusted to make room. qreal startOverhang = 0.0; qreal endOverhang = 0.0; if ( mAxis->textAttributes().isVisible() ) { // these four are used just to calculate startOverhang and endOverhang qreal lowestLabelPosition = signalingNaN; qreal highestLabelPosition = signalingNaN; qreal lowestLabelLongitudinalSize = signalingNaN; qreal highestLabelLongitudinalSize = signalingNaN; TextLayoutItem tickLabel( QString(), mAxis->textAttributes(), refArea, KChartEnums::MeasureOrientationMinimum, Qt::AlignLeft ); const RulerAttributes rulerAttr = mAxis->rulerAttributes(); bool showFirstTick = rulerAttr.showFirstTick(); for ( TickIterator it( axis(), plane, 1, centerTicks ); !it.isAtEnd(); ++it ) { const qreal drawPos = it.position() + ( centerTicks ? 0.5 : 0. ); if ( !showFirstTick ) { showFirstTick = true; continue; } qreal labelSizeTransverse = 0.0; qreal labelMargin = 0.0; QString text = it.text(); if ( !text.isEmpty() ) { QPointF labelPosition = plane->translate( QPointF( geoXy( drawPos, 1.0 ), geoXy( 1.0, drawPos ) ) ); highestLabelPosition = geoXy( labelPosition.x(), labelPosition.y() ); if ( it.type() == TickIterator::MajorTick ) { // add unit prefixes and suffixes, then customize text = customizedLabelText( text, geoXy( Qt::Horizontal, Qt::Vertical ), it.position() ); } else if ( it.type() == TickIterator::MajorTickHeaderDataLabel ) { // unit prefixes and suffixes have already been added in this case - only customize text = axis()->customizedLabel( text ); } tickLabel.setText( text ); QSize sz = tickLabel.sizeHint(); highestLabelLongitudinalSize = geoXy( sz.width(), sz.height() ); if ( ISNAN( lowestLabelLongitudinalSize ) ) { lowestLabelLongitudinalSize = highestLabelLongitudinalSize; lowestLabelPosition = highestLabelPosition; } labelSizeTransverse = geoXy( sz.height(), sz.width() ); labelMargin = rulerAttr.labelMargin(); if ( labelMargin < 0 ) { labelMargin = QFontMetricsF( tickLabel.realFont() ).height() * 0.5; } labelMargin -= tickLabel.marginWidth(); // make up for the margin that's already there } qreal tickLength = it.type() == TickIterator::CustomTick ? customTickLength : axis()->tickLength( it.type() == TickIterator::MinorTick ); size = qMax( size, tickLength + labelMargin + labelSizeTransverse ); } const DataDimension dimX = plane->gridDimensionsList().first(); const DataDimension dimY = plane->gridDimensionsList().last(); QPointF pt = plane->translate( QPointF( dimX.start, dimY.start ) ); const qreal lowestPosition = geoXy( pt.x(), pt.y() ); pt = plane->translate( QPointF( dimX.end, dimY.end ) ); const qreal highestPosition = geoXy( pt.x(), pt.y() ); // the geoXy( 1.0, -1.0 ) here is necessary because Qt's y coordinate is inverted startOverhang = qMax( 0.0, ( lowestPosition - lowestLabelPosition ) * geoXy( 1.0, -1.0 ) + lowestLabelLongitudinalSize * 0.5 ); endOverhang = qMax( 0.0, ( highestLabelPosition - highestPosition ) * geoXy( 1.0, -1.0 ) + highestLabelLongitudinalSize * 0.5 ); } amountOfLeftOverlap = geoXy( startOverhang, 0.0 ); amountOfRightOverlap = geoXy( endOverhang, 0.0 ); amountOfBottomOverlap = geoXy( 0.0, startOverhang ); amountOfTopOverlap = geoXy( 0.0, endOverhang ); const TextAttributes titleTA = titleTextAttributesWithAdjustedRotation(); if ( titleTA.isVisible() && !axis()->titleText().isEmpty() ) { TextLayoutItem title( axis()->titleText(), titleTA, refArea, KChartEnums::MeasureOrientationMinimum, Qt::AlignHCenter | Qt::AlignVCenter ); QFontMetricsF titleFM( title.realFont(), GlobalMeasureScaling::paintDevice() ); size += geoXy( titleFM.height() * 0.33, titleFM.averageCharWidth() * 0.55 ); // spacing size += geoXy( title.sizeHint().height(), title.sizeHint().width() ); } // the size parallel to the axis direction is not determined by us, so we just return 1 return QSize( geoXy( 1, int( size ) ), geoXy( int ( size ), 1 ) ); } /* pure virtual in QLayoutItem */ QSize CartesianAxis::minimumSize() const { return maximumSize(); } /* pure virtual in QLayoutItem */ QSize CartesianAxis::sizeHint() const { return maximumSize(); } /* pure virtual in QLayoutItem */ void CartesianAxis::setGeometry( const QRect& r ) { if ( d->geometry != r ) { d->geometry = r; setCachedSizeDirty(); } } /* pure virtual in QLayoutItem */ QRect CartesianAxis::geometry() const { return d->geometry; } void CartesianAxis::setCustomTickLength( int value ) { if ( d->customTickLength == value ) { return; } d->customTickLength = value; setCachedSizeDirty(); layoutPlanes(); } int CartesianAxis::customTickLength() const { return d->customTickLength; } int CartesianAxis::tickLength( bool subUnitTicks ) const { const RulerAttributes& rulerAttr = rulerAttributes(); return subUnitTicks ? rulerAttr.minorTickMarkLength() : rulerAttr.majorTickMarkLength(); } QMap< qreal, QString > CartesianAxis::annotations() const { return d->annotations; } void CartesianAxis::setAnnotations( const QMap< qreal, QString >& annotations ) { if ( d->annotations == annotations ) return; d->annotations = annotations; setCachedSizeDirty(); layoutPlanes(); } QList< qreal > CartesianAxis::customTicks() const { return d->customTicksPositions; } void CartesianAxis::setCustomTicks( const QList< qreal >& customTicksPositions ) { if ( d->customTicksPositions == customTicksPositions ) return; d->customTicksPositions = customTicksPositions; setCachedSizeDirty(); layoutPlanes(); } #if !defined(QT_NO_DEBUG_STREAM) QDebug operator<<(QDebug dbg, KChart::CartesianAxis::Position pos) { switch (pos) { case KChart::CartesianAxis::Bottom: dbg << "KChart::CartesianAxis::Bottom"; break; case KChart::CartesianAxis::Top: dbg << "KChart::CartesianAxis::Top"; break; case KChart::CartesianAxis::Left: dbg << "KChart::CartesianAxis::Left"; break; case KChart::CartesianAxis::Right: dbg << "KChart::CartesianAxis::Right"; break; default: dbg << "KChart::CartesianAxis::Invalid"; break; } return dbg; } #endif diff --git a/src/KChart/Cartesian/KChartCartesianCoordinatePlane.cpp b/src/KChart/Cartesian/KChartCartesianCoordinatePlane.cpp index 75dd8f3..b663a25 100644 --- a/src/KChart/Cartesian/KChartCartesianCoordinatePlane.cpp +++ b/src/KChart/Cartesian/KChartCartesianCoordinatePlane.cpp @@ -1,920 +1,921 @@ /* * 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 "KChartCartesianCoordinatePlane.h" #include "KChartCartesianCoordinatePlane_p.h" #include "KChartAbstractDiagram.h" #include "KChartAbstractDiagram_p.h" #include "KChartAbstractCartesianDiagram.h" #include "CartesianCoordinateTransformation.h" #include "KChartGridAttributes.h" #include "KChartPaintContext.h" #include "KChartPainterSaver_p.h" #include "KChartBarDiagram.h" #include "KChartStockDiagram.h" #include #include #include #include #include #include +#include using namespace KChart; #define d d_func() CartesianCoordinatePlane::Private::Private() : AbstractCoordinatePlane::Private() , bPaintIsRunning( false ) , hasOwnGridAttributesHorizontal( false ) , hasOwnGridAttributesVertical( false ) , isometricScaling( false ) , horizontalMin( 0 ) , horizontalMax( 0 ) , verticalMin( 0 ) , verticalMax( 0 ) , autoAdjustHorizontalRangeToData( 67 ) , autoAdjustVerticalRangeToData( 67 ) , autoAdjustGridToZoom( true ) , fixedDataCoordinateSpaceRelation( false ) , xAxisStartAtZero( true ) , reverseVerticalPlane( false ) , reverseHorizontalPlane( false ) { } CartesianCoordinatePlane::CartesianCoordinatePlane( Chart* parent ) : AbstractCoordinatePlane( new Private(), parent ) { // this bloc left empty intentionally } CartesianCoordinatePlane::~CartesianCoordinatePlane() { // this bloc left empty intentionally } void CartesianCoordinatePlane::init() { // this bloc left empty intentionally } void CartesianCoordinatePlane::addDiagram( AbstractDiagram* diagram ) { Q_ASSERT_X( dynamic_cast( diagram ), "CartesianCoordinatePlane::addDiagram", "Only cartesian " "diagrams can be added to a cartesian coordinate plane!" ); AbstractCoordinatePlane::addDiagram( diagram ); connect( diagram, SIGNAL(layoutChanged(AbstractDiagram*)), SLOT(slotLayoutChanged(AbstractDiagram*)) ); connect( diagram, SIGNAL(propertiesChanged()), this, SIGNAL(propertiesChanged()) ); } void CartesianCoordinatePlane::paint( QPainter* painter ) { // prevent recursive call: if ( d->bPaintIsRunning ) { return; } d->bPaintIsRunning = true; AbstractDiagramList diags = diagrams(); if ( !diags.isEmpty() ) { PaintContext ctx; ctx.setPainter ( painter ); ctx.setCoordinatePlane ( this ); const QRectF drawArea( drawingArea() ); ctx.setRectangle ( drawArea ); // enabling clipping so that we're not drawing outside PainterSaver painterSaver( painter ); QRect clipRect = drawArea.toRect().adjusted( -1, -1, 1, 1 ); QRegion clipRegion( clipRect ); painter->setClipRegion( clipRegion ); // paint the coordinate system rulers: d->grid->drawGrid( &ctx ); // paint the diagrams: for ( int i = 0; i < diags.size(); i++ ) { if ( diags[i]->isHidden() ) { continue; } bool doDumpPaintTime = AbstractDiagram::Private::get( diags[ i ] )->doDumpPaintTime; - QTime stopWatch; + QElapsedTimer stopWatch; if ( doDumpPaintTime ) { stopWatch.start(); } PainterSaver diagramPainterSaver( painter ); diags[i]->paint( &ctx ); if ( doDumpPaintTime ) { qDebug() << "Painting diagram" << i << "took" << stopWatch.elapsed() << "milliseconds"; } } } d->bPaintIsRunning = false; } void CartesianCoordinatePlane::slotLayoutChanged( AbstractDiagram* ) { layoutDiagrams(); } QRectF CartesianCoordinatePlane::getRawDataBoundingRectFromDiagrams() const { // determine unit of the rectangles of all involved diagrams: qreal minX = 0; qreal maxX = 0; qreal minY = 0; qreal maxY = 0; bool bStarting = true; Q_FOREACH( const AbstractDiagram* diagram, diagrams() ) { QPair dataBoundariesPair = diagram->dataBoundaries(); //qDebug() << "CartesianCoordinatePlane::getRawDataBoundingRectFromDiagrams()\ngets diagram->dataBoundaries: " << dataBoundariesPair.first << dataBoundariesPair.second; if ( bStarting || dataBoundariesPair.first.x() < minX ) minX = dataBoundariesPair.first.x(); if ( bStarting || dataBoundariesPair.first.y() < minY ) minY = dataBoundariesPair.first.y(); if ( bStarting || dataBoundariesPair.second.x() > maxX ) maxX = dataBoundariesPair.second.x(); if ( bStarting || dataBoundariesPair.second.y() > maxY ) maxY = dataBoundariesPair.second.y(); bStarting = false; } //qDebug() << "CartesianCoordinatePlane::getRawDataBoundingRectFromDiagrams()\nreturns data boundaries: " << QRectF( QPointF(minX, minY), QSizeF(maxX - minX, maxY - minY) ); QRectF dataBoundingRect; dataBoundingRect.setBottomLeft( QPointF( minX, minY ) ); dataBoundingRect.setTopRight( QPointF( maxX, maxY ) ); return dataBoundingRect; } QRectF CartesianCoordinatePlane::adjustedToMaxEmptyInnerPercentage( const QRectF& r, unsigned int percentX, unsigned int percentY ) const { QRectF ret = r; if ( ( axesCalcModeX() != Logarithmic || r.left() < 0.0 ) && percentX > 0 && percentX != 100 ) { const bool isPositive = r.left() >= 0; if ( ( r.right() >= 0 ) == isPositive ) { qreal upperBound = qMax( r.left(), r.right() ); qreal lowerBound = qMin( r.left(), r.right() ); qreal innerBound = isPositive ? lowerBound : upperBound; qreal outerBound = isPositive ? upperBound : lowerBound; if ( innerBound / outerBound * 100 <= percentX && d->xAxisStartAtZero ) { if ( isPositive ) { ret.setLeft( 0.0 ); } else { ret.setRight( 0.0 ); } } } } // ### this doesn't seem to take into account that Qt's y coordinate is inverted if ( ( axesCalcModeY() != Logarithmic || r.bottom() < 0.0 ) && percentY > 0 && percentY != 100 ) { const bool isPositive = r.bottom() >= 0; if ( ( r.top() >= 0 ) == isPositive ) { qreal upperBound = qMax( r.top(), r.bottom() ); qreal lowerBound = qMin( r.top(), r.bottom() ); const qreal innerBound = isPositive ? lowerBound : upperBound; const qreal outerBound = isPositive ? upperBound : lowerBound; if ( innerBound / outerBound * 100 <= percentY ) { if ( isPositive ) { ret.setBottom( 0.0 ); } else { ret.setTop( 0.0 ); } } } } return ret; } QRectF CartesianCoordinatePlane::calculateRawDataBoundingRect() const { // are manually set ranges to be applied? const bool bAutoAdjustHorizontalRange = d->autoAdjustHorizontalRangeToData < 100; const bool bAutoAdjustVerticalRange = d->autoAdjustVerticalRangeToData < 100; const bool bHardHorizontalRange = (!bAutoAdjustHorizontalRange) && (d->horizontalMin != d->horizontalMax || (ISNAN(d->horizontalMin) != ISNAN(d->horizontalMax))); const bool bHardVerticalRange = (!bAutoAdjustVerticalRange) && (d->verticalMin != d->verticalMax || (ISNAN(d->verticalMin) != ISNAN(d->verticalMax))); QRectF dataBoundingRect; // if custom boundaries are set on the plane, use them if ( bHardHorizontalRange && bHardVerticalRange ) { dataBoundingRect.setLeft( d->horizontalMin ); dataBoundingRect.setRight( d->horizontalMax ); dataBoundingRect.setBottom( d->verticalMin ); dataBoundingRect.setTop( d->verticalMax ); } else { // determine unit of the rectangles of all involved diagrams: dataBoundingRect = getRawDataBoundingRectFromDiagrams(); if ( bHardHorizontalRange ) { if (!ISNAN(d->horizontalMin)) dataBoundingRect.setLeft( d->horizontalMin ); if (!ISNAN(d->horizontalMax)) dataBoundingRect.setRight( d->horizontalMax ); } if ( bHardVerticalRange ) { if (!ISNAN(d->verticalMin)) dataBoundingRect.setBottom( d->verticalMin ); if (!ISNAN(d->verticalMax)) dataBoundingRect.setTop( d->verticalMax ); } } // recalculate the bounds, if automatic adjusting of ranges is desired AND // both bounds are at the same side of the zero line dataBoundingRect = adjustedToMaxEmptyInnerPercentage( dataBoundingRect, d->autoAdjustHorizontalRangeToData, d->autoAdjustVerticalRangeToData ); if ( bAutoAdjustHorizontalRange ) { const_cast( this )->d->horizontalMin = dataBoundingRect.left(); const_cast( this )->d->horizontalMax = dataBoundingRect.right(); } if ( bAutoAdjustVerticalRange ) { const_cast( this )->d->verticalMin = dataBoundingRect.bottom(); const_cast( this )->d->verticalMax = dataBoundingRect.top(); } // qDebug() << Q_FUNC_INFO << dataBoundingRect; return dataBoundingRect; } DataDimensionsList CartesianCoordinatePlane::getDataDimensionsList() const { const AbstractCartesianDiagram* dgr = diagrams().isEmpty() ? nullptr : qobject_cast< const AbstractCartesianDiagram* >( diagrams().first() ); if ( dgr && dgr->referenceDiagram() ) { dgr = dgr->referenceDiagram(); } const BarDiagram *barDiagram = qobject_cast< const BarDiagram* >( dgr ); const StockDiagram *stockDiagram = qobject_cast< const StockDiagram* >( dgr ); // note: // It does make sense to retrieve the orientation from the first diagram. This is because // a coordinate plane can either be for horizontal *or* for vertical diagrams. Both at the // same time won't work, and thus the orientation for all diagrams is the same as for the first one. const Qt::Orientation diagramOrientation = barDiagram != nullptr ? barDiagram->orientation() : Qt::Vertical; const bool diagramIsVertical = diagramOrientation == Qt::Vertical; DataDimensionsList l; if ( dgr ) { const QRectF r( calculateRawDataBoundingRect() ); // We do not access d->gridAttributesHorizontal/Vertical here, but we use the getter function, // to get the global attrs, if no special ones have been set for the given orientation. const GridAttributes gaH( gridAttributes( Qt::Horizontal ) ); const GridAttributes gaV( gridAttributes( Qt::Vertical ) ); // append the first dimension: for Abscissa axes l.append( DataDimension( r.left(), r.right(), diagramIsVertical ? ( !stockDiagram && dgr->datasetDimension() > 1 ) : true, axesCalcModeX(), gaH.gridGranularitySequence(), gaH.gridStepWidth(), gaH.gridSubStepWidth() ) ); // append the second dimension: for Ordinate axes l.append( DataDimension( r.bottom(), r.top(), diagramIsVertical ? true : ( dgr->datasetDimension() > 1 ), axesCalcModeY(), gaV.gridGranularitySequence(), gaV.gridStepWidth(), gaV.gridSubStepWidth() ) ); } else { l.append( DataDimension() ); // This gets us the default 1..0 / 1..0 grid l.append( DataDimension() ); // shown, if there is no diagram on this plane. } return l; } QRectF CartesianCoordinatePlane::drawingArea() const { // the rectangle the diagrams cover in the *plane*: // We reserve 1px on each side for antialiased drawing, and respect the way QPainter calculates // the width of a painted rect (the size is the rectangle size plus the pen width). The latter // accounts for another pixel that we subtract from height and width. // This way, most clipping for regular pens should be avoided. When pens with a width larger // than 1 are used, this may not be sufficient. return QRectF( areaGeometry() ).adjusted( 1.0, 1.0, -2.0, -2.0 ); } QRectF CartesianCoordinatePlane::logicalArea() const { if ( d->dimensions.isEmpty() ) return QRectF(); const DataDimension dimX = d->dimensions.first(); const DataDimension dimY = d->dimensions.last(); const QPointF pt( qMin( dimX.start, dimX.end ), qMax( dimY.start, dimY.end ) ); const QSizeF siz( qAbs( dimX.distance() ), -qAbs( dimY.distance() ) ); const QRectF dataBoundingRect( pt, siz ); // determine logical top left, taking the "reverse" options into account const QPointF topLeft( d->reverseHorizontalPlane ? dataBoundingRect.right() : dataBoundingRect.left(), d->reverseVerticalPlane ? dataBoundingRect.bottom() : dataBoundingRect.top() ); const qreal width = dataBoundingRect.width() * ( d->reverseHorizontalPlane ? -1.0 : 1.0 ); const qreal height = dataBoundingRect.height() * ( d->reverseVerticalPlane ? -1.0 : 1.0 ); return QRectF( topLeft, QSizeF( width, height ) ); } QRectF CartesianCoordinatePlane::diagramArea() const { const QRectF logArea( logicalArea() ); QPointF physicalTopLeft = d->coordinateTransformation.translate( logArea.topLeft() ); QPointF physicalBottomRight = d->coordinateTransformation.translate( logArea.bottomRight() ); return QRectF( physicalTopLeft, physicalBottomRight ).normalized(); } QRectF CartesianCoordinatePlane::visibleDiagramArea() const { return diagramArea().intersected( drawingArea() ); } void CartesianCoordinatePlane::layoutDiagrams() { d->dimensions = gridDimensionsList(); Q_ASSERT_X ( d->dimensions.count() == 2, "CartesianCoordinatePlane::layoutDiagrams", "Error: gridDimensionsList() did not return exactly two dimensions." ); // physical area of the plane const QRectF physicalArea( drawingArea() ); // .. in contrast to the logical area const QRectF logArea( logicalArea() ); // TODO: isometric scaling for zooming? // the plane area might have changed, so the zoom values might also be changed handleFixedDataCoordinateSpaceRelation( physicalArea ); d->coordinateTransformation.updateTransform( logArea, physicalArea ); update(); } void CartesianCoordinatePlane::setFixedDataCoordinateSpaceRelation( bool fixed ) { d->fixedDataCoordinateSpaceRelation = fixed; d->fixedDataCoordinateSpaceRelationPinnedSize = QSize(); handleFixedDataCoordinateSpaceRelation( drawingArea() ); } bool CartesianCoordinatePlane::hasFixedDataCoordinateSpaceRelation() const { return d->fixedDataCoordinateSpaceRelation; } void CartesianCoordinatePlane::setXAxisStartAtZero(bool fixedStart) { if (d->xAxisStartAtZero == fixedStart) return; d->xAxisStartAtZero = fixedStart; } bool CartesianCoordinatePlane::xAxisStartAtZero() const { return d->xAxisStartAtZero; } void CartesianCoordinatePlane::handleFixedDataCoordinateSpaceRelation( const QRectF& geometry ) { if ( !d->fixedDataCoordinateSpaceRelation ) { return; } // is the new geometry ok? if ( !geometry.isValid() ) { return; } // note that the pinned size can be invalid even after setting it, if geometry wasn't valid. // this is relevant for the cooperation between this method, setFixedDataCoordinateSpaceRelation(), // and handleFixedDataCoordinateSpaceRelation(). if ( !d->fixedDataCoordinateSpaceRelationPinnedSize.isValid() ) { d->fixedDataCoordinateSpaceRelationPinnedSize = geometry.size(); d->fixedDataCoordinateSpaceRelationPinnedZoom = ZoomParameters( zoomFactorX(), zoomFactorY(), zoomCenter() ); return; } // if the plane size was changed, change zoom factors to keep the diagram size constant if ( d->fixedDataCoordinateSpaceRelationPinnedSize != geometry.size() ) { const qreal widthScaling = d->fixedDataCoordinateSpaceRelationPinnedSize.width() / geometry.width(); const qreal heightScaling = d->fixedDataCoordinateSpaceRelationPinnedSize.height() / geometry.height(); const qreal newZoomX = d->fixedDataCoordinateSpaceRelationPinnedZoom.xFactor * widthScaling; const qreal newZoomY = d->fixedDataCoordinateSpaceRelationPinnedZoom.yFactor * heightScaling; const QPointF newCenter = QPointF( d->fixedDataCoordinateSpaceRelationPinnedZoom.xCenter / widthScaling, d->fixedDataCoordinateSpaceRelationPinnedZoom.yCenter / heightScaling ); // Use these internal methods to avoid sending the propertiesChanged signal more than once bool changed = false; if ( doneSetZoomFactorY( newZoomY ) ) changed = true; if ( doneSetZoomFactorX( newZoomX ) ) changed = true; if ( doneSetZoomCenter( newCenter ) ) changed = true; if ( changed ) emit propertiesChanged(); } } const QPointF CartesianCoordinatePlane::translate( const QPointF& diagramPoint ) const { // Note: We do not test if the point lays inside of the data area, // but we just apply the transformation calculations to the point. // This allows for basic calculations done by the user, see e.g. // the file examples/Lines/BubbleChart/mainwindow.cpp return d->coordinateTransformation.translate( diagramPoint ); } const QPointF CartesianCoordinatePlane::translateBack( const QPointF& screenPoint ) const { return d->coordinateTransformation.translateBack( screenPoint ); } void CartesianCoordinatePlane::setIsometricScaling( bool isOn ) { if ( d->isometricScaling != isOn ) { d->isometricScaling = isOn; layoutDiagrams(); emit propertiesChanged(); } } bool CartesianCoordinatePlane::doesIsometricScaling() const { return d->isometricScaling; } bool CartesianCoordinatePlane::doneSetZoomFactorX( qreal factor ) { if ( d->coordinateTransformation.zoom.xFactor == factor ) { return false; } d->coordinateTransformation.zoom.xFactor = factor; if ( d->autoAdjustGridToZoom ) { d->grid->setNeedRecalculate(); } return true; } bool CartesianCoordinatePlane::doneSetZoomFactorY( qreal factor ) { if ( d->coordinateTransformation.zoom.yFactor == factor ) { return false; } d->coordinateTransformation.zoom.yFactor = factor; if ( d->autoAdjustGridToZoom ) { d->grid->setNeedRecalculate(); } return true; } bool CartesianCoordinatePlane::doneSetZoomCenter( const QPointF& point ) { if ( d->coordinateTransformation.zoom.center() == point ) { return false; } d->coordinateTransformation.zoom.setCenter( point ); if ( d->autoAdjustGridToZoom ) { d->grid->setNeedRecalculate(); } return true; } void CartesianCoordinatePlane::setZoomFactors( qreal factorX, qreal factorY ) { if ( doneSetZoomFactorX( factorX ) || doneSetZoomFactorY( factorY ) ) { d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); emit propertiesChanged(); } } void CartesianCoordinatePlane::setZoomFactorX( qreal factor ) { if ( doneSetZoomFactorX( factor ) ) { d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); emit propertiesChanged(); } } void CartesianCoordinatePlane::setZoomFactorY( qreal factor ) { if ( doneSetZoomFactorY( factor ) ) { d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); emit propertiesChanged(); } } void CartesianCoordinatePlane::setZoomCenter( const QPointF& point ) { if ( doneSetZoomCenter( point ) ) { d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); emit propertiesChanged(); } } QPointF CartesianCoordinatePlane::zoomCenter() const { return d->coordinateTransformation.zoom.center(); } qreal CartesianCoordinatePlane::zoomFactorX() const { return d->coordinateTransformation.zoom.xFactor; } qreal CartesianCoordinatePlane::zoomFactorY() const { return d->coordinateTransformation.zoom.yFactor; } CartesianCoordinatePlane::AxesCalcMode CartesianCoordinatePlane::axesCalcModeY() const { return d->coordinateTransformation.axesCalcModeY; } CartesianCoordinatePlane::AxesCalcMode CartesianCoordinatePlane::axesCalcModeX() const { return d->coordinateTransformation.axesCalcModeX; } void CartesianCoordinatePlane::setAxesCalcModes( AxesCalcMode mode ) { if ( d->coordinateTransformation.axesCalcModeY != mode || d->coordinateTransformation.axesCalcModeX != mode ) { d->coordinateTransformation.axesCalcModeY = mode; d->coordinateTransformation.axesCalcModeX = mode; emit propertiesChanged(); emit viewportCoordinateSystemChanged(); Q_FOREACH( AbstractDiagram* diag, diagrams() ) slotLayoutChanged( diag ); } } void CartesianCoordinatePlane::setAxesCalcModeY( AxesCalcMode mode ) { if ( d->coordinateTransformation.axesCalcModeY != mode ) { d->coordinateTransformation.axesCalcModeY = mode; emit propertiesChanged(); setGridNeedsRecalculate(); emit viewportCoordinateSystemChanged(); } } void CartesianCoordinatePlane::setAxesCalcModeX( AxesCalcMode mode ) { if ( d->coordinateTransformation.axesCalcModeX != mode ) { d->coordinateTransformation.axesCalcModeX = mode; emit propertiesChanged(); emit viewportCoordinateSystemChanged(); } } namespace { inline bool fuzzyCompare( qreal a, qreal b ) { if ( ISNAN(a) && ISNAN(b) ) return true; if ( qFuzzyIsNull(a) && qFuzzyIsNull(b) ) return true; return qFuzzyCompare( a, b ); } } void CartesianCoordinatePlane::setHorizontalRange( const QPair< qreal, qreal > & range ) { if ( !fuzzyCompare(d->horizontalMin, range.first) || !fuzzyCompare(d->horizontalMax, range.second) ) { d->autoAdjustHorizontalRangeToData = 100; d->horizontalMin = range.first; d->horizontalMax = range.second; layoutDiagrams(); emit propertiesChanged(); emit boundariesChanged(); } } void CartesianCoordinatePlane::setVerticalRange( const QPair< qreal, qreal > & range ) { if ( !fuzzyCompare(d->verticalMin, range.first) || !fuzzyCompare(d->verticalMax, range.second) ) { d->autoAdjustVerticalRangeToData = 100; d->verticalMin = range.first; d->verticalMax = range.second; layoutDiagrams(); emit propertiesChanged(); emit boundariesChanged(); } } QPair< qreal, qreal > CartesianCoordinatePlane::horizontalRange() const { return QPair( d->horizontalMin, d->horizontalMax ); } QPair< qreal, qreal > CartesianCoordinatePlane::verticalRange() const { return QPair( d->verticalMin, d->verticalMax ); } void CartesianCoordinatePlane::adjustRangesToData() { const QRectF dataBoundingRect( getRawDataBoundingRectFromDiagrams() ); d->horizontalMin = dataBoundingRect.left(); d->horizontalMax = dataBoundingRect.right(); d->verticalMin = dataBoundingRect.top(); d->verticalMax = dataBoundingRect.bottom(); layoutDiagrams(); emit propertiesChanged(); } void CartesianCoordinatePlane::adjustHorizontalRangeToData() { const QRectF dataBoundingRect( getRawDataBoundingRectFromDiagrams() ); d->horizontalMin = dataBoundingRect.left(); d->horizontalMax = dataBoundingRect.right(); layoutDiagrams(); emit propertiesChanged(); } void CartesianCoordinatePlane::adjustVerticalRangeToData() { const QRectF dataBoundingRect( getRawDataBoundingRectFromDiagrams() ); d->verticalMin = dataBoundingRect.bottom(); d->verticalMax = dataBoundingRect.top(); layoutDiagrams(); emit propertiesChanged(); } void CartesianCoordinatePlane::setAutoAdjustHorizontalRangeToData( unsigned int percentEmpty ) { if ( d->autoAdjustHorizontalRangeToData != percentEmpty ) { d->autoAdjustHorizontalRangeToData = percentEmpty; d->horizontalMin = 0.0; d->horizontalMax = 0.0; layoutDiagrams(); emit propertiesChanged(); } } void CartesianCoordinatePlane::setAutoAdjustVerticalRangeToData( unsigned int percentEmpty ) { if ( d->autoAdjustVerticalRangeToData != percentEmpty ) { d->autoAdjustVerticalRangeToData = percentEmpty; d->verticalMin = 0.0; d->verticalMax = 0.0; layoutDiagrams(); emit propertiesChanged(); } } unsigned int CartesianCoordinatePlane::autoAdjustHorizontalRangeToData() const { return d->autoAdjustHorizontalRangeToData; } unsigned int CartesianCoordinatePlane::autoAdjustVerticalRangeToData() const { return d->autoAdjustVerticalRangeToData; } void CartesianCoordinatePlane::setGridAttributes( Qt::Orientation orientation, const GridAttributes& a ) { if ( orientation == Qt::Horizontal ) d->gridAttributesHorizontal = a; else d->gridAttributesVertical = a; setHasOwnGridAttributes( orientation, true ); update(); emit propertiesChanged(); } void CartesianCoordinatePlane::resetGridAttributes( Qt::Orientation orientation ) { setHasOwnGridAttributes( orientation, false ); update(); } const GridAttributes CartesianCoordinatePlane::gridAttributes( Qt::Orientation orientation ) const { if ( hasOwnGridAttributes( orientation ) ) { if ( orientation == Qt::Horizontal ) return d->gridAttributesHorizontal; else return d->gridAttributesVertical; } else { return globalGridAttributes(); } } void CartesianCoordinatePlane::setHasOwnGridAttributes( Qt::Orientation orientation, bool on ) { if ( orientation == Qt::Horizontal ) d->hasOwnGridAttributesHorizontal = on; else d->hasOwnGridAttributesVertical = on; emit propertiesChanged(); } bool CartesianCoordinatePlane::hasOwnGridAttributes( Qt::Orientation orientation ) const { return orientation == Qt::Horizontal ? d->hasOwnGridAttributesHorizontal : d->hasOwnGridAttributesVertical; } void CartesianCoordinatePlane::setAutoAdjustGridToZoom( bool autoAdjust ) { if ( d->autoAdjustGridToZoom != autoAdjust ) { d->autoAdjustGridToZoom = autoAdjust; d->grid->setNeedRecalculate(); emit propertiesChanged(); } } #if defined(Q_COMPILER_MANGLES_RETURN_TYPE) const #endif bool CartesianCoordinatePlane::autoAdjustGridToZoom() const { return d->autoAdjustGridToZoom; } AbstractCoordinatePlane* CartesianCoordinatePlane::sharedAxisMasterPlane( QPainter* painter ) { CartesianCoordinatePlane* plane = this; AbstractCartesianDiagram* diag = dynamic_cast< AbstractCartesianDiagram* >( plane->diagram() ); const CartesianAxis* sharedAxis = nullptr; if ( diag != nullptr ) { const CartesianAxisList axes = diag->axes(); Q_FOREACH( const CartesianAxis* a, axes ) { CartesianCoordinatePlane* p = const_cast< CartesianCoordinatePlane* >( dynamic_cast< const CartesianCoordinatePlane* >( a->coordinatePlane() ) ); if ( p != nullptr && p != this ) { plane = p; sharedAxis = a; } } } if ( plane == this || painter == nullptr ) return plane; const QPointF zero = QPointF( 0, 0 ); const QPointF tenX = QPointF( 10, 0 ); const QPointF tenY = QPointF( 0, 10 ); if ( sharedAxis->isOrdinate() ) { painter->translate( translate( zero ).x(), 0.0 ); const qreal factor = (translate( tenX ) - translate( zero ) ).x() / ( plane->translate( tenX ) - plane->translate( zero ) ).x(); painter->scale( factor, 1.0 ); painter->translate( -plane->translate( zero ).x(), 0.0 ); } if ( sharedAxis->isAbscissa() ) { painter->translate( 0.0, translate( zero ).y() ); const qreal factor = (translate( tenY ) - translate( zero ) ).y() / ( plane->translate( tenY ) - plane->translate( zero ) ).y(); painter->scale( 1.0, factor ); painter->translate( 0.0, -plane->translate( zero ).y() ); } return plane; } void CartesianCoordinatePlane::setHorizontalRangeReversed( bool reverse ) { if ( d->reverseHorizontalPlane == reverse ) return; d->reverseHorizontalPlane = reverse; layoutDiagrams(); emit propertiesChanged(); } bool CartesianCoordinatePlane::isHorizontalRangeReversed() const { return d->reverseHorizontalPlane; } void CartesianCoordinatePlane::setVerticalRangeReversed( bool reverse ) { if ( d->reverseVerticalPlane == reverse ) return; d->reverseVerticalPlane = reverse; layoutDiagrams(); emit propertiesChanged(); } bool CartesianCoordinatePlane::isVerticalRangeReversed() const { return d->reverseVerticalPlane; } QRectF CartesianCoordinatePlane::visibleDataRange() const { QRectF result; const QRectF drawArea = drawingArea(); result.setTopLeft( translateBack( drawArea.topLeft() ) ); result.setBottomRight( translateBack( drawArea.bottomRight() ) ); return result; } void CartesianCoordinatePlane::setGeometry( const QRect& rectangle ) { if ( rectangle == geometry() ) { return; } d->geometry = rectangle; if ( d->isometricScaling ) { const int hfw = heightForWidth( rectangle.width() ); // same scaling for x and y means a fixed aspect ratio, which is enforced here // always shrink the too large dimension if ( hfw < rectangle.height() ) { d->geometry.setHeight( hfw ); } else { d->geometry.setWidth( qRound( qreal( rectangle.width() ) * qreal( rectangle.height() ) / qreal( hfw ) ) ); } } AbstractCoordinatePlane::setGeometry( d->geometry ); Q_FOREACH( AbstractDiagram* diagram, diagrams() ) { diagram->resize( d->geometry.size() ); } } Qt::Orientations CartesianCoordinatePlane::expandingDirections() const { // not completely sure why this is required for isometric scaling... return d->isometricScaling ? Qt::Horizontal : ( Qt::Horizontal | Qt::Vertical ); } bool CartesianCoordinatePlane::hasHeightForWidth() const { return d->isometricScaling; } int CartesianCoordinatePlane::heightForWidth( int w ) const { // ### using anything for dataRect that depends on geometry will close a feedback loop which // prevents the geometry from stabilizing. specifically, visibleDataRange() depends on // drawingArea(), and no good will come out of using it here. QRectF dataRect = logicalArea(); return qRound( qreal( w ) * qAbs( qreal( dataRect.height() ) / qreal( dataRect.width() ) ) ); } QSize CartesianCoordinatePlane::sizeHint() const { QSize sh = AbstractCoordinatePlane::sizeHint(); if ( d->isometricScaling ) { // not sure why the next line doesn't cause an infinite loop, but it improves initial size allocation sh = d->geometry.size(); sh.setHeight( heightForWidth( sh.width() ) ); } return sh; } diff --git a/src/KChart/Cartesian/KChartCartesianGrid.cpp b/src/KChart/Cartesian/KChartCartesianGrid.cpp index 8222a0e..93b4fc5 100644 --- a/src/KChart/Cartesian/KChartCartesianGrid.cpp +++ b/src/KChart/Cartesian/KChartCartesianGrid.cpp @@ -1,501 +1,501 @@ /* * 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 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 ); + std::sort(list.begin(), list.end()); 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/Cartesian/KChartLeveyJenningsGrid.cpp b/src/KChart/Cartesian/KChartLeveyJenningsGrid.cpp index 8a9647d..685380c 100644 --- a/src/KChart/Cartesian/KChartLeveyJenningsGrid.cpp +++ b/src/KChart/Cartesian/KChartLeveyJenningsGrid.cpp @@ -1,366 +1,366 @@ /* * 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 "KChartLeveyJenningsGrid.h" #include "KChartLeveyJenningsDiagram.h" #include "KChartPaintContext.h" #include "KChartPainterSaver_p.h" #include "KChartPrintingParameters.h" #include "KChartMath_p.h" #include using namespace KChart; qreal fastPow10( int x ); DataDimensionsList LeveyJenningsGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const { Q_ASSERT_X ( rawDataDimensions.count() == 2, "CartesianGrid::calculateGrid", "Error: calculateGrid() expects a list with exactly two entries." ); LeveyJenningsCoordinatePlane* plane = dynamic_cast< LeveyJenningsCoordinatePlane*>( mPlane ); Q_ASSERT_X ( plane, "LeveyJenningsGrid::calculateGrid", "Error: PaintContext::calculatePlane() called, but no cartesian plane set." ); DataDimensionsList l( rawDataDimensions ); // 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() ) ); if ( l.first().isCalculated && plane->autoAdjustGridToZoom() && plane->axesCalcModeX() == CartesianCoordinatePlane::Linear && plane->zoomFactorX() > 1.0 ) { l.first().start = translatedBottomLeft.x(); l.first().end = translatedTopRight.x(); } const DataDimension dimX = calculateGridXY( l.first(), Qt::Horizontal, false, false ); if ( dimX.stepWidth ) { // one time for the min/max value const DataDimension minMaxY = calculateGridXY( l.last(), Qt::Vertical, false, false ); 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, false, false ); 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; //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. if ( dimY.subStepWidth == 0 ) l.last().subStepWidth = dimY.stepWidth/2; else l.last().subStepWidth = dimY.subStepWidth; } } } //qDebug() << "CartesianGrid::calculateGrid() final grid Y-range:" << l.last().end - l.last().start << " step width:" << l.last().stepWidth; //qDebug() << "CartesianGrid::calculateGrid() final grid X-range:" << l.first().end - l.first().start << " step width:" << l.first().stepWidth; return l; } #if defined ( Q_WS_WIN) #define trunc(x) ((int)(x)) #endif DataDimension LeveyJenningsGrid::calculateGridXY( const DataDimension& rawDataDimension, Qt::Orientation orientation, bool adjustLower, bool adjustUpper ) const { DataDimension dim( rawDataDimension ); if ( dim.isCalculated && dim.start != dim.end ) { // 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 ); } } 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 LeveyJenningsGrid::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 ); + std::sort(list.begin(), list.end()); 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); //FIXME(khz): make minSteps and maxSteps configurable by the user. const int minSteps = 2; const int maxSteps = 12; 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 i = 0; i < count; ++i ) testList << list.at(i) * 0.1; testList << list; do{ //qDebug() << "list:" << testList; //qDebug( "calculating steps: power: %i", power); calculateSteps( start, end, testList, minSteps, 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("C"); } } //qDebug("LeveyJenningsGrid::calculateStepWidth() found stepWidth %f (%f steps) and sub-stepWidth %f", stepWidth, steps, subStepWidth); } void LeveyJenningsGrid::drawGrid( PaintContext* context ) { // This plane is used for tranlating the coordinates - not for the data boundaries PainterSaver p( context->painter() ); LeveyJenningsCoordinatePlane* plane = qobject_cast< LeveyJenningsCoordinatePlane* >( mPlane->sharedAxisMasterPlane( context->painter() ) ); Q_ASSERT_X ( plane, "LeveyJenningsGrid::drawGrid", "Bad function call: PaintContext::coodinatePlane() NOT a Levey Jennings plane." ); LeveyJenningsDiagram* diag = qobject_cast< LeveyJenningsDiagram* >( plane->diagram() ); if ( !diag ) { return; } const LeveyJenningsGridAttributes gridAttrs( plane->gridAttributes() ); // update the calculated mDataDimensions before using them updateData( context->coordinatePlane() ); // test for programming errors: critical Q_ASSERT_X ( mDataDimensions.count() == 2, "CartesianGrid::drawGrid", "Error: updateData did not return exactly two dimensions." ); // test for invalid boundaries: non-critical if ( !isBoundariesValid( mDataDimensions ) ) { return; } //qDebug() << "B"; DataDimension dimX = mDataDimensions.first(); // this happens if there's only one data point if ( dimX.start == 0.0 && dimX.end == 0.0 ) dimX.end += plane->geometry().width(); // first we draw the expected lines // draw the "mean" line const float meanValue = diag->expectedMeanValue(); const float standardDeviation = diag->expectedStandardDeviation(); // then the calculated ones const float calcMeanValue = diag->calculatedMeanValue(); const float calcStandardDeviation = diag->calculatedStandardDeviation(); // draw the normal range QPointF topLeft = plane->translate( QPointF( dimX.start, meanValue - 2 * standardDeviation ) ); QPointF bottomRight = plane->translate( QPointF( dimX.end, meanValue + 2 * standardDeviation ) ); context->painter()->fillRect( QRectF( topLeft, QSizeF( bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y() ) ), gridAttrs.rangeBrush( LeveyJenningsGridAttributes::NormalRange ) ); // draw the critical range topLeft = plane->translate( QPointF( dimX.start, meanValue + 2 * standardDeviation ) ); bottomRight = plane->translate( QPointF( dimX.end, meanValue + 3 * standardDeviation ) ); context->painter()->fillRect( QRectF( topLeft, QSizeF( bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y() ) ), gridAttrs.rangeBrush( LeveyJenningsGridAttributes::CriticalRange ) ); topLeft = plane->translate( QPointF( dimX.start, meanValue - 2 * standardDeviation ) ); bottomRight = plane->translate( QPointF( dimX.end, meanValue - 3 * standardDeviation ) ); context->painter()->fillRect( QRectF( topLeft, QSizeF( bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y() ) ), gridAttrs.rangeBrush( LeveyJenningsGridAttributes::CriticalRange ) ); // draw the "out of range" range topLeft = plane->translate( QPointF( dimX.start, meanValue + 3 * standardDeviation ) ); bottomRight = plane->translate( QPointF( dimX.end, meanValue + 4 * standardDeviation ) ); context->painter()->fillRect( QRectF( topLeft, QSizeF( bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y() ) ), gridAttrs.rangeBrush( LeveyJenningsGridAttributes::OutOfRange ) ); topLeft = plane->translate( QPointF( dimX.start, meanValue - 3 * standardDeviation ) ); bottomRight = plane->translate( QPointF( dimX.end, meanValue - 4 * standardDeviation ) ); context->painter()->fillRect( QRectF( topLeft, QSizeF( bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y() ) ), gridAttrs.rangeBrush( LeveyJenningsGridAttributes::OutOfRange ) ); // the "expected" grid if ( gridAttrs.isGridVisible( LeveyJenningsGridAttributes::Expected ) ) { context->painter()->setPen( gridAttrs.gridPen( LeveyJenningsGridAttributes::Expected ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue ) ), plane->translate( QPointF( dimX.end, meanValue ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue + 2 * standardDeviation ) ), plane->translate( QPointF( dimX.end, meanValue + 2 * standardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue + 3 * standardDeviation ) ), plane->translate( QPointF( dimX.end, meanValue + 3 * standardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue + 4 * standardDeviation ) ), plane->translate( QPointF( dimX.end, meanValue + 4 * standardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue - 2 * standardDeviation ) ), plane->translate( QPointF( dimX.end, meanValue - 2 * standardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue - 3 * standardDeviation ) ), plane->translate( QPointF( dimX.end, meanValue - 3 * standardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, meanValue - 4 * standardDeviation ) ), plane->translate( QPointF( dimX.end, meanValue - 4 * standardDeviation ) ) ); } // the "calculated" grid if ( gridAttrs.isGridVisible( LeveyJenningsGridAttributes::Calculated ) ) { context->painter()->setPen( gridAttrs.gridPen( LeveyJenningsGridAttributes::Calculated ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, calcMeanValue ) ), plane->translate( QPointF( dimX.end, calcMeanValue ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, calcMeanValue + 2 * calcStandardDeviation ) ), plane->translate( QPointF( dimX.end, calcMeanValue + 2 * calcStandardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, calcMeanValue + 3 * calcStandardDeviation ) ), plane->translate( QPointF( dimX.end, calcMeanValue + 3 * calcStandardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, calcMeanValue - 2 * calcStandardDeviation ) ), plane->translate( QPointF( dimX.end, calcMeanValue - 2 * calcStandardDeviation ) ) ); context->painter()->drawLine( plane->translate( QPointF( dimX.start, calcMeanValue - 3 * calcStandardDeviation ) ), plane->translate( QPointF( dimX.end, calcMeanValue - 3 * calcStandardDeviation ) ) ); } } diff --git a/src/KChart/KChartAbstractDiagram.cpp b/src/KChart/KChartAbstractDiagram.cpp index 1cdc878..56993a7 100644 --- a/src/KChart/KChartAbstractDiagram.cpp +++ b/src/KChart/KChartAbstractDiagram.cpp @@ -1,1197 +1,1197 @@ /* * 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 "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 ), QVariant::fromValue( hidden ), DataHiddenRole ); emit dataHidden(); } void AbstractDiagram::setHidden( int dataset, bool hidden ) { d->setDatasetAttrs( dataset, QVariant::fromValue( hidden ), DataHiddenRole ); emit dataHidden(); } void AbstractDiagram::setHidden( bool hidden ) { d->attributesModel->setModelData( QVariant::fromValue( 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 ), QVariant::fromValue( a ), DataValueLabelAttributesRole ); emit propertiesChanged(); } void AbstractDiagram::setDataValueAttributes( int dataset, const DataValueAttributes & a ) { d->setDatasetAttrs( dataset, QVariant::fromValue( 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( QVariant::fromValue( 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() ) ) ); + painter->setPen( PrintingParameters::scalePen( QPen( brush.color().lighter() ) ) ); 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 ), QVariant::fromValue( pen ), DatasetPenRole ); emit propertiesChanged(); } void AbstractDiagram::setPen( const QPen& pen ) { attributesModel()->setModelData( QVariant::fromValue( pen ), DatasetPenRole ); emit propertiesChanged(); } void AbstractDiagram::setPen( int dataset, const QPen& pen ) { d->setDatasetAttrs( dataset, QVariant::fromValue( 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 ), QVariant::fromValue( brush ), DatasetBrushRole ); emit propertiesChanged(); } void AbstractDiagram::setBrush( const QBrush& brush ) { attributesModel()->setModelData( QVariant::fromValue( brush ), DatasetBrushRole ); emit propertiesChanged(); } void AbstractDiagram::setBrush( int dataset, const QBrush& brush ) { d->setDatasetAttrs( dataset, QVariant::fromValue( 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.cpp b/src/KChart/KChartAbstractDiagram_p.cpp index 8dc0ca3..ed4d691 100644 --- a/src/KChart/KChartAbstractDiagram_p.cpp +++ b/src/KChart/KChartAbstractDiagram_p.cpp @@ -1,678 +1,678 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // // W A R N I N G // ------------- // // This file is not part of the KD Chart API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // #include "KChartAbstractDiagram_p.h" #include "KChartBarDiagram.h" #include "KChartFrameAttributes.h" #include "KChartPainterSaver_p.h" #include #include #include using namespace KChart; LabelPaintInfo::LabelPaintInfo() : isValuePositive( false ) { } LabelPaintInfo::LabelPaintInfo( const QModelIndex& _index, const DataValueAttributes& _attrs, const QPainterPath& _labelArea, const QPointF& _markerPos, bool _isValuePositive, const QString& _value ) : index( _index ) , attrs( _attrs ) , labelArea( _labelArea ) , markerPos( _markerPos ) , isValuePositive( _isValuePositive ) , value( _value ) { } LabelPaintInfo::LabelPaintInfo( const LabelPaintInfo& other ) : index( other.index ) , attrs( other.attrs ) , labelArea( other.labelArea ) , markerPos( other.markerPos ) , isValuePositive( other.isValuePositive ) , value( other.value ) { } AbstractDiagram::Private::Private() : diagram( nullptr ) , doDumpPaintTime( false ) , plane( nullptr ) , attributesModel( new PrivateAttributesModel(nullptr,nullptr) ) , allowOverlappingDataValueTexts( false ) , antiAliasing( true ) , percent( false ) , datasetDimension( 1 ) , databoundariesDirty( true ) , mCachedFontMetrics( QFontMetrics( qApp->font() ) ) { } AbstractDiagram::Private::~Private() { if ( attributesModel && qobject_cast(attributesModel) ) delete attributesModel; } void AbstractDiagram::Private::init() { } void AbstractDiagram::Private::init( AbstractCoordinatePlane* newPlane ) { plane = newPlane; } bool AbstractDiagram::Private::usesExternalAttributesModel() const { return ( ! attributesModel.isNull() ) && ( ! qobject_cast(attributesModel) ); } void AbstractDiagram::Private::setAttributesModel( AttributesModel* amodel ) { if ( attributesModel == amodel ) { return; } if ( !attributesModel.isNull() ) { if ( qobject_cast< PrivateAttributesModel* >( attributesModel ) ) { delete attributesModel; } else { disconnect( attributesModel, SIGNAL(rowsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(columnsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(modelReset()), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(layoutChanged()), diagram, SLOT(setDataBoundariesDirty()) ); disconnect( attributesModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), diagram, SIGNAL(modelDataChanged())); } } emit diagram->attributesModelAboutToChange( amodel, attributesModel ); connect( amodel, SIGNAL(rowsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(columnsInserted(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(rowsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(columnsRemoved(QModelIndex,int,int)), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(modelReset()), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(layoutChanged()), diagram, SLOT(setDataBoundariesDirty()) ); connect( amodel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), diagram, SIGNAL(modelDataChanged())); attributesModel = amodel; } AbstractDiagram::Private::Private( const AbstractDiagram::Private& rhs ) : diagram( nullptr ), doDumpPaintTime( rhs.doDumpPaintTime ), // Do not copy the plane plane( nullptr ), attributesModelRootIndex( QModelIndex() ), attributesModel( rhs.attributesModel ), allowOverlappingDataValueTexts( rhs.allowOverlappingDataValueTexts ), antiAliasing( rhs.antiAliasing ), percent( rhs.percent ), datasetDimension( rhs.datasetDimension ), mCachedFontMetrics( rhs.cachedFontMetrics() ) { attributesModel = new PrivateAttributesModel( nullptr, nullptr); attributesModel->initFrom( rhs.attributesModel ); } // FIXME: Optimize if necessary qreal AbstractDiagram::Private::calcPercentValue( const QModelIndex & index ) const { qreal sum = 0.0; for ( int col = 0; col < attributesModel->columnCount( QModelIndex() ); col++ ) sum += attributesModel->data( attributesModel->index( index.row(), col, QModelIndex() ) ).toReal(); // checked if ( sum == 0.0 ) return 0.0; return attributesModel->data( attributesModel->mapFromSource( index ) ).toReal() / sum * 100.0; } void AbstractDiagram::Private::addLabel( LabelPaintCache* cache, const QModelIndex& index, const CartesianDiagramDataCompressor::CachePosition* position, const PositionPoints& points, const Position& autoPositionPositive, const Position& autoPositionNegative, const qreal value, qreal favoriteAngle /* = 0.0 */ ) { CartesianDiagramDataCompressor::AggregatedDataValueAttributes allAttrs( aggregatedAttrs( index, position ) ); QMap::const_iterator it; for ( it = allAttrs.constBegin(); it != allAttrs.constEnd(); ++it ) { DataValueAttributes dva = it.value(); if ( !dva.isVisible() ) { continue; } const bool isPositive = ( value >= 0.0 ); RelativePosition relPos( dva.position( isPositive ) ); relPos.setReferencePoints( points ); if ( relPos.referencePosition().isUnknown() ) { relPos.setReferencePosition( isPositive ? autoPositionPositive : autoPositionNegative ); } // Rotate the label position (not the label itself) if the diagram is rotated so that the defaults still work if ( isTransposed() ) { KChartEnums::PositionValue posValue = relPos.referencePosition().value(); if ( posValue >= KChartEnums::PositionNorthWest && posValue <= KChartEnums::PositionWest ) { // rotate 90 degrees clockwise posValue = static_cast< KChartEnums::PositionValue >( posValue + 2 ); if ( posValue > KChartEnums::PositionWest ) { // wraparound posValue = static_cast< KChartEnums::PositionValue >( posValue - ( KChartEnums::PositionWest - KChartEnums::PositionNorthWest ) ); } relPos.setReferencePosition( Position( posValue ) ); } } const QPointF referencePoint = relPos.referencePoint(); if ( !diagram->coordinatePlane()->isVisiblePoint( referencePoint ) ) { continue; } const qreal fontHeight = cachedFontMetrics( dva.textAttributes(). calculatedFont( plane, KChartEnums::MeasureOrientationMinimum ), diagram )->height(); // Note: When printing data value texts and padding's Measure is using automatic reference area // detection, the font height is used as reference size for both horizontal and vertical // padding. QSizeF relativeMeasureSize( fontHeight, fontHeight ); if ( !dva.textAttributes().hasRotation() ) { TextAttributes ta = dva.textAttributes(); ta.setRotation( favoriteAngle ); dva.setTextAttributes( ta ); } // get the size of the label text using a subset of the information going into the final layout const QString text = formatDataValueText( dva, index, value ); QTextDocument doc; doc.setDocumentMargin( 0 ); if ( Qt::mightBeRichText( text ) ) { doc.setHtml( text ); } else { doc.setPlainText( text ); } const QFont calculatedFont( dva.textAttributes() .calculatedFont( plane, KChartEnums::MeasureOrientationMinimum ) ); doc.setDefaultFont( calculatedFont ); const QRectF plainRect = doc.documentLayout()->frameBoundingRect( doc.rootFrame() ); /** * A few hints on how the positioning of the text frame is done: * * Let's assume we have a bar chart, a text for a positive value * to be drawn, and "North" as attrs.positivePosition(). * * The reference point (pos) is then set to the top center point * of a bar. The offset now depends on the alignment: * * Top: text is centered horizontally to the bar, bottom of * text frame starts at top of bar * * Bottom: text is centered horizontally to the bar, top of * text frame starts at top of bar * * Center: text is centered horizontally to the bar, center * line of text frame is same as top of bar * * TopLeft: right edge of text frame is horizontal center of * bar, bottom of text frame is top of bar. * * ... * * Positive and negative value labels are treated equally, "North" * also refers to the top of a negative bar, and *not* to the bottom. * * * "NorthEast" likewise refers to the top right edge of the bar, * "NorthWest" to the top left edge of the bar, and so on. * * In other words, attrs.positivePosition() always refers to a * position of the *bar*, and relPos.alignment() always refers * to an alignment of the text frame relative to this position. */ QTransform transform; { // move to the general area where the label should be QPointF calcPoint = relPos.calculatedPoint( relativeMeasureSize ); transform.translate( calcPoint.x(), calcPoint.y() ); // align the text rect; find out by how many half-widths / half-heights to move. int dx = -1; if ( relPos.alignment() & Qt::AlignLeft ) { dx -= 1; } else if ( relPos.alignment() & Qt::AlignRight ) { dx += 1; } int dy = -1; if ( relPos.alignment() & Qt::AlignTop ) { dy -= 1; } else if ( relPos.alignment() & Qt::AlignBottom ) { dy += 1; } transform.translate( qreal( dx ) * plainRect.width() * 0.5, qreal( dy ) * plainRect.height() * 0.5 ); // rotate the text rect around its center transform.translate( plainRect.center().x(), plainRect.center().y() ); int rotation = dva.textAttributes().rotation(); if ( !isPositive && dva.mirrorNegativeValueTextRotation() ) { rotation *= -1; } transform.rotate( rotation ); transform.translate( -plainRect.center().x(), -plainRect.center().y() ); } QPainterPath labelArea; //labelArea.addPolygon( transform.mapToPolygon( plainRect.toRect() ) ); //labelArea.closeSubpath(); // Not doing that because QTransform has a special case for 180° that gives a different than // usual ordering of the points in the polygon returned by mapToPolygon( const QRect & ). // We expect a particular ordering in paintDataValueTextsAndMarkers() by using elementAt( 0 ), // and similar things might happen elsewhere. labelArea.addPolygon( transform.map( QPolygon( plainRect.toRect(), true ) ) ); // store the label geometry and auxiliary data cache->paintReplay.append( LabelPaintInfo( it.key(), dva, labelArea, referencePoint, value >= 0.0, text ) ); } } const QFontMetrics* AbstractDiagram::Private::cachedFontMetrics( const QFont& font, const QPaintDevice* paintDevice) const { if ( ( font != mCachedFont ) || ( paintDevice != mCachedPaintDevice ) ) { mCachedFontMetrics = QFontMetrics( font, const_cast( paintDevice ) ); // TODO what about setting mCachedFont and mCachedPaintDevice? } return &mCachedFontMetrics; } const QFontMetrics AbstractDiagram::Private::cachedFontMetrics() const { return mCachedFontMetrics; } QString AbstractDiagram::Private::formatNumber( qreal value, int decimalDigits ) const { const int digits = qMax(decimalDigits, 0); const qreal roundingEpsilon = pow( 0.1, digits ) * ( value >= 0.0 ? 0.5 : -0.5 ); QString asString = QString::number( value + roundingEpsilon, 'f' ); const int decimalPos = asString.indexOf( QLatin1Char( '.' ) ); if ( decimalPos < 0 ) { return asString; } int last = qMin( decimalPos + digits, asString.length() - 1 ); // remove trailing zeros (and maybe decimal dot) while ( last > decimalPos && asString[ last ] == QLatin1Char( '0' ) ) { last--; } if ( last == decimalPos ) { last--; } asString.chop( asString.length() - last - 1 ); return asString; } void AbstractDiagram::Private::forgetAlreadyPaintedDataValues() { alreadyDrawnDataValueTexts.clear(); prevPaintedDataValueText.clear(); } void AbstractDiagram::Private::paintDataValueTextsAndMarkers( PaintContext* ctx, const LabelPaintCache &cache, bool paintMarkers, bool justCalculateRect /* = false */, QRectF* cumulatedBoundingRect /* = 0 */ ) { if ( justCalculateRect && !cumulatedBoundingRect ) { qWarning() << Q_FUNC_INFO << "Neither painting nor finding the bounding rect, what are we doing?"; } const PainterSaver painterSaver( ctx->painter() ); ctx->painter()->setClipping( false ); if ( paintMarkers && !justCalculateRect ) { Q_FOREACH ( const LabelPaintInfo& info, cache.paintReplay ) { diagram->paintMarker( ctx->painter(), info.index, info.markerPos ); } } TextAttributes ta; { Measure m( 18.0, KChartEnums::MeasureCalculationModeRelative, KChartEnums::MeasureOrientationMinimum ); m.setReferenceArea( ctx->coordinatePlane() ); ta.setFontSize( m ); m.setAbsoluteValue( 6.0 ); ta.setMinimalFontSize( m ); } forgetAlreadyPaintedDataValues(); Q_FOREACH ( const LabelPaintInfo& info, cache.paintReplay ) { const QPointF pos = info.labelArea.elementAt( 0 ); paintDataValueText( ctx->painter(), info.attrs, pos, info.isValuePositive, info.value, justCalculateRect, cumulatedBoundingRect ); const QString comment = info.index.data( KChart::CommentRole ).toString(); if ( comment.isEmpty() ) { continue; } TextBubbleLayoutItem item( comment, ta, ctx->coordinatePlane()->parent(), KChartEnums::MeasureOrientationMinimum, Qt::AlignHCenter | Qt::AlignVCenter ); const QRect rect( pos.toPoint(), item.sizeHint() ); if (cumulatedBoundingRect) { (*cumulatedBoundingRect) |= rect; } if ( !justCalculateRect ) { item.setGeometry( rect ); item.paint( ctx->painter() ); } } if ( cumulatedBoundingRect ) { *cumulatedBoundingRect = ctx->painter()->transform().inverted().mapRect( *cumulatedBoundingRect ); } } QString AbstractDiagram::Private::formatDataValueText( const DataValueAttributes &dva, const QModelIndex& index, qreal value ) const { if ( !dva.isVisible() ) { return QString(); } if ( dva.usePercentage() ) { value = calcPercentValue( index ); } QString ret; if ( dva.dataLabel().isNull() ) { ret = formatNumber( value, dva.decimalDigits() ); } else { ret = dva.dataLabel(); } ret.prepend( dva.prefix() ); ret.append( dva.suffix() ); return ret; } void AbstractDiagram::Private::paintDataValueText( QPainter* painter, const QModelIndex& index, const QPointF& pos, qreal value, bool justCalculateRect /* = false */, QRectF* cumulatedBoundingRect /* = 0 */ ) { const DataValueAttributes dva( diagram->dataValueAttributes( index ) ); const QString text = formatDataValueText( dva, index, value ); paintDataValueText( painter, dva, pos, value >= 0.0, text, justCalculateRect, cumulatedBoundingRect ); } void AbstractDiagram::Private::paintDataValueText( QPainter* painter, const DataValueAttributes& attrs, const QPointF& pos, bool valueIsPositive, const QString& text, bool justCalculateRect /* = false */, QRectF* cumulatedBoundingRect /* = 0 */ ) { if ( !attrs.isVisible() ) { return; } const TextAttributes ta( attrs.textAttributes() ); if ( !ta.isVisible() || ( !attrs.showRepetitiveDataLabels() && prevPaintedDataValueText == text ) ) { return; } prevPaintedDataValueText = text; QTextDocument doc; doc.setDocumentMargin( 0.0 ); if ( Qt::mightBeRichText( text ) ) { doc.setHtml( text ); } else { doc.setPlainText( text ); } const QFont calculatedFont( ta.calculatedFont( plane, KChartEnums::MeasureOrientationMinimum ) ); const PainterSaver painterSaver( painter ); painter->setPen( PrintingParameters::scalePen( ta.pen() ) ); doc.setDefaultFont( calculatedFont ); QAbstractTextDocumentLayout::PaintContext context; context.palette = diagram->palette(); context.palette.setColor( QPalette::Text, ta.pen().color() ); QAbstractTextDocumentLayout* const layout = doc.documentLayout(); layout->setPaintDevice( painter->device() ); painter->translate( pos.x(), pos.y() ); int rotation = ta.rotation(); if ( !valueIsPositive && attrs.mirrorNegativeValueTextRotation() ) { rotation *= -1; } painter->rotate( rotation ); // do overlap detection "as seen by the painter" QTransform transform = painter->worldTransform(); bool drawIt = true; // note: This flag can be set differently for every label text! // In theory a user could e.g. have some small red text on one of the // values that she wants to have written in any case - so we just // do not test if such texts would cover some of the others. if ( !attrs.showOverlappingDataLabels() ) { const QRectF br( layout->frameBoundingRect( doc.rootFrame() ) ); QPolygon pr = transform.mapToPolygon( br.toRect() ); // Using QPainterPath allows us to use intersects() (which has many early-exits) // instead of QPolygon::intersected (which calculates a slow and precise intersection polygon) QPainterPath path; path.addPolygon( pr ); // iterate backwards because recently added items are more likely to overlap, so we spend // less time checking irrelevant items when there is overlap for ( int i = alreadyDrawnDataValueTexts.count() - 1; i >= 0; i-- ) { if ( alreadyDrawnDataValueTexts.at( i ).intersects( path ) ) { // qDebug() << "not painting this label due to overlap"; drawIt = false; break; } } if ( drawIt ) { alreadyDrawnDataValueTexts << path; } } if ( drawIt ) { QRectF rect = layout->frameBoundingRect( doc.rootFrame() ); if ( cumulatedBoundingRect ) { (*cumulatedBoundingRect) |= transform.mapRect( rect ); } if ( !justCalculateRect ) { bool paintBack = false; BackgroundAttributes back( attrs.backgroundAttributes() ); if ( back.isVisible() ) { paintBack = true; painter->setBrush( back.brush() ); } else { painter->setBrush( QBrush() ); } qreal radius = 0.0; FrameAttributes frame( attrs.frameAttributes() ); if ( frame.isVisible() ) { paintBack = true; painter->setPen( frame.pen() ); radius = frame.cornerRadius(); } if ( paintBack ) { QRectF borderRect( QPointF( 0, 0 ), rect.size() ); painter->drawRoundedRect( borderRect, radius, radius ); } layout->draw( painter, context ); } } } QModelIndex AbstractDiagram::Private::indexAt( const QPoint& point ) const { QModelIndexList l = indexesAt( point ); - qSort( l ); + std::sort(l.begin(), l.end()); if ( !l.isEmpty() ) return l.first(); else return QModelIndex(); } QModelIndexList AbstractDiagram::Private::indexesAt( const QPoint& point ) const { return reverseMapper.indexesAt( point ); // which could be empty } QModelIndexList AbstractDiagram::Private::indexesIn( const QRect& rect ) const { return reverseMapper.indexesIn( rect ); } CartesianDiagramDataCompressor::AggregatedDataValueAttributes AbstractDiagram::Private::aggregatedAttrs( const QModelIndex& index, const CartesianDiagramDataCompressor::CachePosition* position ) const { Q_UNUSED( position ); // used by cartesian diagrams only CartesianDiagramDataCompressor::AggregatedDataValueAttributes allAttrs; allAttrs[index] = diagram->dataValueAttributes( index ); return allAttrs; } void AbstractDiagram::Private::setDatasetAttrs( int dataset, const QVariant& data, int role ) { // To store attributes for a dataset, we use the first column // that's associated with it. (i.e., with a dataset dimension // of two, the column of the keys). In most cases however, there's // only one data dimension, and thus also only one column per data set. int column = dataset * datasetDimension; // For DataHiddenRole, also store the flag in the other data points that belong to this data set, // otherwise it's impossible to hide data points in a plotter diagram because there will always // be one model index that belongs to this data point that is not hidden. // For more details on how hiding works, see the data compressor. // Also see KDCH-503 for which this is a workaround. int columnSpan = role == DataHiddenRole ? datasetDimension : 1; for ( int i = 0; i < columnSpan; i++ ) { attributesModel->setHeaderData( column + i, Qt::Horizontal, data, role ); } } QVariant AbstractDiagram::Private::datasetAttrs( int dataset, int role ) const { // See setDataSetAttrs for explanation of column int column = dataset * datasetDimension; return attributesModel->headerData( column, Qt::Horizontal, role ); } void AbstractDiagram::Private::resetDatasetAttrs( int dataset, int role ) { // See setDataSetAttrs for explanation of column int column = dataset * datasetDimension; attributesModel->resetHeaderData( column, Qt::Horizontal, role ); } bool AbstractDiagram::Private::isTransposed() const { // Determine the diagram that specifies the orientation. // That diagram is the reference diagram, if it exists, or otherwise the diagram itself. // Note: In KChart 2.3 or earlier, only a bar diagram can be transposed. const AbstractCartesianDiagram* refDiagram = qobject_cast< const AbstractCartesianDiagram * >( diagram ); if ( !refDiagram ) { return false; } if ( refDiagram->referenceDiagram() ) { refDiagram = refDiagram->referenceDiagram(); } const BarDiagram* barDiagram = qobject_cast< const BarDiagram* >( refDiagram ); if ( !barDiagram ) { return false; } return barDiagram->orientation() == Qt::Horizontal; } LineAttributesInfo::LineAttributesInfo() { } LineAttributesInfo::LineAttributesInfo( const QModelIndex& _index, const QPointF& _value, const QPointF& _nextValue ) : index( _index ) , value ( _value ) , nextValue ( _nextValue ) { } diff --git a/src/KChart/KChartChart.cpp b/src/KChart/KChartChart.cpp index d5d3f93..bbff146 100644 --- a/src/KChart/KChartChart.cpp +++ b/src/KChart/KChartChart.cpp @@ -1,1805 +1,1805 @@ /* * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. * * This file is part of the KD Chart library. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "KChartChart.h" #include "KChartChart_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "KChartCartesianCoordinatePlane.h" #include "KChartAbstractCartesianDiagram.h" #include "KChartHeaderFooter.h" #include "KChartEnums.h" #include "KChartLegend.h" #include "KChartLayoutItems.h" #include #include #include "KChartPainterSaver_p.h" #include "KChartPrintingParameters.h" #include #if defined KDAB_EVAL #include "../evaldialog/evaldialog.h" #endif #if 0 // dumpLayoutTree dumps a QLayout tree in a hopefully easy to read format to stderr - feel free to // use, improve and extend; it is very useful for looking at any layout problem. #include // this is this different from both QRect::isEmpty() and QRect::isNull() for "wrong" QRects, // i.e. those where topLeft() is actually below and / or right of bottomRight(). static bool isZeroArea(const QRect &r) { return !r.width() || !r.height(); } static QString lineProlog(int nestingDepth, int lineno) { QString numbering(QString::number(lineno).rightJustified(5).append(QChar::fromAscii(':'))); QString indent(nestingDepth * 4, QLatin1Char(' ')); return numbering + indent; } static void dumpLayoutTreeRecurse(QLayout *l, int *counter, int depth) { const QLatin1String colorOn(isZeroArea(l->geometry()) ? "\033[0m" : "\033[32m"); const QLatin1String colorOff("\033[0m"); QString prolog = lineProlog(depth, *counter); (*counter)++; qDebug() << colorOn + prolog << l->metaObject()->className() << l->geometry() << "hint" << l->sizeHint() << l->hasHeightForWidth() << "min" << l->minimumSize() << "max" << l->maximumSize() << l->expandingDirections() << l->alignment() << colorOff; for (int i = 0; i < l->count(); i++) { QLayoutItem *child = l->itemAt(i); if (QLayout *childL = child->layout()) { dumpLayoutTreeRecurse(childL, counter, depth + 1); } else { // The isZeroArea check culls usually largely useless output - you might want to remove it in // some debugging situations. Add a boolean parameter to this and dumpLayoutTree() if you do. if (!isZeroArea(child->geometry())) { prolog = lineProlog(depth + 1, *counter); (*counter)++; qDebug() << colorOn + prolog << typeid(*child).name() << child->geometry() << "hint" << child->sizeHint() << child->hasHeightForWidth() << "min" << child->minimumSize() << "max" << child->maximumSize() << child->expandingDirections() << child->alignment() << colorOff; } } } } static void dumpLayoutTree(QLayout *l) { int counter = 0; dumpLayoutTreeRecurse(l, &counter, 0); } #endif static const Qt::Alignment s_gridAlignments[ 3 ][ 3 ] = { // [ row ][ column ] { Qt::AlignTop | Qt::AlignLeft, Qt::AlignTop | Qt::AlignHCenter, Qt::AlignTop | Qt::AlignRight }, { Qt::AlignVCenter | Qt::AlignLeft, Qt::AlignVCenter | Qt::AlignHCenter, Qt::AlignVCenter | Qt::AlignRight }, { Qt::AlignBottom | Qt::AlignLeft, Qt::AlignBottom | Qt::AlignHCenter, Qt::AlignBottom | Qt::AlignRight } }; static void getRowAndColumnForPosition(KChartEnums::PositionValue pos, int* row, int* column) { switch ( pos ) { case KChartEnums::PositionNorthWest: *row = 0; *column = 0; break; case KChartEnums::PositionNorth: *row = 0; *column = 1; break; case KChartEnums::PositionNorthEast: *row = 0; *column = 2; break; case KChartEnums::PositionEast: *row = 1; *column = 2; break; case KChartEnums::PositionSouthEast: *row = 2; *column = 2; break; case KChartEnums::PositionSouth: *row = 2; *column = 1; break; case KChartEnums::PositionSouthWest: *row = 2; *column = 0; break; case KChartEnums::PositionWest: *row = 1; *column = 0; break; case KChartEnums::PositionCenter: *row = 1; *column = 1; break; default: *row = -1; *column = -1; break; } } using namespace KChart; // Layout widgets even if they are not visible (that's why isEmpty() is overridden) - at least that // was the original reason... class MyWidgetItem : public QWidgetItem { public: explicit MyWidgetItem(QWidget *w, Qt::Alignment alignment = Qt::Alignment()) : QWidgetItem( w ) { setAlignment( alignment ); } // All of the methods are reimplemented from QWidgetItem, and work around some oddity in QLayout and / or // KD Chart - I forgot the details between writing this code as an experiment and committing it, very // sorry about that! // Feel free to comment out any of them and then try the line-breaking feature in horizontal legends in // the Legends/Advanced example. It will not work well in various ways - won't get enough space and look // very broken, will inhibit resizing the window etc. QSize sizeHint() const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); return w->sizeHint(); } QSize minimumSize() const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); return w->minimumSize(); } QSize maximumSize() const Q_DECL_OVERRIDE { // Not just passing on w->maximumSize() fixes that the size policy of Legend is disregarded, making // Legend take all available space, which makes both the Legend internal layout and the overall // layout of chart + legend look bad. QWidget::maximumSize() is not a virtual method, it's a // property, so "overriding" that one would be even uglier, and prevent user-set property // values from doing anything. QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); QSize ret = w->maximumSize(); const QSize hint = w->sizeHint(); const QSizePolicy::Policy hPolicy = w->sizePolicy().horizontalPolicy(); if (hPolicy == QSizePolicy::Fixed || hPolicy == QSizePolicy::Maximum) { ret.rwidth() = hint.width(); } const QSizePolicy::Policy vPolicy = w->sizePolicy().verticalPolicy(); if (vPolicy == QSizePolicy::Fixed || vPolicy == QSizePolicy::Maximum) { ret.rheight() = hint.height(); } return ret; } Qt::Orientations expandingDirections() const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); if ( isEmpty() ) { return Qt::Orientations(); } Qt::Orientations e = w->sizePolicy().expandingDirections(); return e; } void setGeometry(const QRect &g) Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); w->setGeometry(g); } QRect geometry() const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); return w->geometry(); } bool hasHeightForWidth() const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); bool ret = !isEmpty() && qobject_cast< Legend* >( w )->hasHeightForWidth(); return ret; } int heightForWidth( int width ) const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); int ret = w->heightForWidth( width ); return ret; } bool isEmpty() const Q_DECL_OVERRIDE { QWidget* w = const_cast< MyWidgetItem * >( this )->widget(); // legend->hide() should indeed hide the legend, // but a legend in a chart that hasn't been shown yet isn't hidden // (as can happen when using Chart::paint() without showing the chart) return w->isHidden() && w->testAttribute( Qt::WA_WState_ExplicitShowHide ); } }; // When "abusing" QLayouts to lay out items with different geometry from the backing QWidgets, // some manual work is required to correctly update all the sublayouts. // This is because all the convenient ways to deal with QLayouts assume QWidgets somewhere. // What this does is somewhat similar to QLayout::activate(), but it never refers to the parent // QWidget which has the wrong geometry. static void invalidateLayoutTree( QLayoutItem *item ) { QLayout *layout = item->layout(); if ( layout ) { const int count = layout->count(); for ( int i = 0; i < count; i++ ) { invalidateLayoutTree( layout->itemAt( i ) ); } } item->invalidate(); } void Chart::Private::slotUnregisterDestroyedLegend( Legend *l ) { chart->takeLegend( l ); } void Chart::Private::slotUnregisterDestroyedHeaderFooter( HeaderFooter* hf ) { chart->takeHeaderFooter( hf ); } void Chart::Private::slotUnregisterDestroyedPlane( AbstractCoordinatePlane* plane ) { coordinatePlanes.removeAll( plane ); Q_FOREACH ( AbstractCoordinatePlane* p, coordinatePlanes ) { if ( p->referenceCoordinatePlane() == plane) { p->setReferenceCoordinatePlane( nullptr ); } } plane->layoutPlanes(); } Chart::Private::Private( Chart* chart_ ) : chart( chart_ ) , useNewLayoutSystem( false ) , layout(nullptr) , vLayout(nullptr) , planesLayout(nullptr) , headerLayout(nullptr) , footerLayout(nullptr) , dataAndLegendLayout(nullptr) , leftOuterSpacer(nullptr) , rightOuterSpacer(nullptr) , topOuterSpacer(nullptr) , bottomOuterSpacer(nullptr) , isFloatingLegendsLayoutDirty( true ) , isPlanesLayoutDirty( true ) , globalLeadingLeft(0) , globalLeadingRight(0) , globalLeadingTop(0) , globalLeadingBottom(0) { for ( int row = 0; row < 3; ++row ) { for ( int column = 0; column < 3; ++column ) { for ( int i = 0; i < 2; i++ ) { innerHdFtLayouts[ i ][ row ][ column ] = nullptr; } } } } Chart::Private::~Private() { } enum VisitorState{ Visited, Unknown }; struct ConnectedComponentsComparator{ bool operator()( const LayoutGraphNode *lhs, const LayoutGraphNode *rhs ) const { return lhs->priority < rhs->priority; } }; static QVector< LayoutGraphNode* > getPrioritySortedConnectedComponents( QVector< LayoutGraphNode* > &nodeList ) { QVector< LayoutGraphNode* >connectedComponents; QHash< LayoutGraphNode*, VisitorState > visitedComponents; Q_FOREACH ( LayoutGraphNode* node, nodeList ) visitedComponents[ node ] = Unknown; for ( int i = 0; i < nodeList.size(); ++i ) { LayoutGraphNode *curNode = nodeList[ i ]; LayoutGraphNode *representativeNode = curNode; if ( visitedComponents[ curNode ] != Visited ) { QStack< LayoutGraphNode* > stack; stack.push( curNode ); while ( !stack.isEmpty() ) { curNode = stack.pop(); Q_ASSERT( visitedComponents[ curNode ] != Visited ); visitedComponents[ curNode ] = Visited; if ( curNode->bottomSuccesor && visitedComponents[ curNode->bottomSuccesor ] != Visited ) stack.push( curNode->bottomSuccesor ); if ( curNode->leftSuccesor && visitedComponents[ curNode->leftSuccesor ] != Visited ) stack.push( curNode->leftSuccesor ); if ( curNode->sharedSuccesor && visitedComponents[ curNode->sharedSuccesor ] != Visited ) stack.push( curNode->sharedSuccesor ); if ( curNode->priority < representativeNode->priority ) representativeNode = curNode; } connectedComponents.append( representativeNode ); } } std::sort( connectedComponents.begin(), connectedComponents.end(), ConnectedComponentsComparator() ); return connectedComponents; } struct PriorityComparator{ public: PriorityComparator( QHash< AbstractCoordinatePlane*, LayoutGraphNode* > mapping ) : m_mapping( mapping ) {} bool operator() ( AbstractCoordinatePlane *lhs, AbstractCoordinatePlane *rhs ) const { const LayoutGraphNode *lhsNode = m_mapping[ lhs ]; Q_ASSERT( lhsNode ); const LayoutGraphNode *rhsNode = m_mapping[ rhs ]; Q_ASSERT( rhsNode ); return lhsNode->priority < rhsNode->priority; } const QHash< AbstractCoordinatePlane*, LayoutGraphNode* > m_mapping; }; void checkExistingAxes( LayoutGraphNode* node ) { if ( node && node->diagramPlane && node->diagramPlane->diagram() ) { AbstractCartesianDiagram *diag = qobject_cast< AbstractCartesianDiagram* >( node->diagramPlane->diagram() ); if ( diag ) { Q_FOREACH( const CartesianAxis* axis, diag->axes() ) { switch ( axis->position() ) { case( CartesianAxis::Top ): node->topAxesLayout = true; break; case( CartesianAxis::Bottom ): node->bottomAxesLayout = true; break; case( CartesianAxis::Left ): node->leftAxesLayout = true; break; case( CartesianAxis::Right ): node->rightAxesLayout = true; break; } } } } } static void mergeNodeAxisInformation( LayoutGraphNode* lhs, LayoutGraphNode* rhs ) { lhs->topAxesLayout |= rhs->topAxesLayout; rhs->topAxesLayout = lhs->topAxesLayout; lhs->bottomAxesLayout |= rhs->bottomAxesLayout; rhs->bottomAxesLayout = lhs->bottomAxesLayout; lhs->leftAxesLayout |= rhs->leftAxesLayout; rhs->leftAxesLayout = lhs->leftAxesLayout; lhs->rightAxesLayout |= rhs->rightAxesLayout; rhs->rightAxesLayout = lhs->rightAxesLayout; } static CoordinatePlaneList findSharingAxisDiagrams( AbstractCoordinatePlane* plane, const CoordinatePlaneList& list, Chart::Private::AxisType type, QVector< CartesianAxis* >* sharedAxes ) { if ( !plane || !plane->diagram() ) return CoordinatePlaneList(); Q_ASSERT( plane ); Q_ASSERT( plane->diagram() ); CoordinatePlaneList result; AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* >( plane->diagram() ); if ( !diagram ) return CoordinatePlaneList(); QList< CartesianAxis* > axes; Q_FOREACH( CartesianAxis* axis, diagram->axes() ) { if ( ( type == Chart::Private::Ordinate && ( axis->position() == CartesianAxis::Left || axis->position() == CartesianAxis::Right ) ) || ( type == Chart::Private::Abscissa && ( axis->position() == CartesianAxis::Top || axis->position() == CartesianAxis::Bottom ) ) ) { axes.append( axis ); } } Q_FOREACH( AbstractCoordinatePlane *curPlane, list ) { AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* > ( curPlane->diagram() ); if ( !diagram ) continue; Q_FOREACH( CartesianAxis* curSearchedAxis, axes ) { Q_FOREACH( CartesianAxis* curAxis, diagram->axes() ) { if ( curSearchedAxis == curAxis ) { result.append( curPlane ); if ( !sharedAxes->contains( curSearchedAxis ) ) sharedAxes->append( curSearchedAxis ); } } } } return result; } /** * this method determines the needed layout of the graph * taking care of the sharing problematic * its NOT allowed to have a diagram that shares * more than one axis in the same direction */ QVector< LayoutGraphNode* > Chart::Private::buildPlaneLayoutGraph() { QHash< AbstractCoordinatePlane*, LayoutGraphNode* > planeNodeMapping; QVector< LayoutGraphNode* > allNodes; // create all nodes and a mapping between plane and nodes Q_FOREACH( AbstractCoordinatePlane* curPlane, coordinatePlanes ) { if ( curPlane->diagram() ) { allNodes.append( new LayoutGraphNode ); allNodes[ allNodes.size() - 1 ]->diagramPlane = curPlane; allNodes[ allNodes.size() - 1 ]->priority = allNodes.size(); checkExistingAxes( allNodes[ allNodes.size() - 1 ] ); planeNodeMapping[ curPlane ] = allNodes[ allNodes.size() - 1 ]; } } // build the graph connections Q_FOREACH( LayoutGraphNode* curNode, allNodes ) { QVector< CartesianAxis* > sharedAxes; CoordinatePlaneList xSharedPlanes = findSharingAxisDiagrams( curNode->diagramPlane, coordinatePlanes, Abscissa, &sharedAxes ); Q_ASSERT( sharedAxes.size() < 2 ); // TODO duplicated code make a method out of it if ( sharedAxes.size() == 1 && xSharedPlanes.size() > 1 ) { //xSharedPlanes.removeAll( sharedAxes.first()->diagram()->coordinatePlane() ); //std::sort( xSharedPlanes.begin(), xSharedPlanes.end(), PriorityComparator( planeNodeMapping ) ); for ( int i = 0; i < xSharedPlanes.size() - 1; ++i ) { LayoutGraphNode *tmpNode = planeNodeMapping[ xSharedPlanes[ i ] ]; Q_ASSERT( tmpNode ); LayoutGraphNode *tmpNode2 = planeNodeMapping[ xSharedPlanes[ i + 1 ] ]; Q_ASSERT( tmpNode2 ); tmpNode->bottomSuccesor = tmpNode2; } // if ( sharedAxes.first()->diagram() && sharedAxes.first()->diagram()->coordinatePlane() ) // { // LayoutGraphNode *lastNode = planeNodeMapping[ xSharedPlanes.last() ]; // Q_ASSERT( lastNode ); // Q_ASSERT( sharedAxes.first()->diagram()->coordinatePlane() ); // LayoutGraphNode *ownerNode = planeNodeMapping[ sharedAxes.first()->diagram()->coordinatePlane() ]; // Q_ASSERT( ownerNode ); // lastNode->bottomSuccesor = ownerNode; // } //merge AxisInformation, needs a two pass run LayoutGraphNode axisInfoNode; for ( int count = 0; count < 2; ++count ) { for ( int i = 0; i < xSharedPlanes.size(); ++i ) { mergeNodeAxisInformation( &axisInfoNode, planeNodeMapping[ xSharedPlanes[ i ] ] ); } } } sharedAxes.clear(); CoordinatePlaneList ySharedPlanes = findSharingAxisDiagrams( curNode->diagramPlane, coordinatePlanes, Ordinate, &sharedAxes ); Q_ASSERT( sharedAxes.size() < 2 ); if ( sharedAxes.size() == 1 && ySharedPlanes.size() > 1 ) { //ySharedPlanes.removeAll( sharedAxes.first()->diagram()->coordinatePlane() ); //std::sort( ySharedPlanes.begin(), ySharedPlanes.end(), PriorityComparator( planeNodeMapping ) ); for ( int i = 0; i < ySharedPlanes.size() - 1; ++i ) { LayoutGraphNode *tmpNode = planeNodeMapping[ ySharedPlanes[ i ] ]; Q_ASSERT( tmpNode ); LayoutGraphNode *tmpNode2 = planeNodeMapping[ ySharedPlanes[ i + 1 ] ]; Q_ASSERT( tmpNode2 ); tmpNode->leftSuccesor = tmpNode2; } // if ( sharedAxes.first()->diagram() && sharedAxes.first()->diagram()->coordinatePlane() ) // { // LayoutGraphNode *lastNode = planeNodeMapping[ ySharedPlanes.last() ]; // Q_ASSERT( lastNode ); // Q_ASSERT( sharedAxes.first()->diagram()->coordinatePlane() ); // LayoutGraphNode *ownerNode = planeNodeMapping[ sharedAxes.first()->diagram()->coordinatePlane() ]; // Q_ASSERT( ownerNode ); // lastNode->bottomSuccesor = ownerNode; // } //merge AxisInformation, needs a two pass run LayoutGraphNode axisInfoNode; for ( int count = 0; count < 2; ++count ) { for ( int i = 0; i < ySharedPlanes.size(); ++i ) { mergeNodeAxisInformation( &axisInfoNode, planeNodeMapping[ ySharedPlanes[ i ] ] ); } } } sharedAxes.clear(); if ( curNode->diagramPlane->referenceCoordinatePlane() ) curNode->sharedSuccesor = planeNodeMapping[ curNode->diagramPlane->referenceCoordinatePlane() ]; } return allNodes; } QHash Chart::Private::buildPlaneLayoutInfos() { /* There are two ways in which planes can be caused to interact in * where they are put layouting wise: The first is the reference plane. If * such a reference plane is set, on a plane, it will use the same cell in the * layout as that one. In addition to this, planes can share an axis. In that case * they will be laid out in relation to each other as suggested by the position * of the axis. If, for example Plane1 and Plane2 share an axis at position Left, * that will result in the layout: Axis Plane1 Plane 2, vertically. If Plane1 * also happens to be Plane2's referece plane, both planes are drawn over each * other. The reference plane concept allows two planes to share the same space * even if neither has any axis, and in case there are shared axis, it is used * to decided, whether the planes should be painted on top of each other or * laid out vertically or horizontally next to each other. */ QHash axisInfos; QHash planeInfos; Q_FOREACH(AbstractCoordinatePlane* plane, coordinatePlanes ) { PlaneInfo p; // first check if we share space with another plane p.referencePlane = plane->referenceCoordinatePlane(); planeInfos.insert( plane, p ); Q_FOREACH( AbstractDiagram* abstractDiagram, plane->diagrams() ) { AbstractCartesianDiagram* diagram = qobject_cast ( abstractDiagram ); if ( !diagram ) { continue; } Q_FOREACH( CartesianAxis* axis, diagram->axes() ) { if ( !axisInfos.contains( axis ) ) { /* If this is the first time we see this axis, add it, with the * current plane. The first plane added to the chart that has * the axis associated with it thus "owns" it, and decides about * layout. */ AxisInfo i; i.plane = plane; axisInfos.insert( axis, i ); } else { AxisInfo i = axisInfos[axis]; if ( i.plane == plane ) { continue; // we don't want duplicates, only shared } /* The user expects diagrams to be added on top, and to the right * so that horizontally we need to move the new diagram, vertically * the reference one. */ PlaneInfo pi = planeInfos[plane]; // plane-to-plane linking overrides linking via axes if ( !pi.referencePlane ) { // we're not the first plane to see this axis, mark us as a slave pi.referencePlane = i.plane; if ( axis->position() == CartesianAxis::Left || axis->position() == CartesianAxis::Right ) { pi.horizontalOffset += 1; } planeInfos[plane] = pi; pi = planeInfos[i.plane]; if ( axis->position() == CartesianAxis::Top || axis->position() == CartesianAxis::Bottom ) { pi.verticalOffset += 1; } planeInfos[i.plane] = pi; } } } } // Create a new grid layout for each plane that has no reference. p = planeInfos[plane]; if ( p.referencePlane == nullptr ) { p.gridLayout = new QGridLayout(); - p.gridLayout->setMargin( 0 ); + p.gridLayout->setContentsMargins( 0, 0, 0, 0 ); planeInfos[plane] = p; } } return planeInfos; } void Chart::Private::slotLayoutPlanes() { /*TODO make sure this is really needed */ const QBoxLayout::Direction oldPlanesDirection = planesLayout ? planesLayout->direction() : QBoxLayout::TopToBottom; if ( planesLayout && dataAndLegendLayout ) dataAndLegendLayout->removeItem( planesLayout ); const bool hadPlanesLayout = planesLayout != nullptr; int left, top, right, bottom; if ( hadPlanesLayout ) planesLayout->getContentsMargins(&left, &top, &right, &bottom); Q_FOREACH( AbstractLayoutItem* plane, planeLayoutItems ) { plane->removeFromParentLayout(); } //TODO they should get a correct parent, but for now it works Q_FOREACH( AbstractLayoutItem* plane, planeLayoutItems ) { if ( dynamic_cast< AutoSpacerLayoutItem* >( plane ) ) delete plane; } planeLayoutItems.clear(); delete planesLayout; //hint: The direction is configurable by the user now, as // we are using a QBoxLayout rather than a QVBoxLayout. (khz, 2007/04/25) planesLayout = new QBoxLayout( oldPlanesDirection ); isPlanesLayoutDirty = true; // here we create the layouts; we need to "run" them before painting if ( useNewLayoutSystem ) { gridPlaneLayout = new QGridLayout; planesLayout->addLayout( gridPlaneLayout ); if (hadPlanesLayout) planesLayout->setContentsMargins(left, top, right, bottom); planesLayout->setObjectName( QString::fromLatin1( "planesLayout" ) ); /* First go through all planes and all axes and figure out whether the planes * need to coordinate. If they do, they share a grid layout, if not, each * get their own. See buildPlaneLayoutInfos() for more details. */ QVector< LayoutGraphNode* > vals = buildPlaneLayoutGraph(); //qDebug() << Q_FUNC_INFO << "GraphNodes" << vals.size(); QVector< LayoutGraphNode* > connectedComponents = getPrioritySortedConnectedComponents( vals ); //qDebug() << Q_FUNC_INFO << "SubGraphs" << connectedComponents.size(); int row = 0; int col = 0; QSet< CartesianAxis* > laidOutAxes; for ( int i = 0; i < connectedComponents.size(); ++i ) { LayoutGraphNode *curComponent = connectedComponents[ i ]; for ( LayoutGraphNode *curRowComponent = curComponent; curRowComponent; curRowComponent = curRowComponent->bottomSuccesor ) { col = 0; for ( LayoutGraphNode *curColComponent = curRowComponent; curColComponent; curColComponent = curColComponent->leftSuccesor ) { Q_ASSERT( curColComponent->diagramPlane->diagrams().size() == 1 ); Q_FOREACH( AbstractDiagram* diagram, curColComponent->diagramPlane->diagrams() ) { const int planeRowOffset = 1;//curColComponent->topAxesLayout ? 1 : 0; const int planeColOffset = 1;//curColComponent->leftAxesLayout ? 1 : 0; //qDebug() << Q_FUNC_INFO << row << col << planeRowOffset << planeColOffset; //qDebug() << Q_FUNC_INFO << row + planeRowOffset << col + planeColOffset; planeLayoutItems << curColComponent->diagramPlane; AbstractCartesianDiagram *cartDiag = qobject_cast< AbstractCartesianDiagram* >( diagram ); if ( cartDiag ) { gridPlaneLayout->addItem( curColComponent->diagramPlane, row + planeRowOffset, col + planeColOffset, 2, 2 ); curColComponent->diagramPlane->setParentLayout( gridPlaneLayout ); QHBoxLayout *leftLayout = nullptr; QHBoxLayout *rightLayout = nullptr; QVBoxLayout *topLayout = nullptr; QVBoxLayout *bottomLayout = nullptr; if ( curComponent->sharedSuccesor ) { gridPlaneLayout->addItem( curColComponent->sharedSuccesor->diagramPlane, row + planeRowOffset, col + planeColOffset, 2, 2 ); curColComponent->sharedSuccesor->diagramPlane->setParentLayout( gridPlaneLayout ); planeLayoutItems << curColComponent->sharedSuccesor->diagramPlane; } Q_FOREACH( CartesianAxis* axis, cartDiag->axes() ) { if ( axis->isAbscissa() ) { if ( curColComponent->bottomSuccesor ) continue; } if ( laidOutAxes.contains( axis ) ) continue; // if ( axis->diagram() != diagram ) // continue; switch ( axis->position() ) { case( CartesianAxis::Top ): if ( !topLayout ) topLayout = new QVBoxLayout; topLayout->addItem( axis ); axis->setParentLayout( topLayout ); break; case( CartesianAxis::Bottom ): if ( !bottomLayout ) bottomLayout = new QVBoxLayout; bottomLayout->addItem( axis ); axis->setParentLayout( bottomLayout ); break; case( CartesianAxis::Left ): if ( !leftLayout ) leftLayout = new QHBoxLayout; leftLayout->addItem( axis ); axis->setParentLayout( leftLayout ); break; case( CartesianAxis::Right ): if ( !rightLayout ) { rightLayout = new QHBoxLayout; } rightLayout->addItem( axis ); axis->setParentLayout( rightLayout ); break; } planeLayoutItems << axis; laidOutAxes.insert( axis ); } if ( leftLayout ) gridPlaneLayout->addLayout( leftLayout, row + planeRowOffset, col, 2, 1, Qt::AlignRight | Qt::AlignVCenter ); if ( rightLayout ) gridPlaneLayout->addLayout( rightLayout, row, col + planeColOffset + 2, 2, 1, Qt::AlignLeft | Qt::AlignVCenter ); if ( topLayout ) gridPlaneLayout->addLayout( topLayout, row, col + planeColOffset, 1, 2, Qt::AlignBottom | Qt::AlignHCenter ); if ( bottomLayout ) gridPlaneLayout->addLayout( bottomLayout, row + planeRowOffset + 2, col + planeColOffset, 1, 2, Qt::AlignTop | Qt::AlignHCenter ); } else { gridPlaneLayout->addItem( curColComponent->diagramPlane, row, col, 4, 4 ); curColComponent->diagramPlane->setParentLayout( gridPlaneLayout ); } col += planeColOffset + 2 + ( 1 ); } } int axisOffset = 2;//curRowComponent->topAxesLayout ? 1 : 0; //axisOffset += curRowComponent->bottomAxesLayout ? 1 : 0; const int rowOffset = axisOffset + 2; row += rowOffset; } // if ( planesLayout->direction() == QBoxLayout::TopToBottom ) // ++row; // else // ++col; } qDeleteAll( vals ); // re-add our grid(s) to the chart's layout if ( dataAndLegendLayout ) { dataAndLegendLayout->addLayout( planesLayout, 1, 1 ); dataAndLegendLayout->setRowStretch( 1, 1000 ); dataAndLegendLayout->setColumnStretch( 1, 1000 ); } slotResizePlanes(); #ifdef NEW_LAYOUT_DEBUG for ( int i = 0; i < gridPlaneLayout->rowCount(); ++i ) { for ( int j = 0; j < gridPlaneLayout->columnCount(); ++j ) { if ( gridPlaneLayout->itemAtPosition( i, j ) ) qDebug() << Q_FUNC_INFO << "item at" << i << j << gridPlaneLayout->itemAtPosition( i, j )->geometry(); else qDebug() << Q_FUNC_INFO << "item at" << i << j << "no item present"; } } //qDebug() << Q_FUNC_INFO << "Relayout ended"; #endif } else { if ( hadPlanesLayout ) { planesLayout->setContentsMargins( left, top, right, bottom ); } planesLayout->setContentsMargins( 0, 0, 0, 0 ); planesLayout->setSpacing( 0 ); planesLayout->setObjectName( QString::fromLatin1( "planesLayout" ) ); /* First go through all planes and all axes and figure out whether the planes * need to coordinate. If they do, they share a grid layout, if not, each * gets their own. See buildPlaneLayoutInfos() for more details. */ QHash planeInfos = buildPlaneLayoutInfos(); QHash axisInfos; Q_FOREACH( AbstractCoordinatePlane* plane, coordinatePlanes ) { Q_ASSERT( planeInfos.contains(plane) ); PlaneInfo& pi = planeInfos[ plane ]; const int column = pi.horizontalOffset; const int row = pi.verticalOffset; //qDebug() << "processing plane at column" << column << "and row" << row; QGridLayout *planeLayout = pi.gridLayout; if ( !planeLayout ) { PlaneInfo& refPi = pi; // if this plane is sharing an axis with another one, recursively check for the original plane and use // the grid of that as planeLayout. while ( !planeLayout && refPi.referencePlane ) { refPi = planeInfos[refPi.referencePlane]; planeLayout = refPi.gridLayout; } Q_ASSERT_X( planeLayout, "Chart::Private::slotLayoutPlanes()", "Invalid reference plane. Please check that the reference plane has been added to the Chart." ); } else { planesLayout->addLayout( planeLayout ); } /* Put the plane in the center of the layout. If this is our own, that's * the middle of the layout, if we are sharing, it's a cell in the center * column of the shared grid. */ planeLayoutItems << plane; plane->setParentLayout( planeLayout ); planeLayout->addItem( plane, row, column, 1, 1 ); //qDebug() << "Chart slotLayoutPlanes() calls planeLayout->addItem("<< row << column << ")"; planeLayout->setRowStretch( row, 2 ); planeLayout->setColumnStretch( column, 2 ); Q_FOREACH( AbstractDiagram* abstractDiagram, plane->diagrams() ) { AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* >( abstractDiagram ); if ( !diagram ) { continue; // FIXME what about polar ? } if ( pi.referencePlane != nullptr ) { pi.topAxesLayout = planeInfos[ pi.referencePlane ].topAxesLayout; pi.bottomAxesLayout = planeInfos[ pi.referencePlane ].bottomAxesLayout; pi.leftAxesLayout = planeInfos[ pi.referencePlane ].leftAxesLayout; pi.rightAxesLayout = planeInfos[ pi.referencePlane ].rightAxesLayout; } // collect all axes of a kind into sublayouts if ( pi.topAxesLayout == nullptr ) { pi.topAxesLayout = new QVBoxLayout; - pi.topAxesLayout->setMargin( 0 ); + pi.topAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.topAxesLayout->setObjectName( QString::fromLatin1( "topAxesLayout" ) ); } if ( pi.bottomAxesLayout == nullptr ) { pi.bottomAxesLayout = new QVBoxLayout; - pi.bottomAxesLayout->setMargin( 0 ); + pi.bottomAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.bottomAxesLayout->setObjectName( QString::fromLatin1( "bottomAxesLayout" ) ); } if ( pi.leftAxesLayout == nullptr ) { pi.leftAxesLayout = new QHBoxLayout; - pi.leftAxesLayout->setMargin( 0 ); + pi.leftAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.leftAxesLayout->setObjectName( QString::fromLatin1( "leftAxesLayout" ) ); } if ( pi.rightAxesLayout == nullptr ) { pi.rightAxesLayout = new QHBoxLayout; - pi.rightAxesLayout->setMargin( 0 ); + pi.rightAxesLayout->setContentsMargins( 0, 0, 0, 0 ); pi.rightAxesLayout->setObjectName( QString::fromLatin1( "rightAxesLayout" ) ); } if ( pi.referencePlane != nullptr ) { planeInfos[ pi.referencePlane ].topAxesLayout = pi.topAxesLayout; planeInfos[ pi.referencePlane ].bottomAxesLayout = pi.bottomAxesLayout; planeInfos[ pi.referencePlane ].leftAxesLayout = pi.leftAxesLayout; planeInfos[ pi.referencePlane ].rightAxesLayout = pi.rightAxesLayout; } //pi.leftAxesLayout->setSizeConstraint( QLayout::SetFixedSize ); Q_FOREACH( CartesianAxis* axis, diagram->axes() ) { if ( axisInfos.contains( axis ) ) { continue; // already laid out this one } Q_ASSERT ( axis ); axis->setCachedSizeDirty(); //qDebug() << "--------------- axis added to planeLayoutItems -----------------"; planeLayoutItems << axis; switch ( axis->position() ) { case CartesianAxis::Top: axis->setParentLayout( pi.topAxesLayout ); pi.topAxesLayout->addItem( axis ); break; case CartesianAxis::Bottom: axis->setParentLayout( pi.bottomAxesLayout ); pi.bottomAxesLayout->addItem( axis ); break; case CartesianAxis::Left: axis->setParentLayout( pi.leftAxesLayout ); pi.leftAxesLayout->addItem( axis ); break; case CartesianAxis::Right: axis->setParentLayout( pi.rightAxesLayout ); pi.rightAxesLayout->addItem( axis ); break; default: Q_ASSERT_X( false, "Chart::paintEvent", "unknown axis position" ); break; }; axisInfos.insert( axis, AxisInfo() ); } /* Put each stack of axes-layouts in the cells surrounding the * associated plane. We are laying out in the oder the planes * were added, and the first one gets to lay out shared axes. * Private axes go here as well, of course. */ if ( !pi.topAxesLayout->parent() ) { planeLayout->addLayout( pi.topAxesLayout, row - 1, column ); } if ( !pi.bottomAxesLayout->parent() ) { planeLayout->addLayout( pi.bottomAxesLayout, row + 1, column ); } if ( !pi.leftAxesLayout->parent() ) { planeLayout->addLayout( pi.leftAxesLayout, row, column - 1 ); } if ( !pi.rightAxesLayout->parent() ) { planeLayout->addLayout( pi.rightAxesLayout,row, column + 1 ); } } // use up to four auto-spacer items in the corners around the diagrams: #define ADD_AUTO_SPACER_IF_NEEDED( \ spacerRow, spacerColumn, hLayoutIsAtTop, hLayout, vLayoutIsAtLeft, vLayout ) \ { \ if ( hLayout || vLayout ) { \ AutoSpacerLayoutItem * spacer \ = new AutoSpacerLayoutItem( hLayoutIsAtTop, hLayout, vLayoutIsAtLeft, vLayout ); \ planeLayout->addItem( spacer, spacerRow, spacerColumn, 1, 1 ); \ spacer->setParentLayout( planeLayout ); \ planeLayoutItems << spacer; \ } \ } if ( plane->isCornerSpacersEnabled() ) { ADD_AUTO_SPACER_IF_NEEDED( row - 1, column - 1, false, pi.leftAxesLayout, false, pi.topAxesLayout ) ADD_AUTO_SPACER_IF_NEEDED( row + 1, column - 1, true, pi.leftAxesLayout, false, pi.bottomAxesLayout ) ADD_AUTO_SPACER_IF_NEEDED( row - 1, column + 1, false, pi.rightAxesLayout, true, pi.topAxesLayout ) ADD_AUTO_SPACER_IF_NEEDED( row + 1, column + 1, true, pi.rightAxesLayout, true, pi.bottomAxesLayout ) } } // re-add our grid(s) to the chart's layout if ( dataAndLegendLayout ) { dataAndLegendLayout->addLayout( planesLayout, 1, 1 ); dataAndLegendLayout->setRowStretch( 1, 1000 ); dataAndLegendLayout->setColumnStretch( 1, 1000 ); } slotResizePlanes(); } } void Chart::Private::createLayouts() { // The toplevel layout provides the left and right global margins layout = new QHBoxLayout( chart ); layout->setContentsMargins( 0, 0, 0, 0 ); layout->setObjectName( QString::fromLatin1( "Chart::Private::layout" ) ); layout->addSpacing( globalLeadingLeft ); leftOuterSpacer = layout->itemAt( layout->count() - 1 )->spacerItem(); // The vLayout provides top and bottom global margins and lays // out headers, footers and the diagram area. vLayout = new QVBoxLayout(); vLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->setObjectName( QString::fromLatin1( "vLayout" ) ); layout->addLayout( vLayout, 1000 ); layout->addSpacing( globalLeadingRight ); rightOuterSpacer = layout->itemAt( layout->count() - 1 )->spacerItem(); // 1. the spacing above the header area vLayout->addSpacing( globalLeadingTop ); topOuterSpacer = vLayout->itemAt( vLayout->count() - 1 )->spacerItem(); // 2. the header area headerLayout = new QGridLayout(); headerLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->addLayout( headerLayout ); // 3. the area containing coordinate planes, axes, and legends dataAndLegendLayout = new QGridLayout(); dataAndLegendLayout->setContentsMargins( 0, 0, 0, 0 ); dataAndLegendLayout->setObjectName( QString::fromLatin1( "dataAndLegendLayout" ) ); vLayout->addLayout( dataAndLegendLayout, 1000 ); // 4. the footer area footerLayout = new QGridLayout(); footerLayout->setContentsMargins( 0, 0, 0, 0 ); footerLayout->setObjectName( QString::fromLatin1( "footerLayout" ) ); vLayout->addLayout( footerLayout ); // 5. Prepare the header / footer layout cells: // Each of the 9 header cells (the 9 footer cells) // contain their own QVBoxLayout // since there can be more than one header (footer) per cell. for ( int row = 0; row < 3; ++row ) { for ( int column = 0; column < 3; ++ column ) { const Qt::Alignment align = s_gridAlignments[ row ][ column ]; for ( int headOrFoot = 0; headOrFoot < 2; headOrFoot++ ) { QVBoxLayout* innerLayout = new QVBoxLayout(); innerLayout->setContentsMargins( 0, 0, 0, 0 ); innerLayout->setAlignment( align ); innerHdFtLayouts[ headOrFoot ][ row ][ column ] = innerLayout; QGridLayout* outerLayout = headOrFoot == 0 ? headerLayout : footerLayout; outerLayout->addLayout( innerLayout, row, column, align ); } } } // 6. the spacing below the footer area vLayout->addSpacing( globalLeadingBottom ); bottomOuterSpacer = vLayout->itemAt( vLayout->count() - 1 )->spacerItem(); // the data+axes area dataAndLegendLayout->addLayout( planesLayout, 1, 1 ); dataAndLegendLayout->setRowStretch( 1, 1 ); dataAndLegendLayout->setColumnStretch( 1, 1 ); } void Chart::Private::slotResizePlanes() { if ( !dataAndLegendLayout ) { return; } if ( !overrideSize.isValid() ) { // activate() takes the size from the layout's parent QWidget, which is not updated when overrideSize // is set. So don't let the layout grab the wrong size in that case. // When overrideSize *is* set, we call layout->setGeometry() in paint( QPainter*, const QRect& ), // which also "activates" the layout in the sense that it distributes space internally. layout->activate(); } // Adapt diagram drawing to the new size Q_FOREACH (AbstractCoordinatePlane* plane, coordinatePlanes ) { plane->layoutDiagrams(); } } void Chart::Private::updateDirtyLayouts() { if ( isPlanesLayoutDirty ) { Q_FOREACH ( AbstractCoordinatePlane* p, coordinatePlanes ) { p->setGridNeedsRecalculate(); p->layoutPlanes(); p->layoutDiagrams(); } } if ( isPlanesLayoutDirty || isFloatingLegendsLayoutDirty ) { chart->reLayoutFloatingLegends(); } isPlanesLayoutDirty = false; isFloatingLegendsLayoutDirty = false; } void Chart::Private::reapplyInternalLayouts() { QRect geo = layout->geometry(); invalidateLayoutTree( layout ); layout->setGeometry( geo ); slotResizePlanes(); } void Chart::Private::paintAll( QPainter* painter ) { updateDirtyLayouts(); QRect rect( QPoint( 0, 0 ), overrideSize.isValid() ? overrideSize : chart->size() ); //qDebug() << this<<"::paintAll() uses layout size" << currentLayoutSize; // Paint the background (if any) AbstractAreaBase::paintBackgroundAttributes( *painter, rect, backgroundAttributes ); // Paint the frame (if any) AbstractAreaBase::paintFrameAttributes( *painter, rect, frameAttributes ); chart->reLayoutFloatingLegends(); Q_FOREACH( AbstractLayoutItem* planeLayoutItem, planeLayoutItems ) { planeLayoutItem->paintAll( *painter ); } Q_FOREACH( TextArea* textLayoutItem, textLayoutItems ) { textLayoutItem->paintAll( *painter ); } Q_FOREACH( Legend *legend, legends ) { const bool hidden = legend->isHidden() && legend->testAttribute( Qt::WA_WState_ExplicitShowHide ); if ( !hidden ) { //qDebug() << "painting legend at " << legend->geometry(); legend->paintIntoRect( *painter, legend->geometry() ); } } } // ******** Chart interface implementation *********** #define d d_func() Chart::Chart ( QWidget* parent ) : QWidget ( parent ) , _d( new Private( this ) ) { #if defined KDAB_EVAL EvalDialog::checkEvalLicense( "KD Chart" ); #endif FrameAttributes frameAttrs; // no frame per default... // frameAttrs.setVisible( true ); frameAttrs.setPen( QPen( Qt::black ) ); frameAttrs.setPadding( 1 ); setFrameAttributes( frameAttrs ); addCoordinatePlane( new CartesianCoordinatePlane ( this ) ); d->createLayouts(); } Chart::~Chart() { delete d; } void Chart::setFrameAttributes( const FrameAttributes &a ) { d->frameAttributes = a; } FrameAttributes Chart::frameAttributes() const { return d->frameAttributes; } void Chart::setBackgroundAttributes( const BackgroundAttributes &a ) { d->backgroundAttributes = a; } BackgroundAttributes Chart::backgroundAttributes() const { return d->backgroundAttributes; } //TODO KChart 3.0; change QLayout into QBoxLayout::Direction void Chart::setCoordinatePlaneLayout( QLayout * layout ) { if (layout == d->planesLayout) return; if (d->planesLayout) { // detach all QLayoutItem's the previous planesLayout has cause // otherwise deleting the planesLayout would delete them too. for(int i = d->planesLayout->count() - 1; i >= 0; --i) { d->planesLayout->takeAt(i); } delete d->planesLayout; } d->planesLayout = qobject_cast( layout ); d->slotLayoutPlanes(); } QLayout* Chart::coordinatePlaneLayout() { return d->planesLayout; } AbstractCoordinatePlane* Chart::coordinatePlane() { if ( d->coordinatePlanes.isEmpty() ) { qWarning() << "Chart::coordinatePlane: warning: no coordinate plane defined."; return nullptr; } else { return d->coordinatePlanes.first(); } } CoordinatePlaneList Chart::coordinatePlanes() { return d->coordinatePlanes; } void Chart::addCoordinatePlane( AbstractCoordinatePlane* plane ) { // Append insertCoordinatePlane( d->coordinatePlanes.count(), plane ); } void Chart::insertCoordinatePlane( int index, AbstractCoordinatePlane* plane ) { if ( index < 0 || index > d->coordinatePlanes.count() ) { return; } connect( plane, SIGNAL(destroyedCoordinatePlane(AbstractCoordinatePlane*)), d, SLOT(slotUnregisterDestroyedPlane(AbstractCoordinatePlane*)) ); connect( plane, SIGNAL(needUpdate()), this, SLOT(update()) ); connect( plane, SIGNAL(needRelayout()), d, SLOT(slotResizePlanes()) ) ; connect( plane, SIGNAL(needLayoutPlanes()), d, SLOT(slotLayoutPlanes()) ) ; connect( plane, SIGNAL(propertiesChanged()),this, SIGNAL(propertiesChanged()) ); d->coordinatePlanes.insert( index, plane ); plane->setParent( this ); d->slotLayoutPlanes(); } void Chart::replaceCoordinatePlane( AbstractCoordinatePlane* plane, AbstractCoordinatePlane* oldPlane_ ) { if ( plane && oldPlane_ != plane ) { AbstractCoordinatePlane* oldPlane = oldPlane_; if ( d->coordinatePlanes.count() ) { if ( ! oldPlane ) { oldPlane = d->coordinatePlanes.first(); if ( oldPlane == plane ) return; } takeCoordinatePlane( oldPlane ); } delete oldPlane; addCoordinatePlane( plane ); } } void Chart::takeCoordinatePlane( AbstractCoordinatePlane* plane ) { const int idx = d->coordinatePlanes.indexOf( plane ); if ( idx != -1 ) { d->coordinatePlanes.takeAt( idx ); disconnect( plane, nullptr, d, nullptr ); disconnect( plane, nullptr, this, nullptr ); plane->removeFromParentLayout(); plane->setParent( nullptr ); d->mouseClickedPlanes.removeAll(plane); } d->slotLayoutPlanes(); // Need to emit the signal: In case somebody has connected the signal // to her own slot for e.g. calling update() on a widget containing the chart. emit propertiesChanged(); } void Chart::setGlobalLeading( int left, int top, int right, int bottom ) { setGlobalLeadingLeft( left ); setGlobalLeadingTop( top ); setGlobalLeadingRight( right ); setGlobalLeadingBottom( bottom ); } void Chart::setGlobalLeadingLeft( int leading ) { d->globalLeadingLeft = leading; d->leftOuterSpacer->changeSize( leading, 0, QSizePolicy::Fixed, QSizePolicy::Minimum ); d->reapplyInternalLayouts(); } int Chart::globalLeadingLeft() const { return d->globalLeadingLeft; } void Chart::setGlobalLeadingTop( int leading ) { d->globalLeadingTop = leading; d->topOuterSpacer->changeSize( 0, leading, QSizePolicy::Minimum, QSizePolicy::Fixed ); d->reapplyInternalLayouts(); } int Chart::globalLeadingTop() const { return d->globalLeadingTop; } void Chart::setGlobalLeadingRight( int leading ) { d->globalLeadingRight = leading; d->rightOuterSpacer->changeSize( leading, 0, QSizePolicy::Fixed, QSizePolicy::Minimum ); d->reapplyInternalLayouts(); } int Chart::globalLeadingRight() const { return d->globalLeadingRight; } void Chart::setGlobalLeadingBottom( int leading ) { d->globalLeadingBottom = leading; d->bottomOuterSpacer->changeSize( 0, leading, QSizePolicy::Minimum, QSizePolicy::Fixed ); d->reapplyInternalLayouts(); } int Chart::globalLeadingBottom() const { return d->globalLeadingBottom; } void Chart::paint( QPainter* painter, const QRect& target ) { if ( target.isEmpty() || !painter ) { return; } QPaintDevice* prevDevice = GlobalMeasureScaling::paintDevice(); GlobalMeasureScaling::setPaintDevice( painter->device() ); // Output on a widget if ( dynamic_cast< QWidget* >( painter->device() ) != nullptr ) { GlobalMeasureScaling::setFactors( qreal( target.width() ) / qreal( geometry().size().width() ), qreal( target.height() ) / qreal( geometry().size().height() ) ); } else { // Output onto a QPixmap PrintingParameters::setScaleFactor( qreal( painter->device()->logicalDpiX() ) / qreal( logicalDpiX() ) ); const qreal resX = qreal( logicalDpiX() ) / qreal( painter->device()->logicalDpiX() ); const qreal resY = qreal( logicalDpiY() ) / qreal( painter->device()->logicalDpiY() ); GlobalMeasureScaling::setFactors( qreal( target.width() ) / qreal( geometry().size().width() ) * resX, qreal( target.height() ) / qreal( geometry().size().height() ) * resY ); } const QPoint translation = target.topLeft(); painter->translate( translation ); // the following layout logic has the disadvantage that repeatedly calling this method can // cause a relayout every time, but since this method's main use seems to be printing, the // gratuitous relayouts shouldn't be much of a performance problem. const bool differentSize = target.size() != size(); QRect oldGeometry; if ( differentSize ) { oldGeometry = geometry(); d->isPlanesLayoutDirty = true; d->isFloatingLegendsLayoutDirty = true; invalidateLayoutTree( d->dataAndLegendLayout ); d->dataAndLegendLayout->setGeometry( QRect( QPoint(), target.size() ) ); } d->overrideSize = target.size(); d->paintAll( painter ); d->overrideSize = QSize(); if ( differentSize ) { invalidateLayoutTree( d->dataAndLegendLayout ); d->dataAndLegendLayout->setGeometry( oldGeometry ); d->isPlanesLayoutDirty = true; d->isFloatingLegendsLayoutDirty = true; } // for debugging // painter->setPen( QPen( Qt::blue, 8 ) ); // painter->drawRect( target ); painter->translate( -translation.x(), -translation.y() ); GlobalMeasureScaling::instance()->resetFactors(); PrintingParameters::resetScaleFactor(); GlobalMeasureScaling::setPaintDevice( prevDevice ); } void Chart::resizeEvent ( QResizeEvent* event ) { d->isPlanesLayoutDirty = true; d->isFloatingLegendsLayoutDirty = true; QWidget::resizeEvent( event ); } void Chart::reLayoutFloatingLegends() { Q_FOREACH( Legend *legend, d->legends ) { const bool hidden = legend->isHidden() && legend->testAttribute( Qt::WA_WState_ExplicitShowHide ); if ( legend->position().isFloating() && !hidden ) { // resize the legend const QSize legendSize( legend->sizeHint() ); legend->setGeometry( QRect( legend->geometry().topLeft(), legendSize ) ); // find the legends corner point (reference point plus any paddings) const RelativePosition relPos( legend->floatingPosition() ); QPointF pt( relPos.calculatedPoint( size() ) ); //qDebug() << pt; // calculate the legend's top left point const Qt::Alignment alignTopLeft = Qt::AlignBottom | Qt::AlignLeft; if ( (relPos.alignment() & alignTopLeft) != alignTopLeft ) { if ( relPos.alignment() & Qt::AlignRight ) pt.rx() -= legendSize.width(); else if ( relPos.alignment() & Qt::AlignHCenter ) pt.rx() -= 0.5 * legendSize.width(); if ( relPos.alignment() & Qt::AlignBottom ) pt.ry() -= legendSize.height(); else if ( relPos.alignment() & Qt::AlignVCenter ) pt.ry() -= 0.5 * legendSize.height(); } //qDebug() << pt << endl; legend->move( static_cast(pt.x()), static_cast(pt.y()) ); } } } void Chart::paintEvent( QPaintEvent* ) { QPainter painter( this ); d->paintAll( &painter ); emit finishedDrawing(); } void Chart::addHeaderFooter( HeaderFooter* hf ) { Q_ASSERT( hf->type() == HeaderFooter::Header || hf->type() == HeaderFooter::Footer ); int row; int column; getRowAndColumnForPosition( hf->position().value(), &row, &column ); if ( row == -1 ) { qWarning( "Unknown header/footer position" ); return; } d->headerFooters.append( hf ); d->textLayoutItems.append( hf ); connect( hf, SIGNAL(destroyedHeaderFooter(HeaderFooter*)), d, SLOT(slotUnregisterDestroyedHeaderFooter(HeaderFooter*)) ); connect( hf, SIGNAL(positionChanged(HeaderFooter*)), d, SLOT(slotHeaderFooterPositionChanged(HeaderFooter*)) ); // set the text attributes (why?) TextAttributes textAttrs( hf->textAttributes() ); Measure measure( textAttrs.fontSize() ); measure.setRelativeMode( this, KChartEnums::MeasureOrientationMinimum ); measure.setValue( 20 ); textAttrs.setFontSize( measure ); hf->setTextAttributes( textAttrs ); // add it to the appropriate layout int innerLayoutIdx = hf->type() == HeaderFooter::Header ? 0 : 1; QVBoxLayout* headerFooterLayout = d->innerHdFtLayouts[ innerLayoutIdx ][ row ][ column ]; hf->setParentLayout( headerFooterLayout ); hf->setAlignment( s_gridAlignments[ row ][ column ] ); headerFooterLayout->addItem( hf ); d->slotResizePlanes(); } void Chart::replaceHeaderFooter( HeaderFooter* headerFooter, HeaderFooter* oldHeaderFooter_ ) { if ( headerFooter && oldHeaderFooter_ != headerFooter ) { HeaderFooter* oldHeaderFooter = oldHeaderFooter_; if ( d->headerFooters.count() ) { if ( ! oldHeaderFooter ) { oldHeaderFooter = d->headerFooters.first(); if ( oldHeaderFooter == headerFooter ) return; } takeHeaderFooter( oldHeaderFooter ); } delete oldHeaderFooter; addHeaderFooter( headerFooter ); } } void Chart::takeHeaderFooter( HeaderFooter* headerFooter ) { const int idx = d->headerFooters.indexOf( headerFooter ); if ( idx == -1 ) { return; } disconnect( headerFooter, SIGNAL(destroyedHeaderFooter(HeaderFooter*)), d, SLOT(slotUnregisterDestroyedHeaderFooter(HeaderFooter*)) ); d->headerFooters.takeAt( idx ); headerFooter->removeFromParentLayout(); headerFooter->setParentLayout( nullptr ); d->textLayoutItems.remove( d->textLayoutItems.indexOf( headerFooter ) ); d->slotResizePlanes(); } void Chart::Private::slotHeaderFooterPositionChanged( HeaderFooter* hf ) { chart->takeHeaderFooter( hf ); chart->addHeaderFooter( hf ); } HeaderFooter* Chart::headerFooter() { if ( d->headerFooters.isEmpty() ) { return nullptr; } else { return d->headerFooters.first(); } } HeaderFooterList Chart::headerFooters() { return d->headerFooters; } void Chart::Private::slotLegendPositionChanged( AbstractAreaWidget* aw ) { Legend* legend = qobject_cast< Legend* >( aw ); Q_ASSERT( legend ); chart->takeLegend( legend ); chart->addLegendInternal( legend, false ); } void Chart::addLegend( Legend* legend ) { legend->show(); addLegendInternal( legend, true ); emit propertiesChanged(); } void Chart::addLegendInternal( Legend* legend, bool setMeasures ) { if ( !legend ) { return; } KChartEnums::PositionValue pos = legend->position().value(); if ( pos == KChartEnums::PositionCenter ) { qWarning( "Not showing legend because PositionCenter is not supported for legends." ); } int row; int column; getRowAndColumnForPosition( pos, &row, &column ); if ( row < 0 && pos != KChartEnums::PositionFloating ) { qWarning( "Not showing legend because of unknown legend position." ); return; } d->legends.append( legend ); legend->setParent( this ); // set text attributes (why?) if ( setMeasures ) { TextAttributes textAttrs( legend->textAttributes() ); Measure measure( textAttrs.fontSize() ); measure.setRelativeMode( this, KChartEnums::MeasureOrientationMinimum ); measure.setValue( 20 ); textAttrs.setFontSize( measure ); legend->setTextAttributes( textAttrs ); textAttrs = legend->titleTextAttributes(); measure.setRelativeMode( this, KChartEnums::MeasureOrientationMinimum ); measure.setValue( 24 ); textAttrs.setFontSize( measure ); legend->setTitleTextAttributes( textAttrs ); legend->setReferenceArea( this ); } // add it to the appropriate layout if ( pos != KChartEnums::PositionFloating ) { legend->needSizeHint(); // in each edge and corner of the outer layout, there's a grid for the different alignments that we create // on demand. we don't remove it when empty. QLayoutItem* edgeItem = d->dataAndLegendLayout->itemAtPosition( row, column ); QGridLayout* alignmentsLayout = dynamic_cast< QGridLayout* >( edgeItem ); Q_ASSERT( !edgeItem || alignmentsLayout ); // if it exists, it must be a QGridLayout if ( !alignmentsLayout ) { alignmentsLayout = new QGridLayout; d->dataAndLegendLayout->addLayout( alignmentsLayout, row, column ); alignmentsLayout->setContentsMargins( 0, 0, 0, 0 ); } // in case there are several legends in the same edge or corner with the same alignment, they are stacked // vertically using a QVBoxLayout. it is created on demand as above. row = 1; column = 1; for ( int i = 0; i < 3; i++ ) { for ( int j = 0; j < 3; j++ ) { Qt::Alignment align = s_gridAlignments[ i ][ j ]; if ( align == legend->alignment() ) { row = i; column = j; break; } } } QLayoutItem* alignmentItem = alignmentsLayout->itemAtPosition( row, column ); QVBoxLayout* sameAlignmentLayout = dynamic_cast< QVBoxLayout* >( alignmentItem ); Q_ASSERT( !alignmentItem || sameAlignmentLayout ); // if it exists, it must be a QVBoxLayout if ( !sameAlignmentLayout ) { sameAlignmentLayout = new QVBoxLayout; alignmentsLayout->addLayout( sameAlignmentLayout, row, column ); sameAlignmentLayout->setContentsMargins( 0, 0, 0, 0 ); } sameAlignmentLayout->addItem( new MyWidgetItem( legend, legend->alignment() ) ); } connect( legend, SIGNAL(destroyedLegend(Legend*)), d, SLOT(slotUnregisterDestroyedLegend(Legend*)) ); connect( legend, SIGNAL(positionChanged(AbstractAreaWidget*)), d, SLOT(slotLegendPositionChanged(AbstractAreaWidget*)) ); connect( legend, SIGNAL(propertiesChanged()), this, SIGNAL(propertiesChanged()) ); d->slotResizePlanes(); } void Chart::replaceLegend( Legend* legend, Legend* oldLegend_ ) { if ( legend && oldLegend_ != legend ) { Legend* oldLegend = oldLegend_; if ( d->legends.count() ) { if ( ! oldLegend ) { oldLegend = d->legends.first(); if ( oldLegend == legend ) return; } takeLegend( oldLegend ); } delete oldLegend; addLegend( legend ); } } void Chart::takeLegend( Legend* legend ) { const int idx = d->legends.indexOf( legend ); if ( idx == -1 ) { return; } d->legends.takeAt( idx ); disconnect( legend, nullptr, d, nullptr ); disconnect( legend, nullptr, this, nullptr ); // the following removes the legend from its layout and destroys its MyWidgetItem (the link to the layout) legend->setParent( nullptr ); d->slotResizePlanes(); emit propertiesChanged(); } Legend* Chart::legend() { return d->legends.isEmpty() ? nullptr : d->legends.first(); } LegendList Chart::legends() { return d->legends; } void Chart::mousePressEvent( QMouseEvent* event ) { const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { QMouseEvent ev( QEvent::MouseButtonPress, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mousePressEvent( &ev ); d->mouseClickedPlanes.append( plane ); } } } void Chart::mouseDoubleClickEvent( QMouseEvent* event ) { const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { QMouseEvent ev( QEvent::MouseButtonPress, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mouseDoubleClickEvent( &ev ); } } } void Chart::mouseMoveEvent( QMouseEvent* event ) { QSet< AbstractCoordinatePlane* > eventReceivers = QSet< AbstractCoordinatePlane* >::fromList( d->mouseClickedPlanes ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { eventReceivers.insert( plane ); } } const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, eventReceivers ) { QMouseEvent ev( QEvent::MouseMove, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mouseMoveEvent( &ev ); } } void Chart::mouseReleaseEvent( QMouseEvent* event ) { QSet< AbstractCoordinatePlane* > eventReceivers = QSet< AbstractCoordinatePlane* >::fromList( d->mouseClickedPlanes ); Q_FOREACH( AbstractCoordinatePlane* plane, d->coordinatePlanes ) { if ( plane->geometry().contains( event->pos() ) && plane->diagrams().size() > 0 ) { eventReceivers.insert( plane ); } } const QPoint pos = mapFromGlobal( event->globalPos() ); Q_FOREACH( AbstractCoordinatePlane* plane, eventReceivers ) { QMouseEvent ev( QEvent::MouseButtonRelease, pos, event->globalPos(), event->button(), event->buttons(), event->modifiers() ); plane->mouseReleaseEvent( &ev ); } d->mouseClickedPlanes.clear(); } bool Chart::event( QEvent* event ) { if ( event->type() == QEvent::ToolTip ) { const QHelpEvent* const helpEvent = static_cast< QHelpEvent* >( event ); Q_FOREACH( const AbstractCoordinatePlane* const plane, d->coordinatePlanes ) { // iterate diagrams in reverse, so that the top-most painted diagram is // queried first for a tooltip before the diagrams behind it const ConstAbstractDiagramList& diagrams = plane->diagrams(); for (int i = diagrams.size() - 1; i >= 0; --i) { const AbstractDiagram* diagram = diagrams[i]; if (diagram->isHidden()) { continue; } const QModelIndex index = diagram->indexAt( helpEvent->pos() ); const QVariant toolTip = index.data( Qt::ToolTipRole ); if ( toolTip.isValid() ) { QPoint pos = mapFromGlobal( helpEvent->pos() ); QRect rect( pos - QPoint( 1, 1 ), QSize( 3, 3 ) ); QToolTip::showText( QCursor::pos(), toolTip.toString(), this, rect ); return true; } } } } return QWidget::event( event ); } bool Chart::useNewLayoutSystem() const { return d_func()->useNewLayoutSystem; } void Chart::setUseNewLayoutSystem( bool value ) { if ( d_func()->useNewLayoutSystem != value ) d_func()->useNewLayoutSystem = value; } diff --git a/src/KChart/KChartDatasetProxyModel.cpp b/src/KChart/KChartDatasetProxyModel.cpp index fe8a11e..92f598d 100644 --- a/src/KChart/KChartDatasetProxyModel.cpp +++ b/src/KChart/KChartDatasetProxyModel.cpp @@ -1,294 +1,294 @@ /* * 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 "KChartDatasetProxyModel.h" #include "KChartMath_p.h" #include using namespace KChart; DatasetProxyModel::DatasetProxyModel(QObject* parent) : QSortFilterProxyModel( parent ) { } QModelIndex DatasetProxyModel::buddy( const QModelIndex& index ) const { return index; } Qt::ItemFlags DatasetProxyModel::flags( const QModelIndex& index ) const { return sourceModel()->flags( mapToSource( index ) ); } void DatasetProxyModel::setDatasetRowDescriptionVector( const DatasetDescriptionVector& configuration ) { Q_ASSERT_X( sourceModel(), "DatasetProxyModel::setDatasetRowDescriptionVector", "A source model must be set before the selection can be configured." ); initializeDatasetDecriptors( configuration, sourceModel()->rowCount(mRootIndex), mRowSrcToProxyMap, mRowProxyToSrcMap ); - clear(); // clear emits layoutChanged() + invalidate(); // clear emits layoutChanged() } void DatasetProxyModel::setDatasetColumnDescriptionVector( const DatasetDescriptionVector& configuration ) { Q_ASSERT_X( sourceModel(), "DatasetProxyModel::setDatasetColumnDescriptionVector", "A source model must be set before the selection can be configured." ); initializeDatasetDecriptors( configuration, sourceModel()->columnCount(mRootIndex), mColSrcToProxyMap, mColProxyToSrcMap ); - clear(); // clear emits layoutChanged() + invalidate(); // clear emits layoutChanged() } void DatasetProxyModel::setDatasetDescriptionVectors( const DatasetDescriptionVector& rowConfig, const DatasetDescriptionVector& columnConfig ) { setDatasetRowDescriptionVector( rowConfig ); setDatasetColumnDescriptionVector( columnConfig ); } QModelIndex DatasetProxyModel::index( int row, int column, const QModelIndex &parent ) const { return mapFromSource( sourceModel()->index( mapProxyRowToSource(row), mapProxyColumnToSource(column), parent ) ); } QModelIndex DatasetProxyModel::parent( const QModelIndex& child ) const { // return mapFromSource( sourceModel()->parent( child ) ); return mapFromSource( sourceModel()->parent( mapToSource( child ) ) ); } QModelIndex DatasetProxyModel::mapFromSource( const QModelIndex & sourceIndex ) const { Q_ASSERT_X( sourceModel(), "DatasetProxyModel::mapFromSource", "A source " "model must be set before the selection can be configured." ); if ( !sourceIndex.isValid() ) return sourceIndex; if ( mRowSrcToProxyMap.isEmpty() && mColSrcToProxyMap.isEmpty() ) { return createIndex( sourceIndex.row(), sourceIndex.column(), sourceIndex.internalPointer() ); } else { int row = mapSourceRowToProxy( sourceIndex.row() ); int column = mapSourceColumnToProxy( sourceIndex.column() ); return createIndex( row, column, sourceIndex.internalPointer() ); } } QModelIndex DatasetProxyModel::mapToSource( const QModelIndex& proxyIndex ) const { Q_ASSERT_X( sourceModel(), "DatasetProxyModel::mapToSource", "A source " "model must be set before the selection can be configured." ); if ( !proxyIndex.isValid() ) return proxyIndex; if ( mRowSrcToProxyMap.isEmpty() && mColSrcToProxyMap.isEmpty() ) { return sourceModel()->index( proxyIndex.row(), proxyIndex.column(), mRootIndex ); } else { int row = mapProxyRowToSource( proxyIndex.row() ); int column = mapProxyColumnToSource( proxyIndex.column() ); return sourceModel()->index( row, column, mRootIndex ); } } bool DatasetProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex & ) const { if ( mRowSrcToProxyMap.isEmpty() ) { // no row mapping set, all rows are passed down: return true; } else { Q_ASSERT( sourceModel() ); Q_ASSERT( mRowSrcToProxyMap.size() == sourceModel()->rowCount(mRootIndex) ); if ( mRowSrcToProxyMap[sourceRow] == -1 ) { // this row is explicitly not accepted: return false; } else { Q_ASSERT( mRowSrcToProxyMap[sourceRow] >= 0 && mRowSrcToProxyMap[sourceRow] < mRowSrcToProxyMap.size() ); return true; } } } bool DatasetProxyModel::filterAcceptsColumn( int sourceColumn, const QModelIndex & ) const { if ( mColSrcToProxyMap.isEmpty() ) { // no column mapping set up yet, all columns are passed down: return true; } else { Q_ASSERT( sourceModel() ); Q_ASSERT( mColSrcToProxyMap.size() == sourceModel()->columnCount(mRootIndex) ); if ( mColSrcToProxyMap[sourceColumn] == -1 ) { // this column is explicitly not accepted: return false; } else { Q_ASSERT( mColSrcToProxyMap[sourceColumn] >= 0 && mColSrcToProxyMap[sourceColumn] < mColSrcToProxyMap.size() ); return true; } } } int DatasetProxyModel::mapProxyRowToSource( const int& proxyRow ) const { if ( mRowProxyToSrcMap.isEmpty() ) { // if no row mapping is set, we pass down the row: return proxyRow; } else { Q_ASSERT( proxyRow >= 0 && proxyRow < mRowProxyToSrcMap.size() ); return mRowProxyToSrcMap[ proxyRow ]; } } int DatasetProxyModel::mapProxyColumnToSource( const int& proxyColumn ) const { if ( mColProxyToSrcMap.isEmpty() ) { // if no column mapping is set, we pass down the column: return proxyColumn; } else { Q_ASSERT( proxyColumn >= 0 && proxyColumn < mColProxyToSrcMap.size() ); return mColProxyToSrcMap[ proxyColumn ]; } } int DatasetProxyModel::mapSourceRowToProxy( const int& sourceRow ) const { if ( mRowSrcToProxyMap.isEmpty() ) { return sourceRow; } else { Q_ASSERT( sourceRow >= 0 && sourceRow < mRowSrcToProxyMap.size() ); return mRowSrcToProxyMap[sourceRow]; } } int DatasetProxyModel::mapSourceColumnToProxy( const int& sourceColumn ) const { if ( mColSrcToProxyMap.isEmpty() ) { return sourceColumn; } else { Q_ASSERT( sourceColumn >= 0 && sourceColumn < mColSrcToProxyMap.size() ); return mColSrcToProxyMap.at( sourceColumn ) ; } } void DatasetProxyModel::resetDatasetDescriptions() { mRowSrcToProxyMap.clear(); mRowProxyToSrcMap.clear(); mColSrcToProxyMap.clear(); mColProxyToSrcMap.clear(); - clear(); + invalidate(); } QVariant DatasetProxyModel::data(const QModelIndex &index, int role) const { return sourceModel()->data( mapToSource( index ), role ); } bool DatasetProxyModel::setData( const QModelIndex& index, const QVariant& value, int role ) { return sourceModel()->setData( mapToSource( index ), value, role ); } QVariant DatasetProxyModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( orientation == Qt::Horizontal ) { if ( mapProxyColumnToSource ( section ) == -1 ) { return QVariant(); } else { return sourceModel()->headerData( mapProxyColumnToSource( section ), orientation, role ); } } else { if ( mapProxyRowToSource ( section ) == -1 ) { return QVariant(); } else { return sourceModel()->headerData( mapProxyRowToSource ( section ), orientation, role ); } } } void DatasetProxyModel::initializeDatasetDecriptors( const DatasetDescriptionVector& inConfiguration, const int sourceCount, DatasetDescriptionVector& outSourceToProxyMap, DatasetDescriptionVector& outProxyToSourceMap ) { // in the current mapping implementation, the proxy-to-source map is // identical to the configuration vector: outProxyToSourceMap = inConfiguration; outSourceToProxyMap.fill( -1, sourceCount ); for ( int index = 0; index < inConfiguration.size(); ++index ) { // make sure the values in inConfiguration point to columns in the // source model: if ( inConfiguration[index] == -1 ) { continue; } Q_ASSERT_X( inConfiguration[ index ] >= 0 && inConfiguration[ index ] < sourceCount, "DatasetProxyModel::initializeDatasetDecriptors", "column index outside of source model" ); Q_ASSERT_X( outSourceToProxyMap[ inConfiguration[ index ] ] == -1 , "DatasetProxyModel::initializeDatasetDecriptors", "no duplicates allowed in mapping configuration, mapping has to be reversible" ); outSourceToProxyMap[ inConfiguration[ index ] ] = index; } } void DatasetProxyModel::setSourceModel(QAbstractItemModel *m) { if ( sourceModel() ) { disconnect( sourceModel(), SIGNAL(layoutChanged()), this, SLOT(resetDatasetDescriptions()) ); } QSortFilterProxyModel::setSourceModel( m ); mRootIndex = QModelIndex(); if ( m ) { connect( m, SIGNAL(layoutChanged()), this, SLOT(resetDatasetDescriptions()) ); connect( m, SIGNAL(layoutChanged()), this, SIGNAL(layoutChanged()) ); } resetDatasetDescriptions(); } void DatasetProxyModel::setSourceRootIndex(const QModelIndex& rootIdx) { mRootIndex = rootIdx; resetDatasetDescriptions(); } diff --git a/src/KChart/KChartLegend.cpp b/src/KChart/KChartLegend.cpp index a7fb019..3bb0a03 100644 --- a/src/KChart/KChartLegend.cpp +++ b/src/KChart/KChartLegend.cpp @@ -1,1259 +1,1259 @@ /* * 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 "KChartLegend.h" #include "KChartLegend_p.h" #include #include #include #include #include "KTextDocument.h" #include #include "KChartLayoutItems.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KChart; Legend::Private::Private() : referenceArea( nullptr ), position( Position::East ), alignment( Qt::AlignCenter ), textAlignment( Qt::AlignCenter ), relativePosition( RelativePosition() ), orientation( Qt::Vertical ), order( Qt::AscendingOrder ), showLines( false ), titleText( QObject::tr( "Legend" ) ), spacing( 1 ), useAutomaticMarkerSize( true ), legendStyle( MarkersOnly ) { // By default we specify a simple, hard point as the 'relative' position's ref. point, // since we can not be sure that there will be any parent specified for the legend. relativePosition.setReferencePoints( PositionPoints( QPointF( 0.0, 0.0 ) ) ); relativePosition.setReferencePosition( Position::NorthWest ); relativePosition.setAlignment( Qt::AlignTop | Qt::AlignLeft ); relativePosition.setHorizontalPadding( Measure( 4.0, KChartEnums::MeasureCalculationModeAbsolute ) ); relativePosition.setVerticalPadding( Measure( 4.0, KChartEnums::MeasureCalculationModeAbsolute ) ); } Legend::Private::~Private() { // this bloc left empty intentionally } #define d d_func() Legend::Legend( QWidget* parent ) : AbstractAreaWidget( new Private(), parent ) { d->referenceArea = parent; init(); } Legend::Legend( AbstractDiagram* diagram, QWidget* parent ) : AbstractAreaWidget( new Private(), parent ) { d->referenceArea = parent; init(); setDiagram( diagram ); } Legend::~Legend() { emit destroyedLegend( this ); } void Legend::init() { setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); d->layout = new QGridLayout( this ); - d->layout->setMargin( 2 ); + d->layout->setContentsMargins( 2, 2, 2, 2 ); d->layout->setSpacing( d->spacing ); const Measure normalFontSizeTitle( 12, KChartEnums::MeasureCalculationModeAbsolute ); const Measure normalFontSizeLabels( 10, KChartEnums::MeasureCalculationModeAbsolute ); const Measure minimalFontSize( 4, KChartEnums::MeasureCalculationModeAbsolute ); TextAttributes textAttrs; textAttrs.setPen( QPen( Qt::black ) ); textAttrs.setFont( QFont( QLatin1String( "helvetica" ), 10, QFont::Normal, false ) ); textAttrs.setFontSize( normalFontSizeLabels ); textAttrs.setMinimalFontSize( minimalFontSize ); setTextAttributes( textAttrs ); TextAttributes titleTextAttrs; titleTextAttrs.setPen( QPen( Qt::black ) ); titleTextAttrs.setFont( QFont( QLatin1String( "helvetica" ), 12, QFont::Bold, false ) ); titleTextAttrs.setFontSize( normalFontSizeTitle ); titleTextAttrs.setMinimalFontSize( minimalFontSize ); setTitleTextAttributes( titleTextAttrs ); FrameAttributes frameAttrs; frameAttrs.setVisible( true ); frameAttrs.setPen( QPen( Qt::black ) ); frameAttrs.setPadding( 1 ); setFrameAttributes( frameAttrs ); d->position = Position::NorthEast; d->alignment = Qt::AlignCenter; } QSize Legend::minimumSizeHint() const { return sizeHint(); } //#define DEBUG_LEGEND_PAINT QSize Legend::sizeHint() const { #ifdef DEBUG_LEGEND_PAINT qDebug() << "Legend::sizeHint() started"; #endif Q_FOREACH( AbstractLayoutItem* paintItem, d->paintItems ) { paintItem->sizeHint(); } return AbstractAreaWidget::sizeHint(); } void Legend::needSizeHint() { buildLegend(); } void Legend::resizeLayout( const QSize& size ) { #ifdef DEBUG_LEGEND_PAINT qDebug() << "Legend::resizeLayout started"; #endif if ( d->layout ) { d->reflowHDatasetItems( this ); d->layout->setGeometry( QRect(QPoint( 0,0 ), size) ); activateTheLayout(); } #ifdef DEBUG_LEGEND_PAINT qDebug() << "Legend::resizeLayout done"; #endif } void Legend::activateTheLayout() { if ( d->layout && d->layout->parent() ) { d->layout->activate(); } } void Legend::setLegendStyle( LegendStyle style ) { if ( d->legendStyle == style ) { return; } d->legendStyle = style; setNeedRebuild(); } Legend::LegendStyle Legend::legendStyle() const { return d->legendStyle; } /** * Creates an exact copy of this legend. */ Legend* Legend::clone() const { Legend* legend = new Legend( new Private( *d ), nullptr ); legend->setTextAttributes( textAttributes() ); legend->setTitleTextAttributes( titleTextAttributes() ); legend->setFrameAttributes( frameAttributes() ); legend->setUseAutomaticMarkerSize( useAutomaticMarkerSize() ); legend->setPosition( position() ); legend->setAlignment( alignment() ); legend->setTextAlignment( textAlignment() ); legend->setLegendStyle( legendStyle() ); return legend; } bool Legend::compare( const Legend* other ) const { if ( other == this ) { return true; } if ( !other ) { return false; } return ( AbstractAreaBase::compare( other ) ) && (isVisible() == other->isVisible()) && (position() == other->position()) && (alignment() == other->alignment())&& (textAlignment() == other->textAlignment())&& (floatingPosition() == other->floatingPosition()) && (orientation() == other->orientation())&& (showLines() == other->showLines())&& (texts() == other->texts())&& (brushes() == other->brushes())&& (pens() == other->pens())&& (markerAttributes() == other->markerAttributes())&& (useAutomaticMarkerSize() == other->useAutomaticMarkerSize()) && (textAttributes() == other->textAttributes()) && (titleText() == other->titleText())&& (titleTextAttributes() == other->titleTextAttributes()) && (spacing() == other->spacing()) && (legendStyle() == other->legendStyle()); } void Legend::paint( QPainter* painter ) { #ifdef DEBUG_LEGEND_PAINT qDebug() << "entering Legend::paint( QPainter* painter )"; #endif if ( !diagram() ) { return; } activateTheLayout(); Q_FOREACH( AbstractLayoutItem* paintItem, d->paintItems ) { paintItem->paint( painter ); } #ifdef DEBUG_LEGEND_PAINT qDebug() << "leaving Legend::paint( QPainter* painter )"; #endif } uint Legend::datasetCount() const { int modelLabelsCount = 0; Q_FOREACH ( DiagramObserver* observer, d->observers ) { AbstractDiagram* diagram = observer->diagram(); Q_ASSERT( diagram->datasetLabels().count() == diagram->datasetBrushes().count() ); modelLabelsCount += diagram->datasetLabels().count(); } return modelLabelsCount; } void Legend::setReferenceArea( const QWidget* area ) { if ( area == d->referenceArea ) { return; } d->referenceArea = area; setNeedRebuild(); } const QWidget* Legend::referenceArea() const { return d->referenceArea ? d->referenceArea : qobject_cast< const QWidget* >( parent() ); } AbstractDiagram* Legend::diagram() const { if ( d->observers.isEmpty() ) { return nullptr; } return d->observers.first()->diagram(); } DiagramList Legend::diagrams() const { DiagramList list; for ( int i = 0; i < d->observers.size(); ++i ) { list << d->observers.at(i)->diagram(); } return list; } ConstDiagramList Legend::constDiagrams() const { ConstDiagramList list; for ( int i = 0; i < d->observers.size(); ++i ) { list << d->observers.at(i)->diagram(); } return list; } void Legend::addDiagram( AbstractDiagram* newDiagram ) { if ( newDiagram ) { DiagramObserver* observer = new DiagramObserver( newDiagram, this ); DiagramObserver* oldObs = d->findObserverForDiagram( newDiagram ); if ( oldObs ) { delete oldObs; d->observers[ d->observers.indexOf( oldObs ) ] = observer; } else { d->observers.append( observer ); } connect( observer, SIGNAL(diagramAboutToBeDestroyed(AbstractDiagram*)), SLOT(resetDiagram(AbstractDiagram*))); connect( observer, SIGNAL(diagramDataChanged(AbstractDiagram*)), SLOT(setNeedRebuild())); connect( observer, SIGNAL(diagramDataHidden(AbstractDiagram*)), SLOT(setNeedRebuild())); connect( observer, SIGNAL(diagramAttributesChanged(AbstractDiagram*)), SLOT(setNeedRebuild())); setNeedRebuild(); } } void Legend::removeDiagram( AbstractDiagram* oldDiagram ) { int datasetBrushOffset = 0; QList< AbstractDiagram * > diagrams = this->diagrams(); for ( int i = 0; i datasetBrushes().count(); i++ ) { d->brushes.remove(datasetBrushOffset + i); d->texts.remove(datasetBrushOffset + i); } for ( int i = 0; i < oldDiagram->datasetPens().count(); i++ ) { d->pens.remove(datasetBrushOffset + i); } break; } datasetBrushOffset += diagrams.at(i)->datasetBrushes().count(); } if ( oldDiagram ) { DiagramObserver *oldObs = d->findObserverForDiagram( oldDiagram ); if ( oldObs ) { delete oldObs; d->observers.removeAt( d->observers.indexOf( oldObs ) ); } setNeedRebuild(); } } void Legend::removeDiagrams() { // removeDiagram() may change the d->observers list. So, build up the list of // diagrams to remove first and then remove them one by one. QList< AbstractDiagram * > diagrams; for ( int i = 0; i < d->observers.size(); ++i ) { diagrams.append( d->observers.at( i )->diagram() ); } for ( int i = 0; i < diagrams.count(); ++i ) { removeDiagram( diagrams[ i ] ); } } void Legend::replaceDiagram( AbstractDiagram* newDiagram, AbstractDiagram* oldDiagram ) { AbstractDiagram* old = oldDiagram; if ( !d->observers.isEmpty() && !old ) { old = d->observers.first()->diagram(); if ( !old ) { d->observers.removeFirst(); // first entry had a 0 diagram } } if ( old ) { removeDiagram( old ); } if ( newDiagram ) { addDiagram( newDiagram ); } } uint Legend::dataSetOffset( AbstractDiagram* diagram ) { uint offset = 0; for ( int i = 0; i < d->observers.count(); ++i ) { if ( d->observers.at(i)->diagram() == diagram ) { return offset; } AbstractDiagram* diagram = d->observers.at(i)->diagram(); if ( !diagram->model() ) { continue; } offset = offset + diagram->model()->columnCount(); } return offset; } void Legend::setDiagram( AbstractDiagram* newDiagram ) { replaceDiagram( newDiagram ); } void Legend::resetDiagram( AbstractDiagram* oldDiagram ) { removeDiagram( oldDiagram ); } void Legend::setVisible( bool visible ) { // do NOT bail out if visible == isVisible(), because the return value of isVisible() also depends // on the visibility of the parent. QWidget::setVisible( visible ); emitPositionChanged(); } void Legend::setNeedRebuild() { buildLegend(); sizeHint(); } void Legend::setPosition( Position position ) { if ( d->position == position ) { return; } d->position = position; emitPositionChanged(); } void Legend::emitPositionChanged() { emit positionChanged( this ); emit propertiesChanged(); } Position Legend::position() const { return d->position; } void Legend::setAlignment( Qt::Alignment alignment ) { if ( d->alignment == alignment ) { return; } d->alignment = alignment; emitPositionChanged(); } Qt::Alignment Legend::alignment() const { return d->alignment; } void Legend::setTextAlignment( Qt::Alignment alignment ) { if ( d->textAlignment == alignment ) { return; } d->textAlignment = alignment; emitPositionChanged(); } Qt::Alignment Legend::textAlignment() const { return d->textAlignment; } void Legend::setLegendSymbolAlignment( Qt::Alignment alignment ) { if ( d->legendLineSymbolAlignment == alignment ) { return; } d->legendLineSymbolAlignment = alignment; emitPositionChanged(); } Qt::Alignment Legend::legendSymbolAlignment() const { return d->legendLineSymbolAlignment ; } void Legend::setFloatingPosition( const RelativePosition& relativePosition ) { d->position = Position::Floating; if ( d->relativePosition != relativePosition ) { d->relativePosition = relativePosition; emitPositionChanged(); } } const RelativePosition Legend::floatingPosition() const { return d->relativePosition; } void Legend::setOrientation( Qt::Orientation orientation ) { if ( d->orientation == orientation ) { return; } d->orientation = orientation; setNeedRebuild(); emitPositionChanged(); } Qt::Orientation Legend::orientation() const { return d->orientation; } void Legend::setSortOrder( Qt::SortOrder order ) { if ( d->order == order ) { return; } d->order = order; setNeedRebuild(); emitPositionChanged(); } Qt::SortOrder Legend::sortOrder() const { return d->order; } void Legend::setShowLines( bool legendShowLines ) { if ( d->showLines == legendShowLines ) { return; } d->showLines = legendShowLines; setNeedRebuild(); emitPositionChanged(); } bool Legend::showLines() const { return d->showLines; } void Legend::setUseAutomaticMarkerSize( bool useAutomaticMarkerSize ) { d->useAutomaticMarkerSize = useAutomaticMarkerSize; setNeedRebuild(); emitPositionChanged(); } bool Legend::useAutomaticMarkerSize() const { return d->useAutomaticMarkerSize; } /** \brief Removes all legend texts that might have been set by setText. This resets the Legend to default behaviour: Texts are created automatically. */ void Legend::resetTexts() { if ( !d->texts.count() ) { return; } d->texts.clear(); setNeedRebuild(); } void Legend::setText( uint dataset, const QString& text ) { if ( d->texts[ dataset ] == text ) { return; } d->texts[ dataset ] = text; setNeedRebuild(); } QString Legend::text( uint dataset ) const { if ( d->texts.find( dataset ) != d->texts.end() ) { return d->texts[ dataset ]; } else { return d->modelLabels[ dataset ]; } } const QMap Legend::texts() const { return d->texts; } void Legend::setColor( uint dataset, const QColor& color ) { if ( d->brushes[ dataset ] != color ) { d->brushes[ dataset ] = color; setNeedRebuild(); update(); } } void Legend::setBrush( uint dataset, const QBrush& brush ) { if ( d->brushes[ dataset ] != brush ) { d->brushes[ dataset ] = brush; setNeedRebuild(); update(); } } QBrush Legend::brush( uint dataset ) const { if ( d->brushes.contains( dataset ) ) { return d->brushes[ dataset ]; } else { return d->modelBrushes[ dataset ]; } } const QMap Legend::brushes() const { return d->brushes; } void Legend::setBrushesFromDiagram( AbstractDiagram* diagram ) { bool changed = false; QList datasetBrushes = diagram->datasetBrushes(); for ( int i = 0; i < datasetBrushes.count(); i++ ) { if ( d->brushes[ i ] != datasetBrushes[ i ] ) { d->brushes[ i ] = datasetBrushes[ i ]; changed = true; } } if ( changed ) { setNeedRebuild(); update(); } } void Legend::setPen( uint dataset, const QPen& pen ) { if ( d->pens[dataset] == pen ) { return; } d->pens[dataset] = pen; setNeedRebuild(); update(); } QPen Legend::pen( uint dataset ) const { if ( d->pens.find( dataset ) != d->pens.end() ) { return d->pens[ dataset ]; } else { return d->modelPens[ dataset ]; } } const QMap Legend::pens() const { return d->pens; } void Legend::setMarkerAttributes( uint dataset, const MarkerAttributes& markerAttributes ) { if ( d->markerAttributes[dataset] == markerAttributes ) { return; } d->markerAttributes[ dataset ] = markerAttributes; setNeedRebuild(); update(); } MarkerAttributes Legend::markerAttributes( uint dataset ) const { if ( d->markerAttributes.find( dataset ) != d->markerAttributes.end() ) { return d->markerAttributes[ dataset ]; } else if ( static_cast( d->modelMarkers.count() ) > dataset ) { return d->modelMarkers[ dataset ]; } else { return MarkerAttributes(); } } const QMap Legend::markerAttributes() const { return d->markerAttributes; } void Legend::setTextAttributes( const TextAttributes &a ) { if ( d->textAttributes == a ) { return; } d->textAttributes = a; setNeedRebuild(); } TextAttributes Legend::textAttributes() const { return d->textAttributes; } void Legend::setTitleText( const QString& text ) { if ( d->titleText == text ) { return; } d->titleText = text; setNeedRebuild(); } QString Legend::titleText() const { return d->titleText; } void Legend::setTitleTextAttributes( const TextAttributes &a ) { if ( d->titleTextAttributes == a ) { return; } d->titleTextAttributes = a; setNeedRebuild(); } TextAttributes Legend::titleTextAttributes() const { return d->titleTextAttributes; } void Legend::forceRebuild() { #ifdef DEBUG_LEGEND_PAINT qDebug() << "entering Legend::forceRebuild()"; #endif buildLegend(); #ifdef DEBUG_LEGEND_PAINT qDebug() << "leaving Legend::forceRebuild()"; #endif } void Legend::setSpacing( uint space ) { if ( d->spacing == space && d->layout->spacing() == int( space ) ) { return; } d->spacing = space; d->layout->setSpacing( space ); setNeedRebuild(); } uint Legend::spacing() const { return d->spacing; } void Legend::setDefaultColors() { Palette pal = Palette::defaultPalette(); for ( int i = 0; i < pal.size(); i++ ) { setBrush( i, pal.getBrush( i ) ); } } void Legend::setRainbowColors() { Palette pal = Palette::rainbowPalette(); for ( int i = 0; i < pal.size(); i++ ) { setBrush( i, pal.getBrush( i ) ); } } void Legend::setSubduedColors( bool ordered ) { Palette pal = Palette::subduedPalette(); if ( ordered ) { for ( int i = 0; i < pal.size(); i++ ) { setBrush( i, pal.getBrush( i ) ); } } else { static const int s_subduedColorsCount = 18; Q_ASSERT( pal.size() >= s_subduedColorsCount ); static const int order[ s_subduedColorsCount ] = { 0, 5, 10, 15, 2, 7, 12, 17, 4, 9, 14, 1, 6, 11, 16, 3, 8, 13 }; for ( int i = 0; i < s_subduedColorsCount; i++ ) { setBrush( i, pal.getBrush( order[i] ) ); } } } void Legend::resizeEvent( QResizeEvent * event ) { Q_UNUSED( event ); #ifdef DEBUG_LEGEND_PAINT qDebug() << "Legend::resizeEvent() called"; #endif forceRebuild(); sizeHint(); QTimer::singleShot( 0, this, SLOT(emitPositionChanged()) ); } void Legend::Private::fetchPaintOptions( Legend *q ) { modelLabels.clear(); modelBrushes.clear(); modelPens.clear(); modelMarkers.clear(); // retrieve the diagrams' settings for all non-hidden datasets for ( int i = 0; i < observers.size(); ++i ) { const AbstractDiagram* diagram = observers.at( i )->diagram(); if ( !diagram ) { continue; } const QStringList diagramLabels = diagram->datasetLabels(); const QList diagramBrushes = diagram->datasetBrushes(); const QList diagramPens = diagram->datasetPens(); const QList diagramMarkers = diagram->datasetMarkers(); const bool ascend = q->sortOrder() == Qt::AscendingOrder; int dataset = ascend ? 0 : diagramLabels.count() - 1; const int end = ascend ? diagramLabels.count() : -1; for ( ; dataset != end; dataset += ascend ? 1 : -1 ) { if ( diagram->isHidden( dataset ) || q->datasetIsHidden( dataset ) ) { continue; } modelLabels += diagramLabels[ dataset ]; modelBrushes += diagramBrushes[ dataset ]; modelPens += diagramPens[ dataset ]; modelMarkers += diagramMarkers[ dataset ]; } } Q_ASSERT( modelLabels.count() == modelBrushes.count() ); } QSizeF Legend::Private::markerSize( Legend *q, int dataset, qreal fontHeight ) const { QSizeF suppliedSize = q->markerAttributes( dataset ).markerSize(); if ( q->useAutomaticMarkerSize() || !suppliedSize.isValid() ) { return QSizeF( fontHeight, fontHeight ); } else { return suppliedSize; } } QSizeF Legend::Private::maxMarkerSize( Legend *q, qreal fontHeight ) const { QSizeF ret( 1.0, 1.0 ); if ( q->legendStyle() != LinesOnly ) { for ( int dataset = 0; dataset < modelLabels.count(); ++dataset ) { ret = ret.expandedTo( markerSize( q, dataset, fontHeight ) ); } } return ret; } HDatasetItem::HDatasetItem() : markerLine(nullptr), label(nullptr), separatorLine(nullptr), spacer(nullptr) {} static void updateToplevelLayout(QWidget *w) { while ( w ) { if ( w->isTopLevel() ) { // The null check has proved necessary during destruction of the Legend / Chart if ( w->layout() ) { w->layout()->update(); } break; } else { w = qobject_cast< QWidget * >( w->parent() ); Q_ASSERT( w ); } } } void Legend::buildLegend() { /* Grid layout partitioning (horizontal orientation): row zero is the title, row one the divider line between title and dataset items, row two for each item: line, marker, text label and separator line in that order. In a vertically oriented legend, row pairs (2, 3), ... contain a possible separator line (first row) and (second row) line, marker, text label each. */ d->destroyOldLayout(); if ( orientation() == Qt::Vertical ) { d->layout->setColumnStretch( 6, 1 ); } else { d->layout->setColumnStretch( 6, 0 ); } d->fetchPaintOptions( this ); const KChartEnums::MeasureOrientation measureOrientation = orientation() == Qt::Vertical ? KChartEnums::MeasureOrientationMinimum : KChartEnums::MeasureOrientationHorizontal; // legend caption if ( !titleText().isEmpty() && titleTextAttributes().isVisible() ) { TextLayoutItem* titleItem = new TextLayoutItem( titleText(), titleTextAttributes(), referenceArea(), measureOrientation, d->textAlignment ); titleItem->setParentWidget( this ); d->paintItems << titleItem; d->layout->addItem( titleItem, 0, 0, 1, 5, Qt::AlignCenter ); // The line between the title and the legend items, if any. if ( showLines() && d->modelLabels.count() ) { HorizontalLineLayoutItem* lineItem = new HorizontalLineLayoutItem; d->paintItems << lineItem; d->layout->addItem( lineItem, 1, 0, 1, 5, Qt::AlignCenter ); } } qreal fontHeight = textAttributes().calculatedFontSize( referenceArea(), measureOrientation ); { QFont tmpFont = textAttributes().font(); tmpFont.setPointSizeF( fontHeight ); if ( GlobalMeasureScaling::paintDevice() ) { fontHeight = QFontMetricsF( tmpFont, GlobalMeasureScaling::paintDevice() ).height(); } else { fontHeight = QFontMetricsF( tmpFont ).height(); } } const QSizeF maxMarkerSize = d->maxMarkerSize( this, fontHeight ); // If we show a marker on a line, we paint it after 8 pixels // of the line have been painted. This allows to see the line style // at the right side of the marker without the line needing to // be too long. // (having the marker in the middle of the line would require longer lines) const int lineLengthLeftOfMarker = 8; int maxLineLength = 18; { bool hasComplexPenStyle = false; for ( int dataset = 0; dataset < d->modelLabels.count(); ++dataset ) { const QPen pn = pen( dataset ); const Qt::PenStyle ps = pn.style(); if ( ps != Qt::NoPen ) { maxLineLength = qMin( pn.width() * 18, maxLineLength ); if ( ps != Qt::SolidLine ) { hasComplexPenStyle = true; } } } if ( legendStyle() != LinesOnly ) { if ( hasComplexPenStyle ) maxLineLength += lineLengthLeftOfMarker; maxLineLength += int( maxMarkerSize.width() ); } } // for all datasets: add (line)marker items and text items to the layout; // actual layout happens in flowHDatasetItems() for horizontal layout, here for vertical for ( int dataset = 0; dataset < d->modelLabels.count(); ++dataset ) { const int vLayoutRow = 2 + dataset * 2; HDatasetItem dsItem; // It is possible to set the marker brush through markerAttributes as well as // the dataset brush set in the diagram - the markerAttributes have higher precedence. MarkerAttributes markerAttrs = markerAttributes( dataset ); markerAttrs.setMarkerSize( d->markerSize( this, dataset, fontHeight ) ); const QBrush markerBrush = markerAttrs.markerColor().isValid() ? QBrush( markerAttrs.markerColor() ) : brush( dataset ); switch ( legendStyle() ) { case MarkersOnly: dsItem.markerLine = new MarkerLayoutItem( diagram(), markerAttrs, markerBrush, markerAttrs.pen(), Qt::AlignLeft | Qt::AlignVCenter ); break; case LinesOnly: dsItem.markerLine = new LineLayoutItem( diagram(), maxLineLength, pen( dataset ), d->legendLineSymbolAlignment, Qt::AlignCenter ); break; case MarkersAndLines: dsItem.markerLine = new LineWithMarkerLayoutItem( diagram(), maxLineLength, pen( dataset ), lineLengthLeftOfMarker, markerAttrs, markerBrush, markerAttrs.pen(), Qt::AlignCenter ); break; default: Q_ASSERT( false ); } dsItem.label = new TextLayoutItem( text( dataset ), textAttributes(), referenceArea(), measureOrientation, d->textAlignment ); dsItem.label->setParentWidget( this ); // horizontal layout is deferred to flowDatasetItems() if ( orientation() == Qt::Horizontal ) { d->hLayoutDatasets << dsItem; continue; } // (actual) vertical layout here if ( dsItem.markerLine ) { d->layout->addItem( dsItem.markerLine, vLayoutRow, 1, 1, 1, Qt::AlignCenter ); d->paintItems << dsItem.markerLine; } d->layout->addItem( dsItem.label, vLayoutRow, 3, 1, 1, Qt::AlignLeft | Qt::AlignVCenter ); d->paintItems << dsItem.label; // horizontal separator line, only between items if ( showLines() && dataset != d->modelLabels.count() - 1 ) { HorizontalLineLayoutItem* lineItem = new HorizontalLineLayoutItem; d->layout->addItem( lineItem, vLayoutRow + 1, 0, 1, 5, Qt::AlignCenter ); d->paintItems << lineItem; } } if ( orientation() == Qt::Horizontal ) { d->flowHDatasetItems( this ); } // vertical line (only in vertical mode) if ( orientation() == Qt::Vertical && showLines() && d->modelLabels.count() ) { VerticalLineLayoutItem* lineItem = new VerticalLineLayoutItem; d->paintItems << lineItem; d->layout->addItem( lineItem, 2, 2, d->modelLabels.count() * 2, 1 ); } updateToplevelLayout( this ); emit propertiesChanged(); #ifdef DEBUG_LEGEND_PAINT qDebug() << "leaving Legend::buildLegend()"; #endif } int HDatasetItem::height() const { return qMax( markerLine->sizeHint().height(), label->sizeHint().height() ); } void Legend::Private::reflowHDatasetItems( Legend *q ) { if (hLayoutDatasets.isEmpty()) { return; } paintItems.clear(); // Dissolve exactly the QHBoxLayout(s) created as "currentLine" in flowHDatasetItems - don't remove the // caption and line under the caption! Those are easily identified because they aren't QLayouts. for ( int i = layout->count() - 1; i >= 0; i-- ) { QLayoutItem *const item = layout->itemAt( i ); QLayout *const hbox = item->layout(); if ( !hbox ) { AbstractLayoutItem *alItem = dynamic_cast< AbstractLayoutItem * >( item ); Q_ASSERT( alItem ); paintItems << alItem; continue; } Q_ASSERT( dynamic_cast< QHBoxLayout * >( hbox ) ); layout->takeAt( i ); // detach children so they aren't deleted with the parent for ( int j = hbox->count() - 1; j >= 0; j-- ) { hbox->takeAt( j ); } delete hbox; } flowHDatasetItems( q ); } // this works pretty much like flow layout for text, and it is only applicable to dataset items // laid out horizontally void Legend::Private::flowHDatasetItems( Legend *q ) { const int separatorLineWidth = 3; // hardcoded in VerticalLineLayoutItem::sizeHint() const int allowedWidth = q->areaGeometry().width(); QHBoxLayout *currentLine = new QHBoxLayout; int columnSpan = 5; int mainLayoutColumn = 0; int row = 0; if ( !titleText.isEmpty() && titleTextAttributes.isVisible() ) { ++row; if (q->showLines()){ ++row; } } layout->addItem( currentLine, row, mainLayoutColumn, /*rowSpan*/1 , columnSpan, Qt::AlignLeft | Qt::AlignVCenter ); mainLayoutColumn += columnSpan; for ( int dataset = 0; dataset < hLayoutDatasets.size(); dataset++ ) { HDatasetItem *hdsItem = &hLayoutDatasets[ dataset ]; bool spacerUsed = false; bool separatorUsed = false; if ( !currentLine->isEmpty() ) { const int separatorWidth = ( q->showLines() ? separatorLineWidth : 0 ) + q->spacing(); const int payloadWidth = hdsItem->markerLine->sizeHint().width() + hdsItem->label->sizeHint().width(); if ( currentLine->sizeHint().width() + separatorWidth + payloadWidth > allowedWidth ) { // too wide, "line break" #ifdef DEBUG_LEGEND_PAINT qDebug() << Q_FUNC_INFO << "break" << mainLayoutColumn << currentLine->sizeHint().width() << currentLine->sizeHint().width() + separatorWidth + payloadWidth << allowedWidth; #endif currentLine = new QHBoxLayout; layout->addItem( currentLine, row, mainLayoutColumn, /*rowSpan*/1 , columnSpan, Qt::AlignLeft | Qt::AlignVCenter ); mainLayoutColumn += columnSpan; } else { // > 1 dataset item in line, put spacing and maybe a separator between them if ( !hdsItem->spacer ) { hdsItem->spacer = new QSpacerItem( q->spacing(), 1 ); } currentLine->addItem( hdsItem->spacer ); spacerUsed = true; if ( q->showLines() ) { if ( !hdsItem->separatorLine ) { hdsItem->separatorLine = new VerticalLineLayoutItem; } paintItems << hdsItem->separatorLine; currentLine->addItem( hdsItem->separatorLine ); separatorUsed = true; } } } // those have no parents in the current layout, so they wouldn't get cleaned up otherwise if ( !spacerUsed ) { delete hdsItem->spacer; hdsItem->spacer = nullptr; } if ( !separatorUsed ) { delete hdsItem->separatorLine; hdsItem->separatorLine = nullptr; } currentLine->addItem( hdsItem->markerLine ); paintItems << hdsItem->markerLine; currentLine->addItem( hdsItem->label ); paintItems << hdsItem->label; } } bool Legend::hasHeightForWidth() const { // this is better than using orientation() because, for layout purposes, we're not height-for-width // *yet* before buildLegend() has been called, and the layout logic might get upset if we say // something that will only be true in the future return !d->hLayoutDatasets.isEmpty(); } int Legend::heightForWidth( int width ) const { if ( d->hLayoutDatasets.isEmpty() ) { return -1; } int ret = 0; // space for caption and line under caption (if any) for (int i = 0; i < 2; i++) { if ( QLayoutItem *item = d->layout->itemAtPosition( i, 0 ) ) { ret += item->sizeHint().height(); } } const int separatorLineWidth = 3; // ### hardcoded in VerticalLineLayoutItem::sizeHint() int currentLineWidth = 0; int currentLineHeight = 0; Q_FOREACH( const HDatasetItem &hdsItem, d->hLayoutDatasets ) { const int payloadWidth = hdsItem.markerLine->sizeHint().width() + hdsItem.label->sizeHint().width(); if ( !currentLineWidth ) { // first iteration currentLineWidth = payloadWidth; } else { const int separatorWidth = ( showLines() ? separatorLineWidth : 0 ) + spacing(); currentLineWidth += separatorWidth + payloadWidth; if ( currentLineWidth > width ) { // too wide, "line break" #ifdef DEBUG_LEGEND_PAINT qDebug() << Q_FUNC_INFO << "heightForWidth break" << currentLineWidth << currentLineWidth + separatorWidth + payloadWidth << width; #endif ret += currentLineHeight + spacing(); currentLineWidth = payloadWidth; currentLineHeight = 0; } } currentLineHeight = qMax( currentLineHeight, hdsItem.height() ); } ret += currentLineHeight; // one less spacings than lines return ret; } void Legend::Private::destroyOldLayout() { // in the horizontal layout case, the QHBoxLayout destructor also deletes child layout items // (it isn't documented that QLayoutItems delete their children) for ( int i = layout->count() - 1; i >= 0; i-- ) { delete layout->takeAt( i ); } Q_ASSERT( !layout->count() ); hLayoutDatasets.clear(); paintItems.clear(); } void Legend::setHiddenDatasets( const QList hiddenDatasets ) { d->hiddenDatasets = hiddenDatasets; } const QList Legend::hiddenDatasets() const { return d->hiddenDatasets; } void Legend::setDatasetHidden( uint dataset, bool hidden ) { if ( hidden && !d->hiddenDatasets.contains( dataset ) ) { d->hiddenDatasets.append( dataset ); } else if ( !hidden && d->hiddenDatasets.contains( dataset ) ) { d->hiddenDatasets.removeAll( dataset ); } } bool Legend::datasetIsHidden( uint dataset ) const { return d->hiddenDatasets.contains( dataset ); } diff --git a/src/KChart/KChartPalette.cpp b/src/KChart/KChartPalette.cpp index a3eb987..f72c2be 100644 --- a/src/KChart/KChartPalette.cpp +++ b/src/KChart/KChartPalette.cpp @@ -1,182 +1,182 @@ /* * 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 "KChartPalette.h" #include "KChartMath_p.h" #include #include using namespace KChart; namespace { static Palette makeDefaultPalette() { Palette p; p.addBrush( Qt::red ); p.addBrush( Qt::green ); p.addBrush( Qt::blue ); p.addBrush( Qt::cyan ); p.addBrush( Qt::magenta ); p.addBrush( Qt::yellow ); p.addBrush( Qt::darkRed ); p.addBrush( Qt::darkGreen ); p.addBrush( Qt::darkBlue ); p.addBrush( Qt::darkCyan ); p.addBrush( Qt::darkMagenta ); p.addBrush( Qt::darkYellow ); return p; } static Palette makeSubduedPalette() { Palette p; p.addBrush( QColor( 0xe0,0x7f,0x70 ) ); p.addBrush( QColor( 0xe2,0xa5,0x6f ) ); p.addBrush( QColor( 0xe0,0xc9,0x70 ) ); p.addBrush( QColor( 0xd1,0xe0,0x70 ) ); p.addBrush( QColor( 0xac,0xe0,0x70 ) ); p.addBrush( QColor( 0x86,0xe0,0x70 ) ); p.addBrush( QColor( 0x70,0xe0,0x7f ) ); p.addBrush( QColor( 0x70,0xe0,0xa4 ) ); p.addBrush( QColor( 0x70,0xe0,0xc9 ) ); p.addBrush( QColor( 0x70,0xd1,0xe0 ) ); p.addBrush( QColor( 0x70,0xac,0xe0 ) ); p.addBrush( QColor( 0x70,0x86,0xe0 ) ); p.addBrush( QColor( 0x7f,0x70,0xe0 ) ); p.addBrush( QColor( 0xa4,0x70,0xe0 ) ); p.addBrush( QColor( 0xc9,0x70,0xe0 ) ); p.addBrush( QColor( 0xe0,0x70,0xd1 ) ); p.addBrush( QColor( 0xe0,0x70,0xac ) ); p.addBrush( QColor( 0xe0,0x70,0x86 ) ); return p; } static Palette makeRainbowPalette() { Palette p; p.addBrush( QColor(255, 0,196) ); p.addBrush( QColor(255, 0, 96) ); p.addBrush( QColor(255, 128,64) ); p.addBrush( Qt::yellow ); p.addBrush( Qt::green ); p.addBrush( Qt::cyan ); p.addBrush( QColor( 96, 96,255) ); p.addBrush( QColor(160, 0,255) ); for ( int i = 8 ; i < 16 ; ++i ) { - p.addBrush( p.getBrush( i - 8 ).color().light(), i ); + p.addBrush( p.getBrush( i - 8 ).color().lighter(), i ); } return p; } } #define d d_func() class Q_DECL_HIDDEN Palette::Private { public: explicit Private() {} ~Private() {} QVector brushes; }; const Palette& Palette::defaultPalette() { static const Palette palette = makeDefaultPalette(); return palette; } const Palette& Palette::subduedPalette() { static const Palette palette = makeSubduedPalette(); return palette; } const Palette& Palette::rainbowPalette() { static const Palette palette = makeRainbowPalette(); return palette; } Palette::Palette( QObject *parent ) : QObject( parent ), _d( new Private ) { } Palette::~Palette() { delete _d; _d = nullptr; } Palette::Palette( const Palette& r ) : QObject(), _d( new Private( *r.d ) ) { } Palette& Palette::operator=( const Palette& r ) { Palette copy( r ); copy.swap( *this ); // emit changed() ? return *this; } bool Palette::isValid() const { return d->brushes.size() >= 1; } int Palette::size() const { return d->brushes.size(); } void Palette::addBrush( const QBrush& brush, int position ) { if ( position < 0 || position >= size() ) { d->brushes.append( brush ); } else { d->brushes.insert( position, brush ); } emit changed(); } QBrush Palette::getBrush( int position ) const { if ( !isValid() ) return QBrush(); return d->brushes.at( position % size() ); } void Palette::removeBrush( int position ) { if ( position < 0 || position >= size() ) return; d->brushes.remove( position ); emit changed(); } diff --git a/src/KChart/Polar/KChartRadarGrid.cpp b/src/KChart/Polar/KChartRadarGrid.cpp index b75cfcd..3c38d04 100644 --- a/src/KChart/Polar/KChartRadarGrid.cpp +++ b/src/KChart/Polar/KChartRadarGrid.cpp @@ -1,225 +1,225 @@ /* * 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 "KChartRadarGrid.h" #include "KChartPaintContext.h" #include "KChartRadarDiagram.h" #include "KChartPieDiagram.h" #include "KChartPrintingParameters.h" #include "KChartMath_p.h" #include using namespace KChart; DataDimensionsList RadarGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const { qDebug("Calling PolarGrid::calculateGrid()"); DataDimensionsList l; //FIXME(khz): do the real calculation l = rawDataDimensions; return l; } static qreal fitFontSizeToGeometry( const QString& text, const QFont& font, const QRectF& geometry, const TextAttributes& ta ) { QFont f = font; const qreal origResult = f.pointSizeF(); qreal result = origResult; const QSizeF mySize = geometry.size(); if ( mySize.isNull() ) return result; const QString t = text; QFontMetrics fm( f ); while ( true ) { const QSizeF textSize = rotatedRect( fm.boundingRect( t ), ta.rotation() ).normalized().size(); if ( textSize.height() <= mySize.height() && textSize.width() <= mySize.width() ) return result; result -= 0.5; if ( result <= 0.0 ) return origResult; f.setPointSizeF( result ); fm = QFontMetrics( f ); } } QPointF scaleToRealPosition( const QPointF& origin, const QRectF& sourceRect, const QRectF& destRect, const AbstractCoordinatePlane& plane ) { QPointF result = plane.translate( origin ); result -= sourceRect.topLeft(); result.setX( result.x() / sourceRect.width() * destRect.width() ); result.setY( result.y() / sourceRect.height() * destRect.height() ); result += destRect.topLeft(); return result; } QPointF scaleToRect( const QPointF& origin, const QRectF& sourceRect, const QRectF& destRect ) { QPointF result( origin ); result -= sourceRect.topLeft(); result.setX( result.x() / sourceRect.width() * destRect.width() ); result.setY( result.y() / sourceRect.height() * destRect.height() ); result += destRect.topLeft(); return result; } void RadarGrid::drawGrid( PaintContext* context ) { const QBrush backupBrush( context->painter()->brush() ); context->painter()->setBrush( QBrush() ); RadarCoordinatePlane* plane = dynamic_cast< RadarCoordinatePlane* >( context->coordinatePlane() ); Q_ASSERT( plane ); Q_ASSERT( plane->diagram() ); QPair< QPointF, QPointF > boundaries = plane->diagram()->dataBoundaries(); Q_ASSERT_X ( plane, "PolarGrid::drawGrid", "Bad function call: PaintContext::coodinatePlane() NOT a polar plane." ); const GridAttributes gridAttrsCircular( plane->gridAttributes( true ) ); const GridAttributes gridAttrsSagittal( plane->gridAttributes( false ) ); //qDebug() << "OK:"; if ( !gridAttrsCircular.isGridVisible() && !gridAttrsSagittal.isGridVisible() ) return; //qDebug() << "A"; // FIXME: we paint the rulers to the settings of the first diagram for now: AbstractPolarDiagram* dgr = dynamic_cast (plane->diagrams().first() ); Q_ASSERT ( dgr ); // only polar diagrams are allowed here // Do not draw a grid for pie diagrams if ( dynamic_cast (plane->diagrams().first() ) ) return; context->painter()->setPen ( PrintingParameters::scalePen( QColor ( Qt::lightGray ) ) ); const qreal min = dgr->dataBoundaries().first.y(); QPointF origin = plane->translate( QPointF( min, 0 ) ) + context->rectangle().topLeft(); //qDebug() << "origin" << origin; const qreal r = qAbs( min ) + dgr->dataBoundaries().second.y(); // use the full extents // distance between two axis lines const qreal step = ( r - qAbs( min ) ) / ( dgr->numberOfGridRings() ); // calculate the height needed for text to be displayed at the bottom and top of the chart QPointF topLeft = context->rectangle().topLeft(); Q_ASSERT( plane->diagram()->model() ); TextAttributes ta = plane->textAttributes(); const int numberOfSpokes = ( int ) ( 360 / plane->angleUnit() ); const qreal stepWidth = boundaries.second.y() / ( dgr->numberOfGridRings() ); QRectF destRect = context->rectangle(); if (ta.isVisible() ) { QAbstractItemModel* model = plane->diagram()->model(); QRectF fontRect = context->rectangle(); fontRect.setSize( QSizeF( fontRect.width(), step / 2.0 ) ); const qreal labelFontSize = fitFontSizeToGeometry( QString::fromLatin1( "TestXYWQgqy" ), ta.font(), fontRect, ta ); QFont labelFont = ta.font(); context->painter()->setPen( ta.pen() ); labelFont.setPointSizeF( labelFontSize ); const QFontMetricsF metric( labelFont ); const qreal labelHeight = metric.height(); QPointF offset; destRect.setY( destRect.y() + 2 * labelHeight ); destRect.setHeight( destRect.height() - 4 * labelHeight ); offset.setY( labelHeight ); offset.setX( 0 ); topLeft += offset; origin += offset; origin = scaleToRealPosition( QPointF( min, 0 ), context->rectangle(), destRect, *plane ); - const qreal aWidth = metric.width( QString::fromLatin1( "A" ) ); + const qreal aWidth = metric.boundingRect( QString::fromLatin1( "A" ) ).width(); const QLineF startLine( origin, scaleToRealPosition( QPointF( r - qAbs( min ), 0 ), context->rectangle(), destRect, *plane ) ); for ( int i = 0; i < model->rowCount(); ++i ) { const QLineF currentLine( origin, scaleToRealPosition( QPointF( r - qAbs( min ), i ), context->rectangle(), destRect, *plane ) ); const int angle = ( int ) startLine.angleTo( currentLine ) % 360; const qreal angleTest = qAbs( angle - 180 ); const QString data = model->headerData( i, Qt::Vertical ).toString(); - const qreal xOffset = metric.width( data ) / 2.0; + const qreal xOffset = metric.boundingRect( data ).width() / 2.0; if ( angleTest < 5.0 ) context->painter()->drawText( currentLine.pointAt( 1 ) + QPointF( -xOffset, labelHeight + qAbs( min ) ) , data ); else if ( qAbs( angleTest - 180 ) < 5.0 ) context->painter()->drawText( currentLine.pointAt( 1 ) - QPointF( xOffset, labelHeight + qAbs( min ) ) , data ); else if ( angle < 175 && angle > 5 ) context->painter()->drawText( currentLine.pointAt( 1 ) - QPointF( xOffset * 2 + qAbs( min ) + aWidth, -labelHeight/ 2.0 + qAbs( min ) ) , data ); else if ( angle < 355 && angle > 185 ) context->painter()->drawText( currentLine.pointAt( 1 ) + QPointF( qAbs( min ) + aWidth, labelHeight/ 2.0 + qAbs( min ) ) , data ); } } context->painter()->setPen ( PrintingParameters::scalePen( QColor ( Qt::lightGray ) ) ); if ( plane->globalGridAttributes().isGridVisible() ) { for ( int j = 1; j < dgr->numberOfGridRings() + 1; ++j ) { QPointF oldPoint( scaleToRealPosition( QPointF( j * step - qAbs( min ), numberOfSpokes - 1 ), context->rectangle(), destRect, *plane ) ); for ( int i = 0; i < numberOfSpokes ; ++i ) { const QPointF newPoint = scaleToRealPosition( QPointF( j * step - qAbs( min ), i ), context->rectangle(), destRect, *plane ); context->painter()->drawLine( oldPoint, newPoint ); oldPoint = newPoint; context->painter()->drawLine( origin, newPoint ); } } context->painter()->setPen( ta.pen() ); qreal fontSize = 0; for ( int i = 0; i < dgr->numberOfGridRings() + 1; ++i ) { const QString text = QString::number( i * stepWidth ); const QPointF translatedPoint = scaleToRealPosition( QPointF( i * step - qAbs( min ), 0 ), context->rectangle(), destRect, *plane ); const QFontMetrics metric( ta.font()/*QFont( "Arial", 10 )*/ ); - const qreal textLength = metric.width( text ); + const qreal textLength = metric.boundingRect( text ).width(); const qreal textHeight = metric.height() / 2.0; QPointF textOffset( textLength, -textHeight ); textOffset = scaleToRect( textOffset, context->rectangle(), destRect ); QPointF _topLeft = topLeft; _topLeft.setY( translatedPoint.y() ); QRectF boundary( _topLeft, ( translatedPoint + QPointF( 0, step / 2.0 ) ) ); const qreal calcFontSize = fitFontSizeToGeometry( text, ta.font(), boundary, ta ); if ( fontSize != calcFontSize ) { QFont paintFont( ta.font() ); paintFont.setPointSizeF( calcFontSize ); ta.setFont( paintFont ); ta.setFontSize( calcFontSize ); const qreal textHeight2 = QFontMetricsF( paintFont ).height() / 2.0; textOffset.setY( - textHeight2 ); textOffset = scaleToRect( textOffset, context->rectangle(), destRect ); context->painter()->setFont( paintFont ); fontSize = calcFontSize; } context->painter()->drawText( translatedPoint + destRect.topLeft() - textOffset, text ); } } plane->setTextAttributes( ta ); context->painter()->setPen ( PrintingParameters::scalePen( QColor ( Qt::lightGray ) ) ); context->painter()->setBrush( backupBrush ); } diff --git a/src/KGantt/kganttdatetimegrid.cpp b/src/KGantt/kganttdatetimegrid.cpp index 91bd67b..46c48ab 100644 --- a/src/KGantt/kganttdatetimegrid.cpp +++ b/src/KGantt/kganttdatetimegrid.cpp @@ -1,1396 +1,1396 @@ /* * 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 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, QVariant::fromValue(st), StartTimeRole ) && model()->setData( idx, QVariant::fromValue(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 tabw = QApplication::fontMetrics().boundingRect( QLatin1String( "XXXXX" ) ).width(); 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 tabw = QApplication::fontMetrics().boundingRect( QLatin1String( "XXXXX" ) ).width(); 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(date); if (d->timeLine->options() & DateTimeTimeLine::Background) { d->drawTimeLine(painter, rect); } } /*! Draw the foreground for a day. */ void DateTimeGrid::drawDayForeground(QPainter* painter, const QRectF& rect, const QDate& date) { Q_UNUSED(date); if (d->timeLine->options() & DateTimeTimeLine::Foreground) { d->drawTimeLine(painter, rect); } } /** 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(); } /** * @return the timeline control object */ DateTimeTimeLine *DateTimeGrid::timeLine() const { return d->timeLine; } void DateTimeGrid::Private::drawTimeLine(QPainter* painter, const QRectF& rect) { qreal x = dateTimeToChartX(timeLine->dateTime()); if (rect.contains(x, rect.top())) { painter->save(); painter->setPen(timeLine->pen()); painter->drawLine(x, rect.top(), x, rect.bottom()); painter->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" diff --git a/src/KGantt/kganttgraphicsscene.cpp b/src/KGantt/kganttgraphicsscene.cpp index 9b171b9..e3c4d77 100644 --- a/src/KGantt/kganttgraphicsscene.cpp +++ b/src/KGantt/kganttgraphicsscene.cpp @@ -1,954 +1,954 @@ /* * 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 "kganttgraphicsscene.h" #include "kganttgraphicsscene_p.h" #include "kganttgraphicsitem.h" #include "kganttconstraint.h" #include "kganttconstraintgraphicsitem.h" #include "kganttitemdelegate.h" #include "kganttabstractrowcontroller.h" #include "kganttdatetimegrid.h" #include "kganttsummaryhandlingproxymodel.h" #include #include #include #include #include #include #include #include #include #include #include // defines HAVE_PRINTER if support for printing should be included #ifdef _WIN32_WCE // There is no printer support under wince even if QT_NO_PRINTER is not set #else #ifndef QT_NO_PRINTER #define HAVE_PRINTER #endif #endif /*!\class KGantt::GraphicsScene * \internal */ using namespace KGantt; GraphicsScene::Private::Private( GraphicsScene* _q ) : q( _q ), dragSource( nullptr ), itemDelegate( new ItemDelegate( _q ) ), rowController( nullptr ), grid( &default_grid ), readOnly( false ), isPrinting( false ), drawColumnLabels( true ), labelsWidth( 0.0 ), summaryHandlingModel( new SummaryHandlingProxyModel( _q ) ), selectionModel( nullptr ) { default_grid.setStartDateTime( QDateTime::currentDateTime().addDays( -1 ) ); } void GraphicsScene::Private::clearConstraintItems() { for(ConstraintGraphicsItem *citem : constraintItems) { // remove constraint from items first for(GraphicsItem *item : items) { item->removeStartConstraint(citem); item->removeEndConstraint(citem); } q->removeItem(citem); delete citem; } constraintItems.clear(); } void GraphicsScene::Private::resetConstraintItems() { clearConstraintItems(); if ( constraintModel.isNull() ) return; QList clst = constraintModel->constraints(); Q_FOREACH( const Constraint& c, clst ) { createConstraintItem( c ); } q->updateItems(); } void GraphicsScene::Private::createConstraintItem( const Constraint& c ) { GraphicsItem* sitem = q->findItem( summaryHandlingModel->mapFromSource( c.startIndex() ) ); GraphicsItem* eitem = q->findItem( summaryHandlingModel->mapFromSource( c.endIndex() ) ); if ( sitem && eitem ) { ConstraintGraphicsItem* citem = new ConstraintGraphicsItem( c ); sitem->addStartConstraint( citem ); eitem->addEndConstraint( citem ); constraintItems.append( citem ); q->addItem( citem ); } //q->insertConstraintItem( c, citem ); } // Delete the constraint item, and clean up pointers in the start- and end item void GraphicsScene::Private::deleteConstraintItem( ConstraintGraphicsItem *citem ) { //qDebug()<<"GraphicsScene::Private::deleteConstraintItem citem="<constraint(); GraphicsItem* item = items.value( summaryHandlingModel->mapFromSource( c.startIndex() ), nullptr ); if ( item ) { item->removeStartConstraint( citem ); } item = items.value( summaryHandlingModel->mapFromSource( c.endIndex() ), nullptr ); if ( item ) { item->removeEndConstraint( citem ); } constraintItems.removeAt(constraintItems.indexOf(citem)); delete citem; } void GraphicsScene::Private::deleteConstraintItem( const Constraint& c ) { deleteConstraintItem( findConstraintItem( c ) ); } ConstraintGraphicsItem* GraphicsScene::Private::findConstraintItem( const Constraint& c ) const { GraphicsItem* item = items.value( summaryHandlingModel->mapFromSource( c.startIndex() ), nullptr ); if ( item ) { const QList clst = item->startConstraints(); QList::const_iterator it = clst.begin(); for ( ; it != clst.end() ; ++it ) { if ( c.compareIndexes((*it)->constraint()) ) break; } if ( it != clst.end() ) { return *it; } } item = items.value( summaryHandlingModel->mapFromSource( c.endIndex() ), nullptr ); if ( item ) { const QList clst = item->endConstraints(); QList::const_iterator it = clst.begin(); for ( ; it != clst.end() ; ++it ) { if ( c.compareIndexes( (*it)->constraint() ) ) break; } if ( it != clst.end() ) { return *it; } } return nullptr; } // NOTE: we might get here after indexes are invalidated, so cannot do any controlled cleanup void GraphicsScene::Private::clearItems() { for(GraphicsItem *item : items) { q->removeItem(item); delete item; } items.clear(); // do last to avoid cleaning up items clearConstraintItems(); } GraphicsScene::GraphicsScene( QObject* parent ) : QGraphicsScene( parent ), _d( new Private( this ) ) { init(); } GraphicsScene::~GraphicsScene() { qDeleteAll( items() ); delete _d; } #define d d_func() void GraphicsScene::init() { setItemIndexMethod( QGraphicsScene::NoIndex ); setConstraintModel( new ConstraintModel( this ) ); connect( d->grid, SIGNAL(gridChanged()), this, SLOT(slotGridChanged()) ); } /* NOTE: The delegate should really be a property * of the view, but that doesn't really fit at * this time */ void GraphicsScene::setItemDelegate( ItemDelegate* delegate ) { if ( !d->itemDelegate.isNull() && d->itemDelegate->parent()==this ) delete d->itemDelegate; d->itemDelegate = delegate; update(); } ItemDelegate* GraphicsScene::itemDelegate() const { return d->itemDelegate; } QAbstractItemModel* GraphicsScene::model() const { assert(!d->summaryHandlingModel.isNull()); return d->summaryHandlingModel->sourceModel(); } void GraphicsScene::setModel( QAbstractItemModel* model ) { assert(!d->summaryHandlingModel.isNull()); d->summaryHandlingModel->setSourceModel(model); d->grid->setModel( d->summaryHandlingModel ); setSelectionModel( new QItemSelectionModel( model, this ) ); } QAbstractProxyModel* GraphicsScene::summaryHandlingModel() const { return d->summaryHandlingModel; } void GraphicsScene::setSummaryHandlingModel( QAbstractProxyModel* proxyModel ) { proxyModel->setSourceModel( model() ); d->summaryHandlingModel = proxyModel; } void GraphicsScene::setRootIndex( const QModelIndex& idx ) { d->grid->setRootIndex( idx ); } QModelIndex GraphicsScene::rootIndex() const { return d->grid->rootIndex(); } ConstraintModel* GraphicsScene::constraintModel() const { return d->constraintModel; } void GraphicsScene::setConstraintModel( ConstraintModel* cm ) { if ( !d->constraintModel.isNull() ) { d->constraintModel->disconnect( this ); d->clearConstraintItems(); } d->constraintModel = cm; connect( cm, SIGNAL(constraintAdded(KGantt::Constraint)), this, SLOT(slotConstraintAdded(KGantt::Constraint)) ); connect( cm, SIGNAL(constraintRemoved(KGantt::Constraint)), this, SLOT(slotConstraintRemoved(KGantt::Constraint)) ); d->resetConstraintItems(); } void GraphicsScene::setSelectionModel( QItemSelectionModel* smodel ) { d->selectionModel = smodel; // TODO: update selection from model and connect signals } QItemSelectionModel* GraphicsScene::selectionModel() const { return d->selectionModel; } void GraphicsScene::setRowController( AbstractRowController* rc ) { d->rowController = rc; } AbstractRowController* GraphicsScene::rowController() const { return d->rowController; } void GraphicsScene::setGrid( AbstractGrid* grid ) { QAbstractItemModel* model = nullptr; if ( grid == nullptr ) grid = &d->default_grid; if ( d->grid ) { d->grid->disconnect( this ); model = d->grid->model(); } d->grid = grid; connect( d->grid, SIGNAL(gridChanged()), this, SLOT(slotGridChanged()) ); d->grid->setModel( model ); slotGridChanged(); } AbstractGrid* GraphicsScene::grid() const { return d->grid; } void GraphicsScene::setReadOnly( bool ro ) { d->readOnly = ro; } bool GraphicsScene::isReadOnly() const { return d->readOnly; } /* Returns the index with column=0 fromt the * same row as idx and with the same parent. * This is used to traverse the tree-structure * of the model */ QModelIndex GraphicsScene::mainIndex( const QModelIndex& idx ) { #if 0 if ( idx.isValid() ) { return idx.model()->index( idx.row(), 0,idx.parent() ); } else { return QModelIndex(); } #else return idx; #endif } /*! Returns the index pointing to the last column * in the same row as idx. This can be thought of * as in "inverse" of mainIndex() */ QModelIndex GraphicsScene::dataIndex( const QModelIndex& idx ) { #if 0 if ( idx.isValid() ) { const QAbstractItemModel* model = idx.model(); return model->index( idx.row(), model->columnCount( idx.parent() )-1,idx.parent() ); } else { return QModelIndex(); } #else return idx; #endif } /*! Creates a new item of type type. * TODO: If the user should be allowed to override * this in any way, it needs to be in View! */ GraphicsItem* GraphicsScene::createItem( ItemType type ) const { #if 0 switch ( type ) { case TypeEvent: return 0; case TypeTask: return new TaskItem; case TypeSummary: return new SummaryItem; default: return 0; } #endif //qDebug() << "GraphicsScene::createItem("<findItem( idx ); const int itemtype = summaryHandlingModel->data( idx, ItemTypeRole ).toInt(); if (!item) { item = q->createItem( static_cast( itemtype ) ); item->setIndex( idx ); q->insertItem( idx, item); } item->updateItem( span, idx ); QModelIndex child; int cr = 0; while ( ( child = idx.child( cr, 0 ) ).isValid() ) { recursiveUpdateMultiItem( span, child ); ++cr; } } void GraphicsScene::updateRow( const QModelIndex& rowidx ) { //qDebug() << "GraphicsScene::updateRow("<mapToSource( rowidx ); Span rg = rowController()->rowGeometry( sidx ); for ( QModelIndex treewalkidx = sidx; treewalkidx.isValid(); treewalkidx = treewalkidx.parent() ) { if ( treewalkidx.data( ItemTypeRole ).toInt() == TypeMulti && !rowController()->isRowExpanded( treewalkidx )) { rg = rowController()->rowGeometry( treewalkidx ); } } bool blocked = blockSignals( true ); for ( int col = 0; col < summaryHandlingModel()->columnCount( rowidx.parent() ); ++col ) { const QModelIndex idx = summaryHandlingModel()->index( rowidx.row(), col, rowidx.parent() ); const QModelIndex sidx = summaryHandlingModel()->mapToSource( idx ); const int itemtype = summaryHandlingModel()->data( idx, ItemTypeRole ).toInt(); const bool isExpanded = rowController()->isRowExpanded( sidx ); if ( itemtype == TypeNone ) { removeItem( idx ); continue; } if ( itemtype == TypeMulti && !isExpanded ) { d->recursiveUpdateMultiItem( rg, idx ); } else { if ( summaryHandlingModel()->data( rowidx.parent(), ItemTypeRole ).toInt() == TypeMulti && !isExpanded ) { //continue; } GraphicsItem* item = findItem( idx ); if (!item) { item = createItem( static_cast( itemtype ) ); item->setIndex( idx ); insertItem(idx, item); } const Span span = rowController()->rowGeometry( sidx ); item->updateItem( span, idx ); } } blockSignals( blocked ); } void GraphicsScene::insertItem( const QPersistentModelIndex& idx, GraphicsItem* item ) { if ( !d->constraintModel.isNull() ) { // Create items for constraints const QModelIndex sidx = summaryHandlingModel()->mapToSource( idx ); const QList clst = d->constraintModel->constraintsForIndex( sidx ); Q_FOREACH( const Constraint& c, clst ) { QModelIndex other_idx; if ( c.startIndex() == sidx ) { other_idx = c.endIndex(); GraphicsItem* other_item = d->items.value(summaryHandlingModel()->mapFromSource( other_idx ),nullptr); if ( !other_item ) continue; ConstraintGraphicsItem* citem = new ConstraintGraphicsItem( c ); item->addStartConstraint( citem ); other_item->addEndConstraint( citem ); d->constraintItems.append( citem ); addItem( citem ); } else if ( c.endIndex() == sidx ) { other_idx = c.startIndex(); GraphicsItem* other_item = d->items.value(summaryHandlingModel()->mapFromSource( other_idx ),nullptr); if ( !other_item ) continue; ConstraintGraphicsItem* citem = new ConstraintGraphicsItem( c ); other_item->addStartConstraint( citem ); item->addEndConstraint( citem ); d->constraintItems.append( citem ); addItem( citem ); } else { assert( 0 ); // Impossible } } } d->items.insert( idx, item ); addItem( item ); } void GraphicsScene::removeItem( const QModelIndex& idx ) { //qDebug() << "GraphicsScene::removeItem("<::iterator it = d->items.find( idx ); if ( it != d->items.end() ) { GraphicsItem* item = *it; assert( item ); // We have to remove the item from the list first because // there is a good chance there will be reentrant calls d->items.erase( it ); { // Remove any constraintitems attached const QSet clst = QSet::fromList( item->startConstraints() ) + QSet::fromList( item->endConstraints() ); Q_FOREACH( ConstraintGraphicsItem* citem, clst ) { d->deleteConstraintItem( citem ); } } // Get rid of the item delete item; } } GraphicsItem* GraphicsScene::findItem( const QModelIndex& idx ) const { if ( !idx.isValid() ) return nullptr; assert( idx.model() == summaryHandlingModel() ); QHash::const_iterator it = d->items.find( idx ); return ( it != d->items.end() )?*it:nullptr; } GraphicsItem* GraphicsScene::findItem( const QPersistentModelIndex& idx ) const { if ( !idx.isValid() ) return nullptr; assert( idx.model() == summaryHandlingModel() ); QHash::const_iterator it = d->items.find( idx ); return ( it != d->items.end() )?*it:nullptr; } void GraphicsScene::clearItems() { d->clearItems(); } void GraphicsScene::updateItems() { for ( QHash::iterator it = d->items.begin(); it != d->items.end(); ++it ) { GraphicsItem* const item = it.value(); const QPersistentModelIndex& idx = it.key(); item->updateItem( Span( item->pos().y(), item->rect().height() ), idx ); } invalidate( QRectF(), QGraphicsScene::BackgroundLayer ); } void GraphicsScene::deleteSubtree( const QModelIndex& _idx ) { QModelIndex idx = dataIndex( _idx ); if ( !idx.model() ) return; const QModelIndex parent( idx.parent() ); const int colcount = idx.model()->columnCount( parent ); {for ( int i = 0; i < colcount; ++i ) { removeItem( parent.child( idx.row(), i ) ); }} const int rowcount = summaryHandlingModel()->rowCount( _idx ); {for ( int i = 0; i < rowcount; ++i ) { deleteSubtree( summaryHandlingModel()->index( i, summaryHandlingModel()->columnCount(_idx)-1, _idx ) ); }} } ConstraintGraphicsItem* GraphicsScene::findConstraintItem( const Constraint& c ) const { return d->findConstraintItem( c ); } void GraphicsScene::slotConstraintAdded( const KGantt::Constraint& c ) { d->createConstraintItem( c ); } void GraphicsScene::slotConstraintRemoved( const KGantt::Constraint& c ) { d->deleteConstraintItem( c ); } void GraphicsScene::slotGridChanged() { updateItems(); update(); emit gridChanged(); } void GraphicsScene::helpEvent( QGraphicsSceneHelpEvent *helpEvent ) { #ifndef QT_NO_TOOLTIP QGraphicsItem *item = itemAt( helpEvent->scenePos(), QTransform() ); if ( GraphicsItem* gitem = qgraphicsitem_cast( item ) ) { QToolTip::showText(helpEvent->screenPos(), gitem->ganttToolTip()); } else if ( ConstraintGraphicsItem* citem = qgraphicsitem_cast( item ) ) { QToolTip::showText(helpEvent->screenPos(), citem->ganttToolTip()); } else { QGraphicsScene::helpEvent( helpEvent ); } #endif /* QT_NO_TOOLTIP */ } void GraphicsScene::drawBackground( QPainter* painter, const QRectF& _rect ) { QRectF scn( sceneRect() ); QRectF rect( _rect ); if ( d->isPrinting && d->drawColumnLabels ) { QRectF headerRect( scn.topLeft()+QPointF( d->labelsWidth, 0 ), QSizeF( scn.width()-d->labelsWidth, d->rowController->headerHeight() )); d->grid->paintHeader( painter, headerRect, rect, 0, nullptr ); #if 0 /* We have to blank out the part of the header that is invisible during * normal rendering when we are printing. */ QRectF labelsTabRect( scn.topLeft(), QSizeF( d->labelsWidth, headerRect.height() ) ); QStyleOptionHeader opt; opt.rect = labelsTabRect.toRect(); opt.text = QLatin1String(""); opt.textAlignment = Qt::AlignCenter; style()->drawControl(QStyle::CE_Header, &opt, painter, 0); #endif scn.setTop( headerRect.bottom() ); scn.setLeft( headerRect.left() ); rect = rect.intersected( scn ); } d->grid->paintGrid( painter, scn, rect, d->rowController ); d->grid->drawBackground(painter, rect); } void GraphicsScene::drawForeground( QPainter* painter, const QRectF& rect ) { d->grid->drawForeground(painter, rect); } void GraphicsScene::itemEntered( const QModelIndex& idx ) { emit entered( idx ); } void GraphicsScene::itemPressed( const QModelIndex& idx ) { emit pressed( idx ); } void GraphicsScene::itemClicked( const QModelIndex& idx ) { emit clicked( idx ); } void GraphicsScene::itemDoubleClicked( const QModelIndex& idx ) { emit qrealClicked( idx ); } void GraphicsScene::setDragSource( GraphicsItem* item ) { d->dragSource = item; } GraphicsItem* GraphicsScene::dragSource() const { return d->dragSource; } /*! Print the Gantt chart using \a printer. If \a drawRowLabels * is true (the default), each row will have it's label printed * on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. * * This version of print() will print multiple pages. */ void GraphicsScene::print( QPrinter* printer, bool drawRowLabels, bool drawColumnLabels ) { #ifndef HAVE_PRINTER Q_UNUSED( printer ); Q_UNUSED( drawRowLabels ); Q_UNUSED( drawColumnLabels ); #else QPainter painter( printer ); doPrint( &painter, printer->pageRect(), sceneRect().left(), sceneRect().right(), printer, drawRowLabels, drawColumnLabels ); #endif } /*! Print part of the Gantt chart from \a start to \a end using \a printer. * If \a drawRowLabels is true (the default), each row will have it's * label printed on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. * * This version of print() will print multiple pages. * * To print a certain range of a chart with a DateTimeGrid, use * qreal DateTimeGrid::mapFromDateTime( const QDateTime& dt) const * to figure out the values for \a start and \a end. */ void GraphicsScene::print( QPrinter* printer, qreal start, qreal end, bool drawRowLabels, bool drawColumnLabels ) { #ifndef HAVE_PRINTER Q_UNUSED( printer ); Q_UNUSED( start ); Q_UNUSED( end ); Q_UNUSED( drawRowLabels ); Q_UNUSED( drawColumnLabels ); #else QPainter painter( printer ); doPrint( &painter, printer->pageRect(), start, end, printer, drawRowLabels, drawColumnLabels ); #endif } /*! Render the GanttView inside the rectangle \a target using the painter \a painter. * If \a drawRowLabels is true (the default), each row will have it's * label printed on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. */ void GraphicsScene::print( QPainter* painter, const QRectF& _targetRect, bool drawRowLabels, bool drawColumnLabels ) { QRectF targetRect( _targetRect ); if ( targetRect.isNull() ) { targetRect = sceneRect(); } doPrint( painter, targetRect, sceneRect().left(), sceneRect().right(), nullptr, drawRowLabels, drawColumnLabels ); } /*! Render the GanttView inside the rectangle \a target using the painter \a painter. * If \a drawRowLabels is true (the default), each row will have it's * label printed on the left side. If \a drawColumnLabels is true (the * default), each column will have it's label printed at the * top side. * * To print a certain range of a chart with a DateTimeGrid, use * qreal DateTimeGrid::mapFromDateTime( const QDateTime& dt) const * to figure out the values for \a start and \a end. */ void GraphicsScene::print( QPainter* painter, qreal start, qreal end, const QRectF& _targetRect, bool drawRowLabels, bool drawColumnLabels ) { QRectF targetRect( _targetRect ); if ( targetRect.isNull() ) { targetRect = sceneRect(); } doPrint( painter, targetRect, start, end, nullptr, drawRowLabels, drawColumnLabels ); } /*!\internal */ void GraphicsScene::doPrint( QPainter* painter, const QRectF& targetRect, qreal start, qreal end, QPrinter* printer, bool drawRowLabels, bool drawColumnLabels ) { assert( painter ); d->isPrinting = true; d->drawColumnLabels = drawColumnLabels; d->labelsWidth = 0.0; QFont sceneFont( font() ); #ifdef HAVE_PRINTER if ( printer ) { sceneFont = QFont( font(), printer ); if ( font().pointSizeF() >= 0.0 ) sceneFont.setPointSizeF( font().pointSizeF() ); else if ( font().pointSize() >= 0 ) sceneFont.setPointSize( font().pointSize() ); else sceneFont.setPixelSize( font().pixelSize() ); } #endif QGraphicsTextItem dummyTextItem( QLatin1String("X") ); dummyTextItem.adjustSize(); QFontMetrics fm(dummyTextItem.font()); sceneFont.setPixelSize( fm.height() ); const QRectF oldScnRect( sceneRect() ); QRectF scnRect( oldScnRect ); scnRect.setLeft( start ); scnRect.setRight( end ); bool b = blockSignals( true ); /* column labels */ if ( d->drawColumnLabels ) { QRectF headerRect( scnRect ); headerRect.setHeight( - d->rowController->headerHeight() ); scnRect.setTop(scnRect.top() - d->rowController->headerHeight()); } /* row labels */ QVector textLabels; if ( drawRowLabels ) { qreal textWidth = 0.; - qreal charWidth = QFontMetricsF(sceneFont).width( QString::fromLatin1( "X" ) ); + qreal charWidth = QFontMetricsF(sceneFont).boundingRect( QString::fromLatin1( "X" ) ).width(); QModelIndex sidx = summaryHandlingModel()->mapToSource( summaryHandlingModel()->index( 0, 0, rootIndex()) ); do { QModelIndex idx = summaryHandlingModel()->mapFromSource( sidx ); const Span rg=rowController()->rowGeometry( sidx ); const QString txt = idx.data( Qt::DisplayRole ).toString(); QGraphicsTextItem* item = new QGraphicsTextItem( txt ); addItem( item ); textLabels << item; - item->setTextWidth( QFontMetricsF(sceneFont).width( txt ) + charWidth ); + item->setTextWidth( QFontMetricsF(sceneFont).boundingRect( txt ).width() + charWidth ); textWidth = qMax( item->textWidth(), textWidth ); item->setPos( 0, rg.start() ); } while ( ( sidx = rowController()->indexBelow( sidx ) ).isValid() ); Q_FOREACH( QGraphicsTextItem* item, textLabels ) { item->setPos( scnRect.left()-textWidth, item->y() ); item->show(); } scnRect.setLeft( scnRect.left()-textWidth ); d->labelsWidth = textWidth; } setSceneRect( scnRect ); painter->save(); painter->setClipRect( targetRect ); qreal yratio = targetRect.height()/scnRect.height(); /* If we're not printing multiple pages, * check if the span fits and adjust: */ if ( !printer && targetRect.width()/scnRect.width() < yratio ) { yratio = targetRect.width()/scnRect.width(); } qreal offset = scnRect.left(); int pagecount = 0; while ( offset < scnRect.right() ) { painter->setFont( sceneFont ); render( painter, targetRect, QRectF( QPointF( offset, scnRect.top()), QSizeF( targetRect.width()/yratio, scnRect.height() ) ) ); offset += targetRect.width()/yratio; ++pagecount; if ( printer && offset < scnRect.right() ) { #ifdef HAVE_PRINTER printer->newPage(); #endif } else { break; } } d->isPrinting = false; d->drawColumnLabels = true; d->labelsWidth = 0.0; qDeleteAll( textLabels ); blockSignals( b ); setSceneRect( oldScnRect ); painter->restore(); } #include "moc_kganttgraphicsscene.cpp" #ifndef KDAB_NO_UNIT_TESTS #include "unittest/test.h" #include #include #include #include "kganttgraphicsview.h" class SceneTestRowController : public KGantt::AbstractRowController { private: static const int ROW_HEIGHT; QPointer m_model; public: SceneTestRowController() { } void setModel( QAbstractItemModel* model ) { m_model = model; } /*reimp*/int headerHeight() const Q_DECL_OVERRIDE { return 40; } /*reimp*/ bool isRowVisible( const QModelIndex& ) const Q_DECL_OVERRIDE { return true;} /*reimp*/ bool isRowExpanded( const QModelIndex& ) const Q_DECL_OVERRIDE { return false; } /*reimp*/ KGantt::Span rowGeometry( const QModelIndex& idx ) const Q_DECL_OVERRIDE { return KGantt::Span( idx.row() * ROW_HEIGHT, ROW_HEIGHT ); } /*reimp*/ int maximumItemHeight() const Q_DECL_OVERRIDE { return ROW_HEIGHT/2; } /*reimp*/int totalHeight() const Q_DECL_OVERRIDE { return m_model->rowCount()* ROW_HEIGHT; } /*reimp*/ QModelIndex indexAt( int height ) const Q_DECL_OVERRIDE { return m_model->index( height/ROW_HEIGHT, 0 ); } /*reimp*/ QModelIndex indexBelow( const QModelIndex& idx ) const Q_DECL_OVERRIDE { if ( !idx.isValid() )return QModelIndex(); return idx.model()->index( idx.row()+1, idx.column(), idx.parent() ); } /*reimp*/ QModelIndex indexAbove( const QModelIndex& idx ) const Q_DECL_OVERRIDE { if ( !idx.isValid() )return QModelIndex(); return idx.model()->index( idx.row()-1, idx.column(), idx.parent() ); } }; class TestLineItem : public QGraphicsLineItem { public: TestLineItem( bool *destroyedFlag ) : QGraphicsLineItem( 0, 0, 10, 10 ), // geometry doesn't matter m_destroyedFlag( destroyedFlag ) {} ~TestLineItem() { *m_destroyedFlag = true; } private: bool *m_destroyedFlag; }; const int SceneTestRowController::ROW_HEIGHT = 30; KDAB_SCOPED_UNITTEST_SIMPLE( KGantt, GraphicsView, "test" ) { QStandardItemModel model; QStandardItem* item = new QStandardItem(); item->setData( KGantt::TypeTask, KGantt::ItemTypeRole ); item->setData( QString::fromLatin1( "Decide on new product" ) ); item->setData( QDateTime( QDate( 2007, 3, 1 ) ), KGantt::StartTimeRole ); item->setData( QDateTime( QDate( 2007, 3, 3 ) ), KGantt::EndTimeRole ); QStandardItem* item2 = new QStandardItem(); item2->setData( KGantt::TypeTask, KGantt::ItemTypeRole ); item2->setData( QString::fromLatin1( "Educate personnel" ) ); item2->setData( QDateTime( QDate( 2007, 3, 3 ) ), KGantt::StartTimeRole ); item2->setData( QDateTime( QDate( 2007, 3, 6 ) ), KGantt::EndTimeRole ); model.appendRow( item ); model.appendRow( item2 ); SceneTestRowController rowController; rowController.setModel( &model ); KGantt::GraphicsView graphicsView; graphicsView.setRowController( &rowController ); graphicsView.setModel( &model ); // Now the interesting stuff - the items above are just for a "realistic environment" bool foreignItemDestroyed = false; TestLineItem *foreignItem = new TestLineItem( &foreignItemDestroyed ); graphicsView.scene()->addItem( foreignItem ); assertFalse( foreignItemDestroyed ); graphicsView.updateScene(); assertFalse( foreignItemDestroyed ); } #endif /* KDAB_NO_UNIT_TESTS */ diff --git a/src/KGantt/kganttitemdelegate.cpp b/src/KGantt/kganttitemdelegate.cpp index f4a130e..ef6fde1 100644 --- a/src/KGantt/kganttitemdelegate.cpp +++ b/src/KGantt/kganttitemdelegate.cpp @@ -1,630 +1,630 @@ /* * 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 "kganttitemdelegate_p.h" #include "kganttglobal.h" #include "kganttstyleoptionganttitem.h" #include "kganttconstraint.h" #include #include #include #include #include #include #ifndef QT_NO_DEBUG_STREAM #define PRINT_INTERACTIONSTATE(x) \ case x: dbg << #x; break; QDebug operator<<( QDebug dbg, KGantt::ItemDelegate::InteractionState state ) { switch ( state ) { PRINT_INTERACTIONSTATE( KGantt::ItemDelegate::State_None ); PRINT_INTERACTIONSTATE( KGantt::ItemDelegate::State_Move ); PRINT_INTERACTIONSTATE( KGantt::ItemDelegate::State_ExtendLeft ); PRINT_INTERACTIONSTATE( KGantt::ItemDelegate::State_ExtendRight ); default: break; } return dbg; } #undef PRINT_INTERACTIONSTATE #endif /* QT_NO_DEBUG_STREAM */ using namespace KGantt; /*!\class KGantt::ItemDelegate kganttitemdelegate.h KGanttItemDelegate *\ingroup KGantt *\brief Class used to render gantt items in a KGantt::GraphicsView * */ /*!\enum KGantt::ItemDelegate::InteractionState * This enum is used for communication between the view and * the delegate about user interaction with gantt items. * * \see KGantt::ItemDelegate::interactionStateFor */ ItemDelegate::Private::Private() { // Brushes QLinearGradient taskgrad( 0., 0., 0., QApplication::fontMetrics().height() ); taskgrad.setColorAt( 0., Qt::green ); taskgrad.setColorAt( 1., Qt::darkGreen ); QLinearGradient summarygrad( 0., 0., 0., QApplication::fontMetrics().height() ); summarygrad.setColorAt( 0., Qt::blue ); summarygrad.setColorAt( 1., Qt::darkBlue ); QLinearGradient eventgrad( 0., 0., 0., QApplication::fontMetrics().height() ); eventgrad.setColorAt( 0., Qt::red ); eventgrad.setColorAt( 1., Qt::darkRed ); defaultbrush[TypeTask] = taskgrad; defaultbrush[TypeSummary] = summarygrad; defaultbrush[TypeEvent] = eventgrad; // Pens QPen pen( QGuiApplication::palette().windowText(), 1. ); defaultpen[TypeTask] = pen; defaultpen[TypeSummary] = pen; defaultpen[TypeEvent] = pen; } QPen ItemDelegate::Private::constraintPen( const QPointF& start, const QPointF& end, const Constraint& constraint ) { QPen pen; QVariant dataPen; // Use default pens... if ( start.x() <= end.x() ) { pen = QPen( Qt::black ); dataPen = constraint.data( Constraint::ValidConstraintPen ); } else { pen = QPen( Qt::red ); dataPen = constraint.data( Constraint::InvalidConstraintPen ); } // ... unless constraint.data() returned a valid pen for this case if ( dataPen.canConvert( QVariant::Pen ) ) { pen = dataPen.value< QPen >(); } return pen; } /*! Constructor. Creates an ItemDelegate with parent \a parent */ ItemDelegate::ItemDelegate( QObject* parent ) : QItemDelegate( parent ), _d( new Private ) { } /*! Destructor */ ItemDelegate::~ItemDelegate() { delete _d; } #define d d_func() /*! Sets the default brush used for items of type \a type to * \a brush. The default brush is used in the case when the model * does not provide an explicit brush. * * \todo Move this to GraphicsView to make delegate stateless. */ void ItemDelegate::setDefaultBrush( ItemType type, const QBrush& brush ) { d->defaultbrush[type] = brush; } /*!\returns The default brush for item type \a type * * \todo Move this to GraphicsView to make delegate stateless. */ QBrush ItemDelegate::defaultBrush( ItemType type ) const { return d->defaultbrush[type]; } /*! Sets the default pen used for items of type \a type to * \a pen. The default pen is used in the case when the model * does not provide an explicit pen. * * \todo Move this to GraphicsView to make delegate stateless. */ void ItemDelegate::setDefaultPen( ItemType type, const QPen& pen ) { d->defaultpen[type]=pen; } /*!\returns The default pen for item type \a type * * \todo Move this to GraphicsView to make delegate stateless. */ QPen ItemDelegate::defaultPen( ItemType type ) const { return d->defaultpen[type]; } /*!\returns The tooltip for index \a idx */ QString ItemDelegate::toolTip( const QModelIndex &idx ) const { if ( !idx.isValid() ) return QString(); const QAbstractItemModel* model = idx.model(); if ( !model ) return QString(); QString tip = model->data( idx, Qt::ToolTipRole ).toString(); if ( !tip.isNull() ) return tip; else return tr( "%1 -> %2: %3", "start time -> end time: item name" ) .arg( model->data( idx, StartTimeRole ).toString() ) .arg( model->data( idx, EndTimeRole ).toString() ) .arg( model->data( idx, Qt::DisplayRole ).toString() ); } /*! \returns The bounding Span for the item identified by \a idx * when rendered with options \a opt. This is often the same as the * span given by the AbstractGrid for \a idx, but it might be larger * in case there are additional texts or decorations on the item. * * Override this to implement new itemtypes or to change the look * of the existing ones. */ Span ItemDelegate::itemBoundingSpan( const StyleOptionGanttItem& opt, const QModelIndex& idx ) const { if ( !idx.isValid() ) return Span(); const QString txt = idx.model()->data( idx, Qt::DisplayRole ).toString(); const int typ = idx.model()->data( idx, ItemTypeRole ).toInt(); QRectF itemRect = opt.itemRect; if ( typ == TypeEvent ) { itemRect = QRectF( itemRect.left()-itemRect.height()/2., itemRect.top(), itemRect.height(), itemRect.height() ); } - int tw = opt.fontMetrics.width( txt ); + int tw = opt.fontMetrics.boundingRect( txt ).width(); tw += static_cast( itemRect.height()/2. ); Span s; switch ( opt.displayPosition ) { case StyleOptionGanttItem::Left: s = Span( itemRect.left()-tw, itemRect.width()+tw ); break; case StyleOptionGanttItem::Right: s = Span( itemRect.left(), itemRect.width()+tw ); break; case StyleOptionGanttItem::Hidden: // fall through case StyleOptionGanttItem::Center: s = Span( itemRect.left(), itemRect.width() ); break; } return s; } /*! \returns The interaction state for position \a pos on item \a idx * when rendered with options \a opt. This is used to tell the view * about how the item should react to mouse click/drag. * * Override to implement new items or interactions. */ ItemDelegate::InteractionState ItemDelegate::interactionStateFor( const QPointF& pos, const StyleOptionGanttItem& opt, const QModelIndex& idx ) const { if ( !idx.isValid() ) return State_None; if ( !( idx.model()->flags( idx ) & Qt::ItemIsEditable ) ) return State_None; const int typ = static_cast( idx.model()->data( idx, ItemTypeRole ).toInt() ); QRectF itemRect( opt.itemRect ); // An event item is infinitely thin, basically just a line, because it has only one date instead of two. // It is painted with an offset of -height/2, which is taken into account here. if ( typ == TypeEvent ) itemRect = QRectF( itemRect.topLeft() - QPointF( itemRect.height() / 2.0, 0 ), QSizeF( itemRect.height(), itemRect.height() ) ); if ( typ == TypeNone || typ == TypeSummary ) return State_None; if ( !itemRect.contains(pos) ) return State_None; if ( typ == TypeEvent ) return State_Move; qreal delta = 5.; if ( itemRect.width() < 15 ) delta = 1.; if ( pos.x() >= itemRect.left() && pos.x() < itemRect.left()+delta ) { return State_ExtendLeft; } else if ( pos.x() <= itemRect.right() && pos.x() > itemRect.right()-delta ) { return State_ExtendRight; } else { return State_Move; } } /*! Paints the gantt item \a idx using \a painter and \a opt */ void ItemDelegate::paintGanttItem( QPainter* painter, const StyleOptionGanttItem& opt, const QModelIndex& idx ) { if ( !idx.isValid() ) return; const ItemType typ = static_cast( idx.model()->data( idx, ItemTypeRole ).toInt() ); const QString& txt = opt.text; QRectF itemRect = opt.itemRect; QRectF boundingRect = opt.boundingRect; boundingRect.setY( itemRect.y() ); boundingRect.setHeight( itemRect.height() ); //qDebug() << "itemRect="<constraintPen( start, end, constraint ); painter->setPen( pen ); painter->setBrush( pen.color() ); painter->drawPolyline( finishStartLine( start, end ) ); painter->drawPolygon( finishStartArrow( start, end ) ); } QPolygonF ItemDelegate::finishStartLine( const QPointF& start, const QPointF& end ) const { QPolygonF poly; qreal midx = end.x() - TURN; qreal midy = ( end.y()-start.y() )/2. + start.y(); if ( start.x() > end.x()-TURN ) { poly << start << QPointF( start.x()+TURN, start.y() ) << QPointF( start.x()+TURN, midy ) << QPointF( end.x()-TURN, midy ) << QPointF( end.x()-TURN, end.y() ) << end; } else { poly << start << QPointF( midx, start.y() ) << QPointF( midx, end.y() ) << end; } return poly; } QPolygonF ItemDelegate::finishStartArrow( const QPointF& start, const QPointF& end ) const { Q_UNUSED(start); QPolygonF poly; poly << end << QPointF( end.x()-TURN/2., end.y()-TURN/2. ) << QPointF( end.x()-TURN/2., end.y()+TURN/2. ); return poly; } void ItemDelegate::paintFinishFinishConstraint( QPainter* painter, const QStyleOptionGraphicsItem& opt, const QPointF& start, const QPointF& end, const Constraint &constraint ) { Q_UNUSED( opt ); const QPen pen = d->constraintPen( start, end, constraint ); painter->setPen( pen ); painter->setBrush( pen.color() ); painter->drawPolyline( finishFinishLine( start, end ) ); painter->drawPolygon( finishFinishArrow( start, end ) ); } QPolygonF ItemDelegate::finishFinishLine( const QPointF& start, const QPointF& end ) const { QPolygonF poly; qreal midx = end.x() + TURN; qreal midy = ( end.y()-start.y() )/2. + start.y(); if ( start.x() > end.x()+TURN ) { poly << start << QPointF( start.x()+TURN, start.y() ) << QPointF( start.x()+TURN, end.y() ) << end; } else { poly << start << QPointF( midx, start.y() ) << QPointF( midx, midy ) << QPointF( end.x()+TURN, midy ) << QPointF( end.x()+TURN, end.y() ) << end; } return poly; } QPolygonF ItemDelegate::finishFinishArrow( const QPointF& start, const QPointF& end ) const { Q_UNUSED(start); QPolygonF poly; poly << end << QPointF( end.x()+TURN/2., end.y()-TURN/2. ) << QPointF( end.x()+TURN/2., end.y()+TURN/2. ); return poly; } void ItemDelegate::paintStartStartConstraint( QPainter* painter, const QStyleOptionGraphicsItem& opt, const QPointF& start, const QPointF& end, const Constraint &constraint ) { Q_UNUSED( opt ); const QPen pen = d->constraintPen( start, end, constraint ); painter->setPen( pen ); painter->setBrush( pen.color() ); painter->drawPolyline( startStartLine( start, end ) ); painter->drawPolygon( startStartArrow( start, end ) ); } QPolygonF ItemDelegate::startStartLine( const QPointF& start, const QPointF& end ) const { Q_UNUSED(start); QPolygonF poly; if ( start.x() > end.x() ) { poly << start << QPointF( end.x()-TURN, start.y() ) << QPointF( end.x()-TURN, end.y() ) << end; } else { poly << start << QPointF( start.x()-TURN, start.y() ) << QPointF( start.x()-TURN, end.y() ) << QPointF( end.x()-TURN, end.y() ) << end; } return poly; } QPolygonF ItemDelegate::startStartArrow( const QPointF& start, const QPointF& end ) const { Q_UNUSED(start); QPolygonF poly; poly << end << QPointF( end.x()-TURN/2., end.y()-TURN/2. ) << QPointF( end.x()-TURN/2., end.y()+TURN/2. ); return poly; } void ItemDelegate::paintStartFinishConstraint( QPainter* painter, const QStyleOptionGraphicsItem& opt, const QPointF& start, const QPointF& end, const Constraint &constraint ) { Q_UNUSED( opt ); const QPen pen = d->constraintPen( start, end, constraint ); painter->setPen( pen ); painter->setBrush( pen.color() ); painter->drawPolyline( startFinishLine( start, end ) ); painter->drawPolygon( startFinishArrow( start, end ) ); } QPolygonF ItemDelegate::startFinishLine( const QPointF& start, const QPointF& end ) const { Q_UNUSED(start); QPolygonF poly; qreal midx = end.x() + TURN; qreal midy = ( end.y()-start.y() )/2. + start.y(); if ( start.x()-TURN > end.x()+TURN ) { poly << start << QPointF( midx, start.y() ) << QPointF( midx, end.y() ) << end; } else { poly << start << QPointF( start.x()-TURN, start.y() ) << QPointF( start.x()-TURN, midy ) << QPointF( midx, midy ) << QPointF( end.x()+TURN, end.y() ) << end; } return poly; } QPolygonF ItemDelegate::startFinishArrow( const QPointF& start, const QPointF& end ) const { Q_UNUSED(start); QPolygonF poly; poly << end << QPointF( end.x()+TURN/2., end.y()-TURN/2. ) << QPointF( end.x()+TURN/2., end.y()+TURN/2. ); return poly; } #include "moc_kganttitemdelegate.cpp" diff --git a/src/KGantt/kganttlegend.cpp b/src/KGantt/kganttlegend.cpp index 5ff5503..62e17b6 100644 --- a/src/KGantt/kganttlegend.cpp +++ b/src/KGantt/kganttlegend.cpp @@ -1,208 +1,208 @@ /* * 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 "kganttlegend.h" #include "kganttlegend_p.h" #include "kganttitemdelegate.h" #include #include #include using namespace KGantt; /*!\class KGantt::Legend kganttlegend.h KGanttLegend * \ingroup KGantt * \brief Legend showing an image and a description for Gantt items * * This is an item view class showing a small Gantt item and it's * text defined by LegendRole. */ /*! Constructor. Creates a Legend with parent \a parent. * The QObject parent is not used for anything internally. */ Legend::Legend( QWidget* parent ) : QAbstractItemView( parent ), _d( new Private ) { setItemDelegate( new ItemDelegate( this ) ); setFrameStyle( QFrame::NoFrame ); } /*! Destructor. Does nothing */ Legend::~Legend() { delete _d; } #define d d_func() QModelIndex Legend::indexAt( const QPoint& point ) const { Q_UNUSED( point ); return QModelIndex(); } QRect Legend::visualRect( const QModelIndex& index ) const { Q_UNUSED( index ); return QRect(); } QSize Legend::sizeHint() const { return measureItem( rootIndex() ); } QSize Legend::minimumSizeHint() const { return measureItem( rootIndex() ); } void Legend::setModel( QAbstractItemModel* model ) { if ( this->model() != nullptr ) { disconnect( this->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(modelDataChanged()) ); disconnect( this->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(modelDataChanged()) ); disconnect( this->model(), SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(modelDataChanged()) ); } QAbstractItemView::setModel( model ); d->proxyModel.setSourceModel( model ); if ( this->model() != nullptr ) { connect( this->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(modelDataChanged()) ); connect( this->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(modelDataChanged()) ); connect( this->model(), SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(modelDataChanged()) ); } } /*! Triggers repainting of the legend. */ void Legend::modelDataChanged() { updateGeometry(); viewport()->update(); } void Legend::paintEvent( QPaintEvent* event ) { Q_UNUSED( event ); // no model, no legend... if ( model() == nullptr ) return; QPainter p( viewport() ); p.fillRect( viewport()->rect(), palette().color( QPalette::Window ) ); drawItem( &p, rootIndex() ); } /*! Creates a StyleOptionGanttItem with all style options filled in * except the target rectangles. */ StyleOptionGanttItem Legend::getStyleOption( const QModelIndex& index ) const { StyleOptionGanttItem opt; opt.displayPosition = StyleOptionGanttItem::Right; opt.displayAlignment = Qt::Alignment( d->proxyModel.data( index, Qt::TextAlignmentRole ).toInt() ); opt.text = index.model()->data( index, LegendRole ).toString(); opt.font = ( index.model()->data( index, Qt::FontRole ) ).value< QFont >(); return opt; } /*! Draws the legend item at \a index and all of it's children recursively * at \a pos onto \a painter. * Reimplement this if you want to draw items in an user defined way. * \returns the rectangle drawn. */ QRect Legend::drawItem( QPainter* painter, const QModelIndex& index, const QPoint& pos ) const { int xPos = pos.x(); int yPos = pos.y(); if ( index.isValid() && index.model() == &d->proxyModel ) { ItemDelegate* const delegate = qobject_cast< ItemDelegate* >( itemDelegate( index ) ); assert( delegate != nullptr ); const QRect r( pos, measureItem( index, false ) ); StyleOptionGanttItem opt = getStyleOption( index ); opt.rect = r; opt.rect.setWidth( r.height() ); const ItemType typ = static_cast( index.model()->data( index, ItemTypeRole ).toInt() ); const int dx = (typ == TypeEvent) ? (r.height() / 2) : 0; opt.itemRect = opt.rect.adjusted(dx,0,dx,0); opt.boundingRect = r; opt.boundingRect.setWidth( r.width() + r.height() ); if ( !opt.text.isNull() ) delegate->paintGanttItem( painter, opt, index ); xPos = r.right(); yPos = r.bottom(); } const int rowCount = d->proxyModel.rowCount( index ); for ( int row = 0; row < rowCount; ++row ) { const QRect r = drawItem( painter, d->proxyModel.index( row, 0, index ), QPoint( pos.x(), yPos ) ); xPos = qMax( xPos, r.right() ); yPos = qMax( yPos, r.bottom() ); } return QRect( pos, QPoint( xPos, yPos ) ); } /*! Calculates the needed space for the legend item at \a index and, if \a recursive is true, * all child items. */ QSize Legend::measureItem( const QModelIndex& index, bool recursive ) const { if ( model() == nullptr ) return QSize(); QSize baseSize; if ( index.model() != nullptr ) { QFontMetrics fm( ( index.model()->data( index, Qt::FontRole ) ).value< QFont >() ); const QString text = index.model()->data( index, LegendRole ).toString(); if ( !text.isEmpty() ) - baseSize += QSize( fm.width( text ) + fm.height() + 2, fm.height() + 2 ); + baseSize += QSize( fm.boundingRect( text ).width() + fm.height() + 2, fm.height() + 2 ); } if ( !recursive ) return baseSize; QSize childrenSize; const int rowCount = d->proxyModel.rowCount( index ); for ( int row = 0; row < rowCount; ++row ) { const QSize childSize = measureItem( d->proxyModel.index( row, 0, index ) ); childrenSize.setWidth( qMax( childrenSize.width(), childSize.width() ) ); childrenSize.rheight() += childSize.height(); } return baseSize + childrenSize; }