| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Charts module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include <private/piesliceitem_p.h> |
| #include <private/piechartitem_p.h> |
| #include <QtCharts/QPieSeries> |
| #include <QtCharts/QPieSlice> |
| #include <private/chartpresenter_p.h> |
| #include <QtGui/QPainter> |
| #include <QtCore/QtMath> |
| #include <QtWidgets/QGraphicsSceneEvent> |
| #include <QtCore/QTime> |
| #include <QtGui/QTextDocument> |
| #include <QtCore/QDebug> |
| |
| QT_CHARTS_BEGIN_NAMESPACE |
| |
| QPointF offset(qreal angle, qreal length) |
| { |
| qreal dx = qSin(qDegreesToRadians(angle)) * length; |
| qreal dy = qCos(qDegreesToRadians(angle)) * length; |
| return QPointF(dx, -dy); |
| } |
| |
| PieSliceItem::PieSliceItem(QGraphicsItem *parent) |
| : QGraphicsObject(parent), |
| m_hovered(false), |
| m_mousePressed(false) |
| { |
| setAcceptHoverEvents(true); |
| setAcceptedMouseButtons(Qt::MouseButtonMask); |
| setZValue(ChartPresenter::PieSeriesZValue); |
| setFlag(QGraphicsItem::ItemIsSelectable); |
| m_labelItem = new QGraphicsTextItem(this); |
| m_labelItem->document()->setDocumentMargin(1.0); |
| } |
| |
| PieSliceItem::~PieSliceItem() |
| { |
| // If user is hovering over the slice and it gets destroyed we do |
| // not get a hover leave event. So we must emit the signal here. |
| if (m_hovered) |
| emit hovered(false); |
| } |
| |
| QRectF PieSliceItem::boundingRect() const |
| { |
| return m_boundingRect; |
| } |
| |
| QPainterPath PieSliceItem::shape() const |
| { |
| // Don't include the label and label arm. |
| // This is used to detect a mouse clicks. We do not want clicks from label. |
| return m_slicePath; |
| } |
| |
| void PieSliceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) |
| { |
| painter->save(); |
| painter->setClipRect(parentItem()->boundingRect()); |
| painter->setPen(m_data.m_slicePen); |
| painter->setBrush(m_data.m_sliceBrush); |
| painter->drawPath(m_slicePath); |
| painter->restore(); |
| |
| if (m_data.m_isLabelVisible) { |
| painter->save(); |
| |
| // Pen for label arm not defined in the QPieSeries api, let's use brush's color instead |
| painter->setBrush(m_data.m_labelBrush); |
| |
| if (m_data.m_labelPosition == QPieSlice::LabelOutside) { |
| painter->setClipRect(parentItem()->boundingRect()); |
| painter->strokePath(m_labelArmPath, m_data.m_labelBrush.color()); |
| } |
| |
| painter->restore(); |
| } |
| } |
| |
| void PieSliceItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/) |
| { |
| m_hovered = true; |
| emit hovered(true); |
| } |
| |
| void PieSliceItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/) |
| { |
| m_hovered = false; |
| emit hovered(false); |
| } |
| |
| void PieSliceItem::mousePressEvent(QGraphicsSceneMouseEvent *event) |
| { |
| emit pressed(event->buttons()); |
| m_mousePressed = true; |
| } |
| |
| void PieSliceItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) |
| { |
| emit released(event->buttons()); |
| if (m_mousePressed) |
| emit clicked(event->buttons()); |
| } |
| |
| void PieSliceItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) |
| { |
| // For Pie slice a press signal needs to be explicitly fired for mouseDoubleClickEvent |
| emit pressed(event->buttons()); |
| emit doubleClicked(event->buttons()); |
| } |
| |
| void PieSliceItem::setLayout(const PieSliceData &sliceData) |
| { |
| m_data = sliceData; |
| updateGeometry(); |
| update(); |
| } |
| |
| void PieSliceItem::updateGeometry() |
| { |
| if (m_data.m_radius <= 0) |
| return; |
| |
| prepareGeometryChange(); |
| |
| // slice path |
| qreal centerAngle; |
| QPointF armStart; |
| m_slicePath = slicePath(m_data.m_center, m_data.m_radius, m_data.m_startAngle, m_data.m_angleSpan, ¢erAngle, &armStart); |
| |
| m_labelItem->setVisible(m_data.m_isLabelVisible); |
| |
| if (m_data.m_isLabelVisible) { |
| // text rect |
| m_labelTextRect = ChartPresenter::textBoundingRect(m_data.m_labelFont, |
| m_data.m_labelText, |
| 0); |
| |
| QString label(m_data.m_labelText); |
| m_labelItem->setDefaultTextColor(m_data.m_labelBrush.color()); |
| m_labelItem->setFont(m_data.m_labelFont); |
| |
| // text position |
| if (m_data.m_labelPosition == QPieSlice::LabelOutside) { |
| setFlag(QGraphicsItem::ItemClipsChildrenToShape, false); |
| |
| // label arm path |
| QPointF labelTextStart; |
| m_labelArmPath = labelArmPath(armStart, centerAngle, |
| m_data.m_radius * m_data.m_labelArmLengthFactor, |
| m_labelTextRect.width(), &labelTextStart); |
| |
| m_labelTextRect.moveBottomLeft(labelTextStart); |
| if (m_labelTextRect.left() < 0) |
| m_labelTextRect.setLeft(0); |
| else if (m_labelTextRect.left() < parentItem()->boundingRect().left()) |
| m_labelTextRect.setLeft(parentItem()->boundingRect().left()); |
| if (m_labelTextRect.right() > parentItem()->boundingRect().right()) |
| m_labelTextRect.setRight(parentItem()->boundingRect().right()); |
| |
| label = ChartPresenter::truncatedText(m_data.m_labelFont, m_data.m_labelText, |
| qreal(0.0), m_labelTextRect.width(), |
| m_labelTextRect.height(), m_labelTextRect); |
| m_labelArmPath = labelArmPath(armStart, centerAngle, |
| m_data.m_radius * m_data.m_labelArmLengthFactor, |
| m_labelTextRect.width(), &labelTextStart); |
| m_labelTextRect.moveBottomLeft(labelTextStart); |
| |
| m_labelItem->setTextWidth(m_labelTextRect.width() |
| + m_labelItem->document()->documentMargin()); |
| m_labelItem->setHtml(label); |
| m_labelItem->setRotation(0); |
| m_labelItem->setPos(m_labelTextRect.x(), m_labelTextRect.y() + 1.0); |
| } else { |
| // label inside |
| setFlag(QGraphicsItem::ItemClipsChildrenToShape); |
| m_labelItem->setTextWidth(m_labelTextRect.width() |
| + m_labelItem->document()->documentMargin()); |
| m_labelItem->setHtml(label); |
| |
| QPointF textCenter; |
| if (m_data.m_holeRadius > 0) { |
| textCenter = m_data.m_center + offset(centerAngle, m_data.m_holeRadius |
| + (m_data.m_radius |
| - m_data.m_holeRadius) / 2); |
| } else { |
| textCenter = m_data.m_center + offset(centerAngle, m_data.m_radius / 2); |
| } |
| m_labelItem->setPos(textCenter.x() - m_labelItem->boundingRect().width() / 2, |
| textCenter.y() - m_labelTextRect.height() / 2); |
| |
| QPointF labelCenter = m_labelItem->boundingRect().center(); |
| m_labelItem->setTransformOriginPoint(labelCenter); |
| |
| if (m_data.m_labelPosition == QPieSlice::LabelInsideTangential) { |
| m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2); |
| } else if (m_data.m_labelPosition == QPieSlice::LabelInsideNormal) { |
| if (m_data.m_startAngle + m_data.m_angleSpan / 2 < 180) |
| m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2 - 90); |
| else |
| m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2 + 90); |
| } else { |
| m_labelItem->setRotation(0); |
| } |
| } |
| // Hide label if it's outside the bounding rect of parent item |
| QRectF labelRect(m_labelItem->boundingRect()); |
| labelRect.moveTopLeft(m_labelItem->pos()); |
| if ((parentItem()->boundingRect().left() |
| < (labelRect.left() + m_labelItem->document()->documentMargin() + 1.0)) |
| && (parentItem()->boundingRect().right() |
| > (labelRect.right() - m_labelItem->document()->documentMargin() - 1.0)) |
| && (parentItem()->boundingRect().top() |
| < (labelRect.top() + m_labelItem->document()->documentMargin() + 1.0)) |
| && (parentItem()->boundingRect().bottom() |
| > (labelRect.bottom() - m_labelItem->document()->documentMargin() - 1.0))) |
| m_labelItem->show(); |
| else |
| m_labelItem->hide(); |
| } |
| |
| // bounding rect |
| if (m_data.m_isLabelVisible) |
| m_boundingRect = m_slicePath.boundingRect().united(m_labelArmPath.boundingRect()).united(m_labelTextRect); |
| else |
| m_boundingRect = m_slicePath.boundingRect(); |
| |
| // Inflate bounding rect by 2/3 pen width to make sure it encompasses whole slice also for thick pens |
| // and miter joins. |
| int penWidth = (m_data.m_slicePen.width() * 2) / 3; |
| m_boundingRect = m_boundingRect.adjusted(-penWidth, -penWidth, penWidth, penWidth); |
| } |
| |
| QPointF PieSliceItem::sliceCenter(QPointF point, qreal radius, QPieSlice *slice) |
| { |
| if (slice->isExploded()) { |
| qreal centerAngle = slice->startAngle() + (slice->angleSpan() / 2); |
| qreal len = radius * slice->explodeDistanceFactor(); |
| point += offset(centerAngle, len); |
| } |
| return point; |
| } |
| |
| QPainterPath PieSliceItem::slicePath(QPointF center, qreal radius, qreal startAngle, qreal angleSpan, qreal *centerAngle, QPointF *armStart) |
| { |
| // calculate center angle |
| *centerAngle = startAngle + (angleSpan / 2); |
| |
| // calculate slice rectangle |
| QRectF rect(center.x() - radius, center.y() - radius, radius * 2, radius * 2); |
| |
| // slice path |
| QPainterPath path; |
| if (m_data.m_holeRadius > 0) { |
| QRectF insideRect(center.x() - m_data.m_holeRadius, center.y() - m_data.m_holeRadius, m_data.m_holeRadius * 2, m_data.m_holeRadius * 2); |
| path.arcMoveTo(rect, -startAngle + 90); |
| path.arcTo(rect, -startAngle + 90, -angleSpan); |
| path.arcTo(insideRect, -startAngle + 90 - angleSpan, angleSpan); |
| path.closeSubpath(); |
| } else { |
| path.moveTo(rect.center()); |
| path.arcTo(rect, -startAngle + 90, -angleSpan); |
| path.closeSubpath(); |
| } |
| |
| // calculate label arm start point |
| *armStart = center; |
| *armStart += offset(*centerAngle, radius + PIESLICE_LABEL_GAP); |
| |
| return path; |
| } |
| |
| QPainterPath PieSliceItem::labelArmPath(QPointF start, qreal angle, qreal length, qreal textWidth, QPointF *textStart) |
| { |
| // Normalize the angle to 0-360 range |
| // NOTE: We are using int here on purpose. Depenging on platform and hardware |
| // qreal can be a double, float or something the user gives to the Qt configure |
| // (QT_COORD_TYPE). Compilers do not seem to support modulo for double or float |
| // but there are fmod() and fmodf() functions for that. So instead of some #ifdef |
| // that might break we just use int. Precision for this is just fine for our needs. |
| int normalized = angle * 10.0; |
| normalized = normalized % 3600; |
| if (normalized < 0) |
| normalized += 3600; |
| angle = (qreal) normalized / 10.0; |
| |
| // prevent label arm pointing straight down because it will look bad |
| if (angle < 180 && angle > 170) |
| angle = 170; |
| if (angle > 180 && angle < 190) |
| angle = 190; |
| |
| // line from slice to label |
| QPointF parm1 = start + offset(angle, length); |
| |
| // line to underline the label |
| QPointF parm2 = parm1; |
| if (angle < 180) { // arm swings the other way on the left side |
| parm2 += QPointF(textWidth, 0); |
| *textStart = parm1; |
| } else { |
| parm2 += QPointF(-textWidth, 0); |
| *textStart = parm2; |
| } |
| |
| QPainterPath path; |
| path.moveTo(start); |
| path.lineTo(parm1); |
| path.lineTo(parm2); |
| |
| return path; |
| } |
| |
| QT_CHARTS_END_NAMESPACE |
| |
| #include "moc_piesliceitem_p.cpp" |