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 | |
55 | PieView::PieView(QWidget *parent) |
56 | : QAbstractItemView(parent) |
57 | { |
58 | horizontalScrollBar()->setRange(0, 0); |
59 | verticalScrollBar()->setRange(0, 0); |
60 | } |
61 | |
62 | void 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 | |
86 | bool 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 | |
98 | QModelIndex 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 | |
163 | bool 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 | |
173 | QRect 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 | |
211 | QRegion 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 | |
248 | int PieView::horizontalOffset() const |
249 | { |
250 | return horizontalScrollBar()->value(); |
251 | } |
252 | |
253 | void 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 | |
263 | void PieView::mouseMoveEvent(QMouseEvent *event) |
264 | { |
265 | if (rubberBand) |
266 | rubberBand->setGeometry(QRect(origin, event->position().toPoint()).normalized()); |
267 | QAbstractItemView::mouseMoveEvent(event); |
268 | } |
269 | |
270 | void PieView::mouseReleaseEvent(QMouseEvent *event) |
271 | { |
272 | QAbstractItemView::mouseReleaseEvent(event); |
273 | if (rubberBand) |
274 | rubberBand->hide(); |
275 | viewport()->update(); |
276 | } |
277 | |
278 | QModelIndex 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 | |
309 | void 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 | |
385 | void PieView::resizeEvent(QResizeEvent * /* event */) |
386 | { |
387 | updateGeometries(); |
388 | } |
389 | |
390 | int PieView::rows(const QModelIndex &index) const |
391 | { |
392 | return model()->rowCount(model()->parent(index)); |
393 | } |
394 | |
395 | void 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 | |
410 | void 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 | |
424 | void PieView::scrollContentsBy(int dx, int dy) |
425 | { |
426 | viewport()->scroll(dx, dy); |
427 | } |
428 | |
429 | void 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 | |
459 | void 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 | |
507 | void 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 | |
515 | int PieView::verticalOffset() const |
516 | { |
517 | return verticalScrollBar()->value(); |
518 | } |
519 | |
520 | /* |
521 | Returns the position of the item in viewport coordinates. |
522 | */ |
523 | |
524 | QRect 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 | |
539 | QRegion 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 | |