/**************************************************************************** ** ** Copyright (C) 2012 Digia Plc ** All rights reserved. ** For any questions to Digia, please use contact form at http://qt.digia.com ** ** This file is part of the Qt Commercial Charts Add-on. ** ** $QT_BEGIN_LICENSE$ ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. ** ** If you have questions regarding the use of this file, please use ** contact form at http://qt.digia.com ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qlegend.h" #include "qchart_p.h" #include "qseries.h" #include "legendmarker_p.h" #include "legendscrollbutton_p.h" #include "qxyseries.h" #include "qlineseries.h" #include "qareaseries.h" #include "qscatterseries.h" #include "qsplineseries.h" #include "qbarseries.h" #include "qstackedbarseries.h" #include "qpercentbarseries.h" #include "qbarset.h" #include "qpieseries.h" #include "qpieslice.h" #include "chartpresenter_p.h" #include #include #include QTCOMMERCIALCHART_BEGIN_NAMESPACE /*! \class QLegend \brief part of QtCommercial chart API. QLegend is a graphical object, whics displays legend of the chart. Legend state is updated by QChart, when series have been changed. By default, legend is drawn by QChart, but user can set a new parent to legend and handle the drawing manually. User isn't supposed to create or delete legend objects, but can reference it via QChart class. \mainclass \sa QChart, QSeries */ /*! \enum QLegend::Layout This enum describes the possible position for legend inside chart. \value LayoutTop \value LayoutBottom \value LayoutLeft \value LayoutRight */ /*! \fn void QLegend::clicked(QSeries* series, Qt::MouseButton button) \brief Notifies when series has been clicked on legend \a series \a button */ /*! \fn void QLegend::clicked(QBarSet* barset, Qt::MouseButton button) \brief Notifies when barset has been clicked on legend \a barset \a button */ /*! \fn void QLegend::clicked(QPieSlice* slice, Qt::MouseButton button) \brief Notifies when pie slice has been clicked on legend \a slice \a button */ /*! Constructs the legend object and sets the parent to \a parent */ QLegend::QLegend(QChart *chart):QGraphicsWidget(chart), m_margin(5), m_pos(0,0), m_minimumSize(50,20), // TODO: magic numbers m_maximumSize(150,100), m_size(m_minimumSize), m_brush(Qt::darkGray), // TODO: default should come from theme m_alignment(QLegend::AlignmentTop), mFirstMarker(0), m_attachedToChart(true), m_chart(chart) { m_scrollButtonLeft = new LegendScrollButton(LegendScrollButton::ScrollButtonIdLeft, this); m_scrollButtonRight = new LegendScrollButton(LegendScrollButton::ScrollButtonIdRight, this); m_scrollButtonUp = new LegendScrollButton(LegendScrollButton::ScrollButtonIdUp, this); m_scrollButtonDown = new LegendScrollButton(LegendScrollButton::ScrollButtonIdDown, this); setZValue(ChartPresenter::LegendZValue); } /*! Paints the legend to given \a painter. Paremeters \a option and \a widget arent used. */ void QLegend::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option) Q_UNUSED(widget) painter->setOpacity(opacity()); painter->setPen(m_pen); painter->setBrush(m_brush); // painter->drawRect(boundingRect()); } /*! Bounding rect of legend. */ QRectF QLegend::boundingRect() const { return QRectF(m_pos,m_size); } /*! Sets the \a brush of legend. Brush affects the background of legend. */ void QLegend::setBrush(const QBrush &brush) { if (m_brush != brush) { m_brush = brush; update(); } } /*! Returns the brush used by legend. */ QBrush QLegend::brush() const { return m_brush; } /*! Sets the \a pen of legend. Pen affects the legend borders. */ void QLegend::setPen(const QPen &pen) { if (m_pen != pen) { m_pen = pen; update(); } } /*! Returns the pen used by legend */ QPen QLegend::pen() const { return m_pen; } /*! Sets the \a preferred layout for legend. Legend tries to paint itself on the defined position in chart. \sa QLegend::Layout */ void QLegend::setAlignmnent(QLegend::Alignments alignment) { // if (!m_attachedToChart) { m_alignment = alignment; updateLayout(); m_chart->resize(m_chart->size()); // } } /*! Returns the preferred layout for legend */ QLegend::Alignments QLegend::alignment() const { return m_alignment; } /*! Returns the maximum size of legend. */ QSizeF QLegend::maximumSize() const { return m_maximumSize; } /*! Sets the maximum \a size for legend. The legend can't grow bigger than this size. If there are more series than legend can fit to this size, scroll buttons are displayed. */ void QLegend::setMaximumSize(const QSizeF size) { m_maximumSize = size; updateLayout(); } /*! Returns the current size of legend. */ QSizeF QLegend::size() const { return m_size; } /*! Sets the \a size of legend. If size is bigger than maximum size of legend, the legend is resized to the maximum size. \sa setMmaximumSize() */ void QLegend::setSize(const QSizeF size) { m_size = size; if (m_size.width() > m_maximumSize.width()) { m_size.setWidth(m_maximumSize.width()); } if (m_size.height() > m_maximumSize.height()) { m_size.setHeight(m_maximumSize.height()); } } /*! Sets position of legend to \a pos */ void QLegend::setPos(const QPointF &pos) { m_pos = pos; updateLayout(); } /*! \internal \a series \a domain Should be called when series is added to chart. */ void QLegend::handleSeriesAdded(QSeries *series, Domain *domain) { Q_UNUSED(domain) switch (series->type()) { case QSeries::SeriesTypeLine: { QLineSeries *lineSeries = static_cast(series); appendMarkers(lineSeries); break; } case QSeries::SeriesTypeArea: { QAreaSeries *areaSeries = static_cast(series); appendMarkers(areaSeries); break; } case QSeries::SeriesTypeBar: { QBarSeries *barSeries = static_cast(series); appendMarkers(barSeries); break; } case QSeries::SeriesTypeStackedBar: { QStackedBarSeries *stackedBarSeries = static_cast(series); appendMarkers(stackedBarSeries); break; } case QSeries::SeriesTypePercentBar: { QPercentBarSeries *percentBarSeries = static_cast(series); appendMarkers(percentBarSeries); break; } case QSeries::SeriesTypeScatter: { QScatterSeries *scatterSeries = static_cast(series); appendMarkers(scatterSeries); break; } case QSeries::SeriesTypePie: { QPieSeries *pieSeries = static_cast(series); appendMarkers(pieSeries); connect(pieSeries,SIGNAL(added(QList)),this,SLOT(handleAdded(QList))); break; } case QSeries::SeriesTypeSpline: { QSplineSeries *splineSeries = static_cast(series); appendMarkers(splineSeries); break; } default: { qWarning()<< "QLegend::handleSeriesAdded" << series->type() << "unknown series type."; break; } } updateLayout(); } /*! \internal \a series Should be called when series is removed from chart. */ void QLegend::handleSeriesRemoved(QSeries *series) { switch (series->type()) { case QSeries::SeriesTypeArea: { QAreaSeries *areaSeries = static_cast(series); deleteMarkers(areaSeries); break; } case QSeries::SeriesTypePie: { QPieSeries *pieSeries = static_cast(series); disconnect(pieSeries, SIGNAL(added(QList)), this, SLOT(handleAdded(QList))); deleteMarkers(series); break; } default: { // All other types deleteMarkers(series); break; } } updateLayout(); } /*! \internal \a slices Should be called when slices are added to pie chart. */ void QLegend::handleAdded(QList slices) { QPieSeries* series = static_cast (sender()); foreach(QPieSlice* s, slices) { LegendMarker* marker = new LegendMarker(series, s, this); marker->setName(s->label()); marker->setBrush(s->brush()); connect(marker, SIGNAL(clicked(QPieSlice*,Qt::MouseButton)), this, SIGNAL(clicked(QPieSlice*,Qt::MouseButton))); connect(s, SIGNAL(changed()), marker, SLOT(changed())); connect(s, SIGNAL(destroyed()), marker, SLOT(deleteLater())); connect(marker, SIGNAL(destroyed()), this, SLOT(handleMarkerDestroyed())); m_markers.append(marker); childItems().append(marker); } updateLayout(); } /*! \internal \a slices Should be called when slices are removed from pie chart. Currently unused, because removed slices are also deleted and we listen destroyed signal */ void QLegend::handleRemoved(QList slices) { Q_UNUSED(slices) } /*! \internal Notifies legend that some marker has been removed. Sent by legend markers when destroyed */ void QLegend::handleMarkerDestroyed() { LegendMarker* m = static_cast (sender()); m_markers.removeOne(m); updateLayout(); } /*! \internal \a event Handles clicked signals from scroll buttons */ void QLegend::scrollButtonClicked(LegendScrollButton *scrollButton) { Q_ASSERT(scrollButton); switch (scrollButton->id()) { case LegendScrollButton::ScrollButtonIdLeft: case LegendScrollButton::ScrollButtonIdUp: { // Lower limit is same in these cases mFirstMarker--; checkFirstMarkerBounds(); break; } case LegendScrollButton::ScrollButtonIdRight: case LegendScrollButton::ScrollButtonIdDown: { mFirstMarker++; checkFirstMarkerBounds(); break; } default: { break; } } updateLayout(); } /*! Detaches the legend from chart. Chart won't change layout of the legend. */ void QLegend::detachFromChart() { m_attachedToChart = false; } /*! Attaches the legend to chart. Chart may change layout of the legend. */ void QLegend::attachToChart() { m_attachedToChart = true; } /*! Returns true, if legend is attached to chart. */ bool QLegend::attachedToChart() { return m_attachedToChart; } /*! \internal Helper function. Appends markers from \a series to legend. */ void QLegend::appendMarkers(QAreaSeries* series) { LegendMarker* marker = new LegendMarker(series,this); connect(marker, SIGNAL(clicked(QSeries *, Qt::MouseButton)), this, SIGNAL(clicked(QSeries *, Qt::MouseButton))); connect(marker, SIGNAL(destroyed()), this, SLOT(handleMarkerDestroyed())); connect(series,SIGNAL(updated()),marker,SLOT(changed())); marker->changed(); m_markers.append(marker); childItems().append(marker); } /*! \internal Helper function. Appends markers from \a series to legend. */ void QLegend::appendMarkers(QXYSeries* series) { LegendMarker* marker = new LegendMarker(series,this); connect(marker, SIGNAL(clicked(QSeries *, Qt::MouseButton)), this, SIGNAL(clicked(QSeries *, Qt::MouseButton))); connect(marker, SIGNAL(destroyed()), this, SLOT(handleMarkerDestroyed())); connect(series,SIGNAL(updated()),marker,SLOT(changed())); marker->changed(); m_markers.append(marker); childItems().append(marker); } /*! \internal Helper function. Appends markers from \a series to legend. */ void QLegend::appendMarkers(QBarSeries *series) { foreach(QBarSet* set, series->barSets()) { LegendMarker* marker = new LegendMarker(series, set, this); connect(marker, SIGNAL(clicked(QBarSet *, Qt::MouseButton)), this, SIGNAL(clicked(QBarSet *, Qt::MouseButton))); connect(set, SIGNAL(valueChanged()), marker, SLOT(changed())); connect(marker, SIGNAL(destroyed()), this, SLOT(handleMarkerDestroyed())); marker->changed(); m_markers.append(marker); childItems().append(marker); } } /*! \internal Helper function. Appends markers from \a series to legend. */ void QLegend::appendMarkers(QPieSeries *series) { foreach(QPieSlice* slice, series->slices()) { LegendMarker* marker = new LegendMarker(series, slice, this); connect(marker, SIGNAL(clicked(QPieSlice *, Qt::MouseButton)), this, SIGNAL(clicked(QPieSlice *, Qt::MouseButton))); connect(slice, SIGNAL(changed()), marker, SLOT(changed())); connect(slice, SIGNAL(destroyed()), marker, SLOT(deleteLater())); connect(marker, SIGNAL(destroyed()), this, SLOT(handleMarkerDestroyed())); marker->changed(); m_markers.append(marker); childItems().append(marker); } } /*! \internal Deletes all markers that are created from \a series */ void QLegend::deleteMarkers(QSeries *series) { // Search all markers that belong to given series and delete them. foreach (LegendMarker *m, m_markers) { if (m->series() == series) { m_markers.removeOne(m); delete m; } } } /*! \internal Updates layout of legend. Tries to fit as many markers as possible up to the maximum size of legend. If items don't fit, sets the visibility of scroll buttons accordingly. Causes legend to be resized. */ void QLegend::updateLayout() { // Calculate layout for markers and text if (m_markers.count() <= 0) { // Nothing to do return; } // Find out widest item. QSizeF markerMaxSize = maximumMarkerSize(); checkFirstMarkerBounds(); // Use max height as scroll button size rescaleScrollButtons(QSize(markerMaxSize.height() ,markerMaxSize.height())); qreal totalWidth = 0; qreal totalHeight = 0; switch (m_alignment) { // Both cases organise items horizontally case QLegend::AlignmentBottom: case QLegend::AlignmentTop: { qreal xStep = markerMaxSize.width(); qreal x = m_pos.x() + m_margin; qreal y = m_pos.y() + m_margin; int column = 0; int maxColumns = 1; qreal scrollButtonWidth = 0; // Set correct visibility for scroll scrollbuttons if (scrollButtonsVisible()) { m_scrollButtonLeft->setVisible(true); m_scrollButtonRight->setVisible(true); // scrollbuttons visible, so add their width to total width totalWidth += (m_scrollButtonLeft->boundingRect().width() + m_margin) * 2; scrollButtonWidth = m_scrollButtonLeft->boundingRect().width() + m_margin; // start position changes by scrollbutton width x += scrollButtonWidth; } else { m_scrollButtonLeft->setVisible(false); m_scrollButtonRight->setVisible(false); } m_scrollButtonUp->setVisible(false); m_scrollButtonDown->setVisible(false); for (int i=0; i < m_markers.count(); i++) { LegendMarker *m = m_markers.at(i); if (i < mFirstMarker) { // Markers before first are not visible. m->setVisible(false); } else { if ((x + xStep + scrollButtonWidth + m_margin) > (m_pos.x() + m_maximumSize.width())) { // This marker would go outside legend rect. m->setVisible(false); } else { // This marker is ok m->setVisible(true); m->setPos(x, y); x += xStep; column++; } } maxColumns = column; } m_scrollButtonLeft->setPos(m_pos.x() + m_margin, y); m_scrollButtonRight->setPos(x + m_margin, y); totalWidth += maxColumns * markerMaxSize.width() + m_margin * 2; totalHeight = markerMaxSize.height() + m_margin * 2; break; } // Both cases organize items vertically case QLegend::AlignmentLeft: case QLegend::AlignmentRight: { qreal yStep = markerMaxSize.height(); qreal x = m_pos.x() + m_margin; qreal y = m_pos.y() + m_margin; int row = 1; int maxRows = 1; qreal scrollButtonHeight = 0; // Set correct visibility for scroll scrollbuttons if (scrollButtonsVisible()) { m_scrollButtonUp->setVisible(true); m_scrollButtonDown->setVisible(true); totalHeight += (m_scrollButtonUp->boundingRect().height() + m_margin) * 2; // scrollbuttons visible, so add their height to total height scrollButtonHeight = m_scrollButtonUp->boundingRect().height(); y += scrollButtonHeight + m_margin; // start position changes by scrollbutton height } else { m_scrollButtonUp->setVisible(false); m_scrollButtonDown->setVisible(false); } m_scrollButtonLeft->setVisible(false); m_scrollButtonRight->setVisible(false); for (int i=0; i < m_markers.count(); i++) { LegendMarker* m = m_markers.at(i); if (i < mFirstMarker) { // Markers before first are not visible. m->setVisible(false); } else { if ((y + yStep + scrollButtonHeight) > (m_pos.y() + m_maximumSize.height())) { // This marker would go outside legend rect. m->setVisible(false); } else { // This marker is ok m->setVisible(true); m->setPos(x, y); y += yStep; row++; } } maxRows = row; } m_scrollButtonUp->setPos(m_pos.x() + m_margin, m_pos.y() + m_margin); m_scrollButtonDown->setPos(m_pos.x() + m_margin, y + m_margin); totalWidth += markerMaxSize.width() + m_margin * 2; totalHeight = maxRows * markerMaxSize.height() + m_margin * 4 + scrollButtonHeight; // TODO: check this break; } default: { break; } } m_size.setWidth(totalWidth); m_size.setHeight(totalHeight); update(); } /*! \internal Sets the size of scroll buttons to \a size */ void QLegend::rescaleScrollButtons(const QSize &size) { QPolygonF left; left << QPointF(size.width(), 0) << QPointF(0, size.height() / 2) << QPointF(size.width(), size.height()); QPolygonF right; right << QPointF(0, 0) << QPointF(size.width(), size.height() / 2) << QPointF(0, size.height()); QPolygonF up; up << QPointF(0, size.height()) << QPointF(size.width() / 2,0) << QPointF(size.width(), size.height()); QPolygonF down; down << QPointF(0, 0) << QPointF(size.width() / 2, size.height()) << QPointF(size.width(), 0); m_scrollButtonLeft->setPolygon(left); m_scrollButtonRight->setPolygon(right); m_scrollButtonUp->setPolygon(up); m_scrollButtonDown->setPolygon(down); } /*! \internal Finds out maximum size of single marker. Marker sizes depend on series names. */ QSizeF QLegend::maximumMarkerSize() { QSizeF max(0,0); foreach (LegendMarker* m, m_markers) { if (m->boundingRect().width() > max.width()) max.setWidth(m->boundingRect().width()); if (m->boundingRect().height() > max.height()) max.setHeight(m->boundingRect().height()); } return max; } /*! \internal Checks that first marker is in acceptable bounds. Bounds range from 0 to (maximum number of markers - visible markers) If scrollbuttons are visible, they affect the number of visible markers. */ void QLegend::checkFirstMarkerBounds() { if ((m_alignment == QLegend::AlignmentLeft) || (m_alignment == QLegend::AlignmentRight)) { // Bounds limited by height. int max; if (scrollButtonsVisible()) { max = (m_maximumSize.height() - m_scrollButtonLeft->boundingRect().height() * 2 - m_margin * 4) / maximumMarkerSize().height(); } else { max = m_maximumSize.height() / maximumMarkerSize().height(); } if (mFirstMarker > m_markers.count() - max) mFirstMarker = m_markers.count() - max; } else { // Bounds limited by width int max; if (scrollButtonsVisible()) { max = (m_maximumSize.width() - m_scrollButtonLeft->boundingRect().width() * 2 - m_margin*4) / maximumMarkerSize().width(); } else { max = m_maximumSize.width() / maximumMarkerSize().width(); } if (mFirstMarker > m_markers.count() - max) mFirstMarker = m_markers.count() - max; } if (mFirstMarker < 0) mFirstMarker = 0; } /*! \internal Helper function. Visibility of scroll buttons isn't quite obvious, so helper function clarifies the logic. */ bool QLegend::scrollButtonsVisible() { // Just a helper to clarify, what the magic below means :) if ((m_alignment == QLegend::AlignmentTop) || (m_alignment == QLegend::AlignmentBottom)) { return (maximumMarkerSize().width() * m_markers.count() + m_margin * 2 > m_maximumSize.width()); } else if ((m_alignment == QLegend::AlignmentLeft) || (m_alignment == QLegend::AlignmentRight)) { return (maximumMarkerSize().height() * m_markers.count() + m_margin * 2 > m_maximumSize.height()); } return (maximumMarkerSize().height() * m_markers.count() + m_margin * 2 > m_maximumSize.height()); } #include "moc_qlegend.cpp" QTCOMMERCIALCHART_END_NAMESPACE