1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Charts module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 or (at your option) any later version
20 ** approved by the KDE Free Qt Foundation. The licenses are as published by
21 ** the Free Software Foundation and appearing in the file LICENSE.GPL3
22 ** included in the packaging of this file. Please review the following
23 ** information to ensure the GNU General Public License requirements will
24 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25 **
26 ** $QT_END_LICENSE$
27 **
28 ****************************************************************************/
29 
30 #include <private/piesliceitem_p.h>
31 #include <private/piechartitem_p.h>
32 #include <QtCharts/QPieSeries>
33 #include <QtCharts/QPieSlice>
34 #include <private/chartpresenter_p.h>
35 #include <QtGui/QPainter>
36 #include <QtCore/QtMath>
37 #include <QtWidgets/QGraphicsSceneEvent>
38 #include <QtCore/QTime>
39 #include <QtGui/QTextDocument>
40 #include <QtCore/QDebug>
41 
42 QT_CHARTS_BEGIN_NAMESPACE
43 
offset(qreal angle,qreal length)44 QPointF offset(qreal angle, qreal length)
45 {
46     qreal dx = qSin(qDegreesToRadians(angle)) * length;
47     qreal dy = qCos(qDegreesToRadians(angle)) * length;
48     return QPointF(dx, -dy);
49 }
50 
PieSliceItem(QGraphicsItem * parent)51 PieSliceItem::PieSliceItem(QGraphicsItem *parent)
52     : QGraphicsObject(parent),
53       m_hovered(false),
54       m_mousePressed(false)
55 {
56     setAcceptHoverEvents(true);
57     setAcceptedMouseButtons(Qt::MouseButtonMask);
58     setZValue(ChartPresenter::PieSeriesZValue);
59     setFlag(QGraphicsItem::ItemIsSelectable);
60     m_labelItem = new QGraphicsTextItem(this);
61     m_labelItem->document()->setDocumentMargin(1.0);
62 }
63 
~PieSliceItem()64 PieSliceItem::~PieSliceItem()
65 {
66     // If user is hovering over the slice and it gets destroyed we do
67     // not get a hover leave event. So we must emit the signal here.
68     if (m_hovered)
69         emit hovered(false);
70 }
71 
boundingRect() const72 QRectF PieSliceItem::boundingRect() const
73 {
74     return m_boundingRect;
75 }
76 
shape() const77 QPainterPath PieSliceItem::shape() const
78 {
79     // Don't include the label and label arm.
80     // This is used to detect a mouse clicks. We do not want clicks from label.
81     return m_slicePath;
82 }
83 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)84 void PieSliceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
85 {
86     painter->save();
87     painter->setClipRect(parentItem()->boundingRect());
88     painter->setPen(m_data.m_slicePen);
89     painter->setBrush(m_data.m_sliceBrush);
90     painter->drawPath(m_slicePath);
91     painter->restore();
92 
93     if (m_data.m_isLabelVisible) {
94         painter->save();
95 
96         // Pen for label arm not defined in the QPieSeries api, let's use brush's color instead
97         painter->setBrush(m_data.m_labelBrush);
98 
99         if (m_data.m_labelPosition == QPieSlice::LabelOutside) {
100             painter->setClipRect(parentItem()->boundingRect());
101             painter->strokePath(m_labelArmPath, m_data.m_labelBrush.color());
102         }
103 
104         painter->restore();
105     }
106 }
107 
hoverEnterEvent(QGraphicsSceneHoverEvent *)108 void PieSliceItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
109 {
110     m_hovered = true;
111     emit hovered(true);
112 }
113 
hoverLeaveEvent(QGraphicsSceneHoverEvent *)114 void PieSliceItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
115 {
116     m_hovered = false;
117     emit hovered(false);
118 }
119 
mousePressEvent(QGraphicsSceneMouseEvent * event)120 void PieSliceItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
121 {
122     emit pressed(event->buttons());
123     m_mousePressed = true;
124 }
125 
mouseReleaseEvent(QGraphicsSceneMouseEvent * event)126 void PieSliceItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
127 {
128     emit released(event->buttons());
129     if (m_mousePressed)
130         emit clicked(event->buttons());
131 }
132 
mouseDoubleClickEvent(QGraphicsSceneMouseEvent * event)133 void PieSliceItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
134 {
135     // For Pie slice a press signal needs to be explicitly fired for mouseDoubleClickEvent
136     emit pressed(event->buttons());
137     emit doubleClicked(event->buttons());
138 }
139 
setLayout(const PieSliceData & sliceData)140 void PieSliceItem::setLayout(const PieSliceData &sliceData)
141 {
142     m_data = sliceData;
143     updateGeometry();
144     update();
145 }
146 
updateGeometry()147 void PieSliceItem::updateGeometry()
148 {
149     if (m_data.m_radius <= 0)
150         return;
151 
152     prepareGeometryChange();
153 
154     // slice path
155     qreal centerAngle;
156     QPointF armStart;
157     m_slicePath = slicePath(m_data.m_center, m_data.m_radius, m_data.m_startAngle, m_data.m_angleSpan, &centerAngle, &armStart);
158 
159     m_labelItem->setVisible(m_data.m_isLabelVisible);
160 
161     if (m_data.m_isLabelVisible) {
162         // text rect
163         m_labelTextRect = ChartPresenter::textBoundingRect(m_data.m_labelFont,
164                                                            m_data.m_labelText,
165                                                            0);
166 
167         QString label(m_data.m_labelText);
168         m_labelItem->setDefaultTextColor(m_data.m_labelBrush.color());
169         m_labelItem->setFont(m_data.m_labelFont);
170 
171         // text position
172         if (m_data.m_labelPosition == QPieSlice::LabelOutside) {
173             setFlag(QGraphicsItem::ItemClipsChildrenToShape, false);
174 
175             // label arm path
176             QPointF labelTextStart;
177             m_labelArmPath = labelArmPath(armStart, centerAngle,
178                                           m_data.m_radius * m_data.m_labelArmLengthFactor,
179                                           m_labelTextRect.width(), &labelTextStart);
180 
181             m_labelTextRect.moveBottomLeft(labelTextStart);
182             if (m_labelTextRect.left() < 0)
183                 m_labelTextRect.setLeft(0);
184             else if (m_labelTextRect.left() < parentItem()->boundingRect().left())
185                 m_labelTextRect.setLeft(parentItem()->boundingRect().left());
186             if (m_labelTextRect.right() > parentItem()->boundingRect().right())
187                 m_labelTextRect.setRight(parentItem()->boundingRect().right());
188 
189             label = ChartPresenter::truncatedText(m_data.m_labelFont, m_data.m_labelText,
190                                                   qreal(0.0), m_labelTextRect.width(),
191                                                   m_labelTextRect.height(), m_labelTextRect);
192             m_labelArmPath = labelArmPath(armStart, centerAngle,
193                                           m_data.m_radius * m_data.m_labelArmLengthFactor,
194                                           m_labelTextRect.width(), &labelTextStart);
195             m_labelTextRect.moveBottomLeft(labelTextStart);
196 
197             m_labelItem->setTextWidth(m_labelTextRect.width()
198                                       + m_labelItem->document()->documentMargin());
199             m_labelItem->setHtml(label);
200             m_labelItem->setRotation(0);
201             m_labelItem->setPos(m_labelTextRect.x(), m_labelTextRect.y() + 1.0);
202         } else {
203             // label inside
204             setFlag(QGraphicsItem::ItemClipsChildrenToShape);
205             m_labelItem->setTextWidth(m_labelTextRect.width()
206                                       + m_labelItem->document()->documentMargin());
207             m_labelItem->setHtml(label);
208 
209             QPointF textCenter;
210             if (m_data.m_holeRadius > 0) {
211                 textCenter = m_data.m_center + offset(centerAngle, m_data.m_holeRadius
212                                                       + (m_data.m_radius
213                                                          - m_data.m_holeRadius) / 2);
214             } else {
215                 textCenter = m_data.m_center + offset(centerAngle, m_data.m_radius / 2);
216             }
217             m_labelItem->setPos(textCenter.x() - m_labelItem->boundingRect().width() / 2,
218                                 textCenter.y() - m_labelTextRect.height() / 2);
219 
220             QPointF labelCenter = m_labelItem->boundingRect().center();
221             m_labelItem->setTransformOriginPoint(labelCenter);
222 
223             if (m_data.m_labelPosition == QPieSlice::LabelInsideTangential) {
224                 m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2);
225             } else if (m_data.m_labelPosition == QPieSlice::LabelInsideNormal) {
226                 if (m_data.m_startAngle + m_data.m_angleSpan / 2 < 180)
227                     m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2 - 90);
228                 else
229                     m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2 + 90);
230             } else {
231                 m_labelItem->setRotation(0);
232             }
233         }
234         // Hide label if it's outside the bounding rect of parent item
235         QRectF labelRect(m_labelItem->boundingRect());
236         labelRect.moveTopLeft(m_labelItem->pos());
237         if ((parentItem()->boundingRect().left()
238                     < (labelRect.left() + m_labelItem->document()->documentMargin() + 1.0))
239                 && (parentItem()->boundingRect().right()
240                     > (labelRect.right() - m_labelItem->document()->documentMargin() - 1.0))
241                 && (parentItem()->boundingRect().top()
242                     < (labelRect.top() + m_labelItem->document()->documentMargin() + 1.0))
243                 && (parentItem()->boundingRect().bottom()
244                     > (labelRect.bottom() - m_labelItem->document()->documentMargin() - 1.0)))
245             m_labelItem->show();
246         else
247             m_labelItem->hide();
248     }
249 
250     //  bounding rect
251     if (m_data.m_isLabelVisible)
252         m_boundingRect = m_slicePath.boundingRect().united(m_labelArmPath.boundingRect()).united(m_labelTextRect);
253     else
254         m_boundingRect = m_slicePath.boundingRect();
255 
256     // Inflate bounding rect by 2/3 pen width to make sure it encompasses whole slice also for thick pens
257     // and miter joins.
258     int penWidth = (m_data.m_slicePen.width() * 2) / 3;
259     m_boundingRect = m_boundingRect.adjusted(-penWidth, -penWidth, penWidth, penWidth);
260 }
261 
sliceCenter(QPointF point,qreal radius,QPieSlice * slice)262 QPointF PieSliceItem::sliceCenter(QPointF point, qreal radius, QPieSlice *slice)
263 {
264     if (slice->isExploded()) {
265         qreal centerAngle = slice->startAngle() + (slice->angleSpan() / 2);
266         qreal len = radius * slice->explodeDistanceFactor();
267         point += offset(centerAngle, len);
268     }
269     return point;
270 }
271 
slicePath(QPointF center,qreal radius,qreal startAngle,qreal angleSpan,qreal * centerAngle,QPointF * armStart)272 QPainterPath PieSliceItem::slicePath(QPointF center, qreal radius, qreal startAngle, qreal angleSpan, qreal *centerAngle, QPointF *armStart)
273 {
274     // calculate center angle
275     *centerAngle = startAngle + (angleSpan / 2);
276 
277     // calculate slice rectangle
278     QRectF rect(center.x() - radius, center.y() - radius, radius * 2, radius * 2);
279 
280     // slice path
281     QPainterPath path;
282     if (m_data.m_holeRadius > 0) {
283         QRectF insideRect(center.x() - m_data.m_holeRadius, center.y() - m_data.m_holeRadius, m_data.m_holeRadius * 2, m_data.m_holeRadius * 2);
284         path.arcMoveTo(rect, -startAngle + 90);
285         path.arcTo(rect, -startAngle + 90, -angleSpan);
286         path.arcTo(insideRect, -startAngle + 90 - angleSpan, angleSpan);
287         path.closeSubpath();
288     } else {
289         path.moveTo(rect.center());
290         path.arcTo(rect, -startAngle + 90, -angleSpan);
291         path.closeSubpath();
292     }
293 
294     // calculate label arm start point
295     *armStart = center;
296     *armStart += offset(*centerAngle, radius + PIESLICE_LABEL_GAP);
297 
298     return path;
299 }
300 
labelArmPath(QPointF start,qreal angle,qreal length,qreal textWidth,QPointF * textStart)301 QPainterPath PieSliceItem::labelArmPath(QPointF start, qreal angle, qreal length, qreal textWidth, QPointF *textStart)
302 {
303     // Normalize the angle to 0-360 range
304     // NOTE: We are using int here on purpose. Depenging on platform and hardware
305     // qreal can be a double, float or something the user gives to the Qt configure
306     // (QT_COORD_TYPE). Compilers do not seem to support modulo for double or float
307     // but there are fmod() and fmodf() functions for that. So instead of some #ifdef
308     // that might break we just use int. Precision for this is just fine for our needs.
309     int normalized = angle * 10.0;
310     normalized = normalized % 3600;
311     if (normalized < 0)
312         normalized += 3600;
313     angle = (qreal) normalized / 10.0;
314 
315     // prevent label arm pointing straight down because it will look bad
316     if (angle < 180 && angle > 170)
317         angle = 170;
318     if (angle > 180 && angle < 190)
319         angle = 190;
320 
321     // line from slice to label
322     QPointF parm1 = start + offset(angle, length);
323 
324     // line to underline the label
325     QPointF parm2 = parm1;
326     if (angle < 180) { // arm swings the other way on the left side
327         parm2 += QPointF(textWidth, 0);
328         *textStart = parm1;
329     } else {
330         parm2 += QPointF(-textWidth, 0);
331         *textStart = parm2;
332     }
333 
334     QPainterPath path;
335     path.moveTo(start);
336     path.lineTo(parm1);
337     path.lineTo(parm2);
338 
339     return path;
340 }
341 
342 QT_CHARTS_END_NAMESPACE
343 
344 #include "moc_piesliceitem_p.cpp"
345