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
42QT_CHARTS_BEGIN_NAMESPACE
43
44QPointF offset(qreal angle, qreal length)
45{
46 qreal dx = qSin(v: qDegreesToRadians(degrees: angle)) * length;
47 qreal dy = qCos(v: qDegreesToRadians(degrees: angle)) * length;
48 return QPointF(dx, -dy);
49}
50
51PieSliceItem::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(flag: QGraphicsItem::ItemIsSelectable);
60 m_labelItem = new QGraphicsTextItem(this);
61 m_labelItem->document()->setDocumentMargin(1.0);
62}
63
64PieSliceItem::~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(state: false);
70}
71
72QRectF PieSliceItem::boundingRect() const
73{
74 return m_boundingRect;
75}
76
77QPainterPath 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
84void 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(path: 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(path: m_labelArmPath, pen: m_data.m_labelBrush.color());
102 }
103
104 painter->restore();
105 }
106}
107
108void PieSliceItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
109{
110 m_hovered = true;
111 emit hovered(state: true);
112}
113
114void PieSliceItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
115{
116 m_hovered = false;
117 emit hovered(state: false);
118}
119
120void PieSliceItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
121{
122 emit pressed(buttons: event->buttons());
123 m_mousePressed = true;
124}
125
126void PieSliceItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
127{
128 emit released(buttons: event->buttons());
129 if (m_mousePressed)
130 emit clicked(buttons: event->buttons());
131}
132
133void PieSliceItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
134{
135 // For Pie slice a press signal needs to be explicitly fired for mouseDoubleClickEvent
136 emit pressed(buttons: event->buttons());
137 emit doubleClicked(buttons: event->buttons());
138}
139
140void PieSliceItem::setLayout(const PieSliceData &sliceData)
141{
142 m_data = sliceData;
143 updateGeometry();
144 update();
145}
146
147void 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(center: m_data.m_center, radius: m_data.m_radius, startAngle: m_data.m_startAngle, angleSpan: m_data.m_angleSpan, centerAngle: &centerAngle, armStart: &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(font: m_data.m_labelFont,
164 text: m_data.m_labelText,
165 angle: 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(flag: QGraphicsItem::ItemClipsChildrenToShape, enabled: false);
174
175 // label arm path
176 QPointF labelTextStart;
177 m_labelArmPath = labelArmPath(start: armStart, angle: centerAngle,
178 length: m_data.m_radius * m_data.m_labelArmLengthFactor,
179 textWidth: m_labelTextRect.width(), textStart: &labelTextStart);
180
181 m_labelTextRect.moveBottomLeft(p: 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(font: m_data.m_labelFont, text: m_data.m_labelText,
190 angle: qreal(0.0), maxWidth: m_labelTextRect.width(),
191 maxHeight: m_labelTextRect.height(), boundingRect&: m_labelTextRect);
192 m_labelArmPath = labelArmPath(start: armStart, angle: centerAngle,
193 length: m_data.m_radius * m_data.m_labelArmLengthFactor,
194 textWidth: m_labelTextRect.width(), textStart: &labelTextStart);
195 m_labelTextRect.moveBottomLeft(p: 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(ax: m_labelTextRect.x(), ay: m_labelTextRect.y() + 1.0);
202 } else {
203 // label inside
204 setFlag(flag: 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(angle: centerAngle, length: m_data.m_holeRadius
212 + (m_data.m_radius
213 - m_data.m_holeRadius) / 2);
214 } else {
215 textCenter = m_data.m_center + offset(angle: centerAngle, length: m_data.m_radius / 2);
216 }
217 m_labelItem->setPos(ax: textCenter.x() - m_labelItem->boundingRect().width() / 2,
218 ay: 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(p: 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(r: m_labelArmPath.boundingRect()).united(r: 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(xp1: -penWidth, yp1: -penWidth, xp2: penWidth, yp2: penWidth);
260}
261
262QPointF 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(angle: centerAngle, length: len);
268 }
269 return point;
270}
271
272QPainterPath 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, angle: -startAngle + 90);
285 path.arcTo(rect, startAngle: -startAngle + 90, arcLength: -angleSpan);
286 path.arcTo(rect: insideRect, startAngle: -startAngle + 90 - angleSpan, arcLength: angleSpan);
287 path.closeSubpath();
288 } else {
289 path.moveTo(p: rect.center());
290 path.arcTo(rect, startAngle: -startAngle + 90, arcLength: -angleSpan);
291 path.closeSubpath();
292 }
293
294 // calculate label arm start point
295 *armStart = center;
296 *armStart += offset(angle: *centerAngle, length: radius + PIESLICE_LABEL_GAP);
297
298 return path;
299}
300
301QPainterPath 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(p: start);
336 path.lineTo(p: parm1);
337 path.lineTo(p: parm2);
338
339 return path;
340}
341
342QT_CHARTS_END_NAMESPACE
343
344#include "moc_piesliceitem_p.cpp"
345

source code of qtcharts/src/charts/piechart/piesliceitem.cpp