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 examples of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:BSD$
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** BSD License Usage
18** Alternatively, you may use this file under the terms of the BSD license
19** as follows:
20**
21** "Redistribution and use in source and binary forms, with or without
22** modification, are permitted provided that the following conditions are
23** met:
24** * Redistributions of source code must retain the above copyright
25** notice, this list of conditions and the following disclaimer.
26** * Redistributions in binary form must reproduce the above copyright
27** notice, this list of conditions and the following disclaimer in
28** the documentation and/or other materials provided with the
29** distribution.
30** * Neither the name of The Qt Company Ltd nor the names of its
31** contributors may be used to endorse or promote products derived
32** from this software without specific prior written permission.
33**
34**
35** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46**
47** $QT_END_LICENSE$
48**
49****************************************************************************/
50
51#include "pieview.h"
52
53#include <QtWidgets>
54
55PieView::PieView(QWidget *parent)
56 : QAbstractItemView(parent)
57{
58 horizontalScrollBar()->setRange(0, 0);
59 verticalScrollBar()->setRange(0, 0);
60}
61
62void PieView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
63 const QList<int> &roles)
64{
65 QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
66
67 if (!roles.contains(Qt::DisplayRole))
68 return;
69
70 validItems = 0;
71 totalValue = 0.0;
72
73 for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
74
75 QModelIndex index = model()->index(row, 1, rootIndex());
76 double value = model()->data(index, Qt::DisplayRole).toDouble();
77
78 if (value > 0.0) {
79 totalValue += value;
80 validItems++;
81 }
82 }
83 viewport()->update();
84}
85
86bool PieView::edit(const QModelIndex &index, EditTrigger trigger, QEvent *event)
87{
88 if (index.column() == 0)
89 return QAbstractItemView::edit(index, trigger, event);
90 else
91 return false;
92}
93
94/*
95 Returns the item that covers the coordinate given in the view.
96*/
97
98QModelIndex PieView::indexAt(const QPoint &point) const
99{
100 if (validItems == 0)
101 return QModelIndex();
102
103 // Transform the view coordinates into contents widget coordinates.
104 int wx = point.x() + horizontalScrollBar()->value();
105 int wy = point.y() + verticalScrollBar()->value();
106
107 if (wx < totalSize) {
108 double cx = wx - totalSize / 2;
109 double cy = totalSize / 2 - wy; // positive cy for items above the center
110
111 // Determine the distance from the center point of the pie chart.
112 double d = std::sqrt(std::pow(cx, 2) + std::pow(cy, 2));
113
114 if (d == 0 || d > pieSize / 2)
115 return QModelIndex();
116
117 // Determine the angle of the point.
118 double angle = qRadiansToDegrees(std::atan2(cy, cx));
119 if (angle < 0)
120 angle = 360 + angle;
121
122 // Find the relevant slice of the pie.
123 double startAngle = 0.0;
124
125 for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
126
127 QModelIndex index = model()->index(row, 1, rootIndex());
128 double value = model()->data(index).toDouble();
129
130 if (value > 0.0) {
131 double sliceAngle = 360 * value / totalValue;
132
133 if (angle >= startAngle && angle < (startAngle + sliceAngle))
134 return model()->index(row, 1, rootIndex());
135
136 startAngle += sliceAngle;
137 }
138 }
139 } else {
140 QStyleOptionViewItem option;
141 initViewItemOption(&option);
142 double itemHeight = QFontMetrics(option.font).height();
143 int listItem = int((wy - margin) / itemHeight);
144 int validRow = 0;
145
146 for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
147
148 QModelIndex index = model()->index(row, 1, rootIndex());
149 if (model()->data(index).toDouble() > 0.0) {
150
151 if (listItem == validRow)
152 return model()->index(row, 0, rootIndex());
153
154 // Update the list index that corresponds to the next valid row.
155 ++validRow;
156 }
157 }
158 }
159
160 return QModelIndex();
161}
162
163bool PieView::isIndexHidden(const QModelIndex & /*index*/) const
164{
165 return false;
166}
167
168/*
169 Returns the rectangle of the item at position \a index in the
170 model. The rectangle is in contents coordinates.
171*/
172
173QRect PieView::itemRect(const QModelIndex &index) const
174{
175 if (!index.isValid())
176 return QRect();
177
178 // Check whether the index's row is in the list of rows represented
179 // by slices.
180 QModelIndex valueIndex;
181
182 if (index.column() != 1)
183 valueIndex = model()->index(index.row(), 1, rootIndex());
184 else
185 valueIndex = index;
186
187 if (model()->data(valueIndex).toDouble() <= 0.0)
188 return QRect();
189
190 int listItem = 0;
191 for (int row = index.row()-1; row >= 0; --row) {
192 if (model()->data(model()->index(row, 1, rootIndex())).toDouble() > 0.0)
193 listItem++;
194 }
195
196 switch (index.column()) {
197 case 0: {
198 QStyleOptionViewItem option;
199 initViewItemOption(&option);
200 const qreal itemHeight = QFontMetricsF(option.font).height();
201 return QRect(totalSize,
202 qRound(margin + listItem * itemHeight),
203 totalSize - margin, qRound(itemHeight));
204 }
205 case 1:
206 return viewport()->rect();
207 }
208 return QRect();
209}
210
211QRegion PieView::itemRegion(const QModelIndex &index) const
212{
213 if (!index.isValid())
214 return QRegion();
215
216 if (index.column() != 1)
217 return itemRect(index);
218
219 if (model()->data(index).toDouble() <= 0.0)
220 return QRegion();
221
222 double startAngle = 0.0;
223 for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
224
225 QModelIndex sliceIndex = model()->index(row, 1, rootIndex());
226 double value = model()->data(sliceIndex).toDouble();
227
228 if (value > 0.0) {
229 double angle = 360 * value / totalValue;
230
231 if (sliceIndex == index) {
232 QPainterPath slicePath;
233 slicePath.moveTo(totalSize / 2, totalSize / 2);
234 slicePath.arcTo(margin, margin, margin + pieSize, margin + pieSize,
235 startAngle, angle);
236 slicePath.closeSubpath();
237
238 return QRegion(slicePath.toFillPolygon().toPolygon());
239 }
240
241 startAngle += angle;
242 }
243 }
244
245 return QRegion();
246}
247
248int PieView::horizontalOffset() const
249{
250 return horizontalScrollBar()->value();
251}
252
253void PieView::mousePressEvent(QMouseEvent *event)
254{
255 QAbstractItemView::mousePressEvent(event);
256 origin = event->position().toPoint();
257 if (!rubberBand)
258 rubberBand = new QRubberBand(QRubberBand::Rectangle, viewport());
259 rubberBand->setGeometry(QRect(origin, QSize()));
260 rubberBand->show();
261}
262
263void PieView::mouseMoveEvent(QMouseEvent *event)
264{
265 if (rubberBand)
266 rubberBand->setGeometry(QRect(origin, event->position().toPoint()).normalized());
267 QAbstractItemView::mouseMoveEvent(event);
268}
269
270void PieView::mouseReleaseEvent(QMouseEvent *event)
271{
272 QAbstractItemView::mouseReleaseEvent(event);
273 if (rubberBand)
274 rubberBand->hide();
275 viewport()->update();
276}
277
278QModelIndex PieView::moveCursor(QAbstractItemView::CursorAction cursorAction,
279 Qt::KeyboardModifiers /*modifiers*/)
280{
281 QModelIndex current = currentIndex();
282
283 switch (cursorAction) {
284 case MoveLeft:
285 case MoveUp:
286 if (current.row() > 0)
287 current = model()->index(current.row() - 1, current.column(),
288 rootIndex());
289 else
290 current = model()->index(0, current.column(), rootIndex());
291 break;
292 case MoveRight:
293 case MoveDown:
294 if (current.row() < rows(current) - 1)
295 current = model()->index(current.row() + 1, current.column(),
296 rootIndex());
297 else
298 current = model()->index(rows(current) - 1, current.column(),
299 rootIndex());
300 break;
301 default:
302 break;
303 }
304
305 viewport()->update();
306 return current;
307}
308
309void PieView::paintEvent(QPaintEvent *event)
310{
311 QItemSelectionModel *selections = selectionModel();
312 QStyleOptionViewItem option;
313 initViewItemOption(&option);
314
315 QBrush background = option.palette.base();
316 QPen foreground(option.palette.color(QPalette::WindowText));
317
318 QPainter painter(viewport());
319 painter.setRenderHint(QPainter::Antialiasing);
320
321 painter.fillRect(event->rect(), background);
322 painter.setPen(foreground);
323
324 // Viewport rectangles
325 QRect pieRect = QRect(margin, margin, pieSize, pieSize);
326
327 if (validItems <= 0)
328 return;
329
330 painter.save();
331 painter.translate(pieRect.x() - horizontalScrollBar()->value(),
332 pieRect.y() - verticalScrollBar()->value());
333 painter.drawEllipse(0, 0, pieSize, pieSize);
334 double startAngle = 0.0;
335 int row;
336
337 for (row = 0; row < model()->rowCount(rootIndex()); ++row) {
338 QModelIndex index = model()->index(row, 1, rootIndex());
339 double value = model()->data(index).toDouble();
340
341 if (value > 0.0) {
342 double angle = 360 * value / totalValue;
343
344 QModelIndex colorIndex = model()->index(row, 0, rootIndex());
345 QColor color = QColor(model()->data(colorIndex, Qt::DecorationRole).toString());
346
347 if (currentIndex() == index)
348 painter.setBrush(QBrush(color, Qt::Dense4Pattern));
349 else if (selections->isSelected(index))
350 painter.setBrush(QBrush(color, Qt::Dense3Pattern));
351 else
352 painter.setBrush(QBrush(color));
353
354 painter.drawPie(0, 0, pieSize, pieSize, int(startAngle*16), int(angle*16));
355
356 startAngle += angle;
357 }
358 }
359 painter.restore();
360
361 int keyNumber = 0;
362
363 for (row = 0; row < model()->rowCount(rootIndex()); ++row) {
364 QModelIndex index = model()->index(row, 1, rootIndex());
365 double value = model()->data(index).toDouble();
366
367 if (value > 0.0) {
368 QModelIndex labelIndex = model()->index(row, 0, rootIndex());
369
370 QStyleOptionViewItem option;
371 initViewItemOption(&option);
372
373 option.rect = visualRect(labelIndex);
374 if (selections->isSelected(labelIndex))
375 option.state |= QStyle::State_Selected;
376 if (currentIndex() == labelIndex)
377 option.state |= QStyle::State_HasFocus;
378 itemDelegate()->paint(&painter, option, labelIndex);
379
380 ++keyNumber;
381 }
382 }
383}
384
385void PieView::resizeEvent(QResizeEvent * /* event */)
386{
387 updateGeometries();
388}
389
390int PieView::rows(const QModelIndex &index) const
391{
392 return model()->rowCount(model()->parent(index));
393}
394
395void PieView::rowsInserted(const QModelIndex &parent, int start, int end)
396{
397 for (int row = start; row <= end; ++row) {
398 QModelIndex index = model()->index(row, 1, rootIndex());
399 double value = model()->data(index).toDouble();
400
401 if (value > 0.0) {
402 totalValue += value;
403 ++validItems;
404 }
405 }
406
407 QAbstractItemView::rowsInserted(parent, start, end);
408}
409
410void PieView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
411{
412 for (int row = start; row <= end; ++row) {
413 QModelIndex index = model()->index(row, 1, rootIndex());
414 double value = model()->data(index).toDouble();
415 if (value > 0.0) {
416 totalValue -= value;
417 --validItems;
418 }
419 }
420
421 QAbstractItemView::rowsAboutToBeRemoved(parent, start, end);
422}
423
424void PieView::scrollContentsBy(int dx, int dy)
425{
426 viewport()->scroll(dx, dy);
427}
428
429void PieView::scrollTo(const QModelIndex &index, ScrollHint)
430{
431 QRect area = viewport()->rect();
432 QRect rect = visualRect(index);
433
434 if (rect.left() < area.left()) {
435 horizontalScrollBar()->setValue(
436 horizontalScrollBar()->value() + rect.left() - area.left());
437 } else if (rect.right() > area.right()) {
438 horizontalScrollBar()->setValue(
439 horizontalScrollBar()->value() + qMin(
440 rect.right() - area.right(), rect.left() - area.left()));
441 }
442
443 if (rect.top() < area.top()) {
444 verticalScrollBar()->setValue(
445 verticalScrollBar()->value() + rect.top() - area.top());
446 } else if (rect.bottom() > area.bottom()) {
447 verticalScrollBar()->setValue(
448 verticalScrollBar()->value() + qMin(
449 rect.bottom() - area.bottom(), rect.top() - area.top()));
450 }
451
452 update();
453}
454
455/*
456 Find the indices corresponding to the extent of the selection.
457*/
458
459void PieView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)
460{
461 // Use content widget coordinates because we will use the itemRegion()
462 // function to check for intersections.
463
464 QRect contentsRect = rect.translated(
465 horizontalScrollBar()->value(),
466 verticalScrollBar()->value()).normalized();
467
468 int rows = model()->rowCount(rootIndex());
469 int columns = model()->columnCount(rootIndex());
470 QModelIndexList indexes;
471
472 for (int row = 0; row < rows; ++row) {
473 for (int column = 0; column < columns; ++column) {
474 QModelIndex index = model()->index(row, column, rootIndex());
475 QRegion region = itemRegion(index);
476 if (region.intersects(contentsRect))
477 indexes.append(index);
478 }
479 }
480
481 if (indexes.size() > 0) {
482 int firstRow = indexes.at(0).row();
483 int lastRow = firstRow;
484 int firstColumn = indexes.at(0).column();
485 int lastColumn = firstColumn;
486
487 for (int i = 1; i < indexes.size(); ++i) {
488 firstRow = qMin(firstRow, indexes.at(i).row());
489 lastRow = qMax(lastRow, indexes.at(i).row());
490 firstColumn = qMin(firstColumn, indexes.at(i).column());
491 lastColumn = qMax(lastColumn, indexes.at(i).column());
492 }
493
494 QItemSelection selection(
495 model()->index(firstRow, firstColumn, rootIndex()),
496 model()->index(lastRow, lastColumn, rootIndex()));
497 selectionModel()->select(selection, command);
498 } else {
499 QModelIndex noIndex;
500 QItemSelection selection(noIndex, noIndex);
501 selectionModel()->select(selection, command);
502 }
503
504 update();
505}
506
507void PieView::updateGeometries()
508{
509 horizontalScrollBar()->setPageStep(viewport()->width());
510 horizontalScrollBar()->setRange(0, qMax(0, 2 * totalSize - viewport()->width()));
511 verticalScrollBar()->setPageStep(viewport()->height());
512 verticalScrollBar()->setRange(0, qMax(0, totalSize - viewport()->height()));
513}
514
515int PieView::verticalOffset() const
516{
517 return verticalScrollBar()->value();
518}
519
520/*
521 Returns the position of the item in viewport coordinates.
522*/
523
524QRect PieView::visualRect(const QModelIndex &index) const
525{
526 QRect rect = itemRect(index);
527 if (!rect.isValid())
528 return rect;
529
530 return QRect(rect.left() - horizontalScrollBar()->value(),
531 rect.top() - verticalScrollBar()->value(),
532 rect.width(), rect.height());
533}
534
535/*
536 Returns a region corresponding to the selection in viewport coordinates.
537*/
538
539QRegion PieView::visualRegionForSelection(const QItemSelection &selection) const
540{
541 int ranges = selection.count();
542
543 if (ranges == 0)
544 return QRect();
545
546 QRegion region;
547 for (int i = 0; i < ranges; ++i) {
548 const QItemSelectionRange &range = selection.at(i);
549 for (int row = range.top(); row <= range.bottom(); ++row) {
550 for (int col = range.left(); col <= range.right(); ++col) {
551 QModelIndex index = model()->index(row, col, rootIndex());
552 region += visualRect(index);
553 }
554 }
555 }
556 return region;
557}
558