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(min: 0, max: 0); |
59 | verticalScrollBar()->setRange(min: 0, max: 0); |
60 | } |
61 | |
62 | void PieView::dataChanged(const QModelIndex &topLeft, |
63 | const QModelIndex &bottomRight, |
64 | const QVector<int> &roles) |
65 | { |
66 | QAbstractItemView::dataChanged(topLeft, bottomRight, roles); |
67 | |
68 | if (!roles.contains(t: Qt::DisplayRole)) |
69 | return; |
70 | |
71 | validItems = 0; |
72 | totalValue = 0.0; |
73 | |
74 | for (int row = 0; row < model()->rowCount(parent: rootIndex()); ++row) { |
75 | |
76 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
77 | double value = model()->data(index, role: Qt::DisplayRole).toDouble(); |
78 | |
79 | if (value > 0.0) { |
80 | totalValue += value; |
81 | validItems++; |
82 | } |
83 | } |
84 | viewport()->update(); |
85 | } |
86 | |
87 | bool PieView::edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) |
88 | { |
89 | if (index.column() == 0) |
90 | return QAbstractItemView::edit(index, trigger, event); |
91 | else |
92 | return false; |
93 | } |
94 | |
95 | /* |
96 | Returns the item that covers the coordinate given in the view. |
97 | */ |
98 | |
99 | QModelIndex PieView::indexAt(const QPoint &point) const |
100 | { |
101 | if (validItems == 0) |
102 | return QModelIndex(); |
103 | |
104 | // Transform the view coordinates into contents widget coordinates. |
105 | int wx = point.x() + horizontalScrollBar()->value(); |
106 | int wy = point.y() + verticalScrollBar()->value(); |
107 | |
108 | if (wx < totalSize) { |
109 | double cx = wx - totalSize / 2; |
110 | double cy = totalSize / 2 - wy; // positive cy for items above the center |
111 | |
112 | // Determine the distance from the center point of the pie chart. |
113 | double d = std::sqrt(x: std::pow(x: cx, y: 2) + std::pow(x: cy, y: 2)); |
114 | |
115 | if (d == 0 || d > pieSize / 2) |
116 | return QModelIndex(); |
117 | |
118 | // Determine the angle of the point. |
119 | double angle = qRadiansToDegrees(radians: std::atan2(y: cy, x: cx)); |
120 | if (angle < 0) |
121 | angle = 360 + angle; |
122 | |
123 | // Find the relevant slice of the pie. |
124 | double startAngle = 0.0; |
125 | |
126 | for (int row = 0; row < model()->rowCount(parent: rootIndex()); ++row) { |
127 | |
128 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
129 | double value = model()->data(index).toDouble(); |
130 | |
131 | if (value > 0.0) { |
132 | double sliceAngle = 360 * value / totalValue; |
133 | |
134 | if (angle >= startAngle && angle < (startAngle + sliceAngle)) |
135 | return model()->index(row, column: 1, parent: rootIndex()); |
136 | |
137 | startAngle += sliceAngle; |
138 | } |
139 | } |
140 | } else { |
141 | double itemHeight = QFontMetrics(viewOptions().font).height(); |
142 | int listItem = int((wy - margin) / itemHeight); |
143 | int validRow = 0; |
144 | |
145 | for (int row = 0; row < model()->rowCount(parent: rootIndex()); ++row) { |
146 | |
147 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
148 | if (model()->data(index).toDouble() > 0.0) { |
149 | |
150 | if (listItem == validRow) |
151 | return model()->index(row, column: 0, parent: rootIndex()); |
152 | |
153 | // Update the list index that corresponds to the next valid row. |
154 | ++validRow; |
155 | } |
156 | } |
157 | } |
158 | |
159 | return QModelIndex(); |
160 | } |
161 | |
162 | bool PieView::isIndexHidden(const QModelIndex & /*index*/) const |
163 | { |
164 | return false; |
165 | } |
166 | |
167 | /* |
168 | Returns the rectangle of the item at position \a index in the |
169 | model. The rectangle is in contents coordinates. |
170 | */ |
171 | |
172 | QRect PieView::itemRect(const QModelIndex &index) const |
173 | { |
174 | if (!index.isValid()) |
175 | return QRect(); |
176 | |
177 | // Check whether the index's row is in the list of rows represented |
178 | // by slices. |
179 | QModelIndex valueIndex; |
180 | |
181 | if (index.column() != 1) |
182 | valueIndex = model()->index(row: index.row(), column: 1, parent: rootIndex()); |
183 | else |
184 | valueIndex = index; |
185 | |
186 | if (model()->data(index: valueIndex).toDouble() <= 0.0) |
187 | return QRect(); |
188 | |
189 | int listItem = 0; |
190 | for (int row = index.row()-1; row >= 0; --row) { |
191 | if (model()->data(index: model()->index(row, column: 1, parent: rootIndex())).toDouble() > 0.0) |
192 | listItem++; |
193 | } |
194 | |
195 | switch (index.column()) { |
196 | case 0: { |
197 | const qreal itemHeight = QFontMetricsF(viewOptions().font).height(); |
198 | |
199 | return QRect(totalSize, |
200 | qRound(d: margin + listItem * itemHeight), |
201 | totalSize - margin, qRound(d: itemHeight)); |
202 | } |
203 | case 1: |
204 | return viewport()->rect(); |
205 | } |
206 | return QRect(); |
207 | } |
208 | |
209 | QRegion PieView::itemRegion(const QModelIndex &index) const |
210 | { |
211 | if (!index.isValid()) |
212 | return QRegion(); |
213 | |
214 | if (index.column() != 1) |
215 | return itemRect(index); |
216 | |
217 | if (model()->data(index).toDouble() <= 0.0) |
218 | return QRegion(); |
219 | |
220 | double startAngle = 0.0; |
221 | for (int row = 0; row < model()->rowCount(parent: rootIndex()); ++row) { |
222 | |
223 | QModelIndex sliceIndex = model()->index(row, column: 1, parent: rootIndex()); |
224 | double value = model()->data(index: sliceIndex).toDouble(); |
225 | |
226 | if (value > 0.0) { |
227 | double angle = 360 * value / totalValue; |
228 | |
229 | if (sliceIndex == index) { |
230 | QPainterPath slicePath; |
231 | slicePath.moveTo(x: totalSize / 2, y: totalSize / 2); |
232 | slicePath.arcTo(x: margin, y: margin, w: margin + pieSize, h: margin + pieSize, |
233 | startAngle, arcLength: angle); |
234 | slicePath.closeSubpath(); |
235 | |
236 | return QRegion(slicePath.toFillPolygon().toPolygon()); |
237 | } |
238 | |
239 | startAngle += angle; |
240 | } |
241 | } |
242 | |
243 | return QRegion(); |
244 | } |
245 | |
246 | int PieView::horizontalOffset() const |
247 | { |
248 | return horizontalScrollBar()->value(); |
249 | } |
250 | |
251 | void PieView::mousePressEvent(QMouseEvent *event) |
252 | { |
253 | QAbstractItemView::mousePressEvent(event); |
254 | origin = event->pos(); |
255 | if (!rubberBand) |
256 | rubberBand = new QRubberBand(QRubberBand::Rectangle, viewport()); |
257 | rubberBand->setGeometry(QRect(origin, QSize())); |
258 | rubberBand->show(); |
259 | } |
260 | |
261 | void PieView::mouseMoveEvent(QMouseEvent *event) |
262 | { |
263 | if (rubberBand) |
264 | rubberBand->setGeometry(QRect(origin, event->pos()).normalized()); |
265 | QAbstractItemView::mouseMoveEvent(event); |
266 | } |
267 | |
268 | void PieView::mouseReleaseEvent(QMouseEvent *event) |
269 | { |
270 | QAbstractItemView::mouseReleaseEvent(event); |
271 | if (rubberBand) |
272 | rubberBand->hide(); |
273 | viewport()->update(); |
274 | } |
275 | |
276 | QModelIndex PieView::moveCursor(QAbstractItemView::CursorAction cursorAction, |
277 | Qt::KeyboardModifiers /*modifiers*/) |
278 | { |
279 | QModelIndex current = currentIndex(); |
280 | |
281 | switch (cursorAction) { |
282 | case MoveLeft: |
283 | case MoveUp: |
284 | if (current.row() > 0) |
285 | current = model()->index(row: current.row() - 1, column: current.column(), |
286 | parent: rootIndex()); |
287 | else |
288 | current = model()->index(row: 0, column: current.column(), parent: rootIndex()); |
289 | break; |
290 | case MoveRight: |
291 | case MoveDown: |
292 | if (current.row() < rows(index: current) - 1) |
293 | current = model()->index(row: current.row() + 1, column: current.column(), |
294 | parent: rootIndex()); |
295 | else |
296 | current = model()->index(row: rows(index: current) - 1, column: current.column(), |
297 | parent: rootIndex()); |
298 | break; |
299 | default: |
300 | break; |
301 | } |
302 | |
303 | viewport()->update(); |
304 | return current; |
305 | } |
306 | |
307 | void PieView::paintEvent(QPaintEvent *event) |
308 | { |
309 | QItemSelectionModel *selections = selectionModel(); |
310 | QStyleOptionViewItem option = viewOptions(); |
311 | |
312 | QBrush background = option.palette.base(); |
313 | QPen foreground(option.palette.color(cr: QPalette::WindowText)); |
314 | |
315 | QPainter painter(viewport()); |
316 | painter.setRenderHint(hint: QPainter::Antialiasing); |
317 | |
318 | painter.fillRect(event->rect(), background); |
319 | painter.setPen(foreground); |
320 | |
321 | // Viewport rectangles |
322 | QRect pieRect = QRect(margin, margin, pieSize, pieSize); |
323 | |
324 | if (validItems <= 0) |
325 | return; |
326 | |
327 | painter.save(); |
328 | painter.translate(dx: pieRect.x() - horizontalScrollBar()->value(), |
329 | dy: pieRect.y() - verticalScrollBar()->value()); |
330 | painter.drawEllipse(x: 0, y: 0, w: pieSize, h: pieSize); |
331 | double startAngle = 0.0; |
332 | int row; |
333 | |
334 | for (row = 0; row < model()->rowCount(parent: rootIndex()); ++row) { |
335 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
336 | double value = model()->data(index).toDouble(); |
337 | |
338 | if (value > 0.0) { |
339 | double angle = 360 * value / totalValue; |
340 | |
341 | QModelIndex colorIndex = model()->index(row, column: 0, parent: rootIndex()); |
342 | QColor color = QColor(model()->data(index: colorIndex, role: Qt::DecorationRole).toString()); |
343 | |
344 | if (currentIndex() == index) |
345 | painter.setBrush(QBrush(color, Qt::Dense4Pattern)); |
346 | else if (selections->isSelected(index)) |
347 | painter.setBrush(QBrush(color, Qt::Dense3Pattern)); |
348 | else |
349 | painter.setBrush(QBrush(color)); |
350 | |
351 | painter.drawPie(x: 0, y: 0, w: pieSize, h: pieSize, a: int(startAngle*16), alen: int(angle*16)); |
352 | |
353 | startAngle += angle; |
354 | } |
355 | } |
356 | painter.restore(); |
357 | |
358 | int keyNumber = 0; |
359 | |
360 | for (row = 0; row < model()->rowCount(parent: rootIndex()); ++row) { |
361 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
362 | double value = model()->data(index).toDouble(); |
363 | |
364 | if (value > 0.0) { |
365 | QModelIndex labelIndex = model()->index(row, column: 0, parent: rootIndex()); |
366 | |
367 | QStyleOptionViewItem option = viewOptions(); |
368 | option.rect = visualRect(index: labelIndex); |
369 | if (selections->isSelected(index: labelIndex)) |
370 | option.state |= QStyle::State_Selected; |
371 | if (currentIndex() == labelIndex) |
372 | option.state |= QStyle::State_HasFocus; |
373 | itemDelegate()->paint(painter: &painter, option, index: labelIndex); |
374 | |
375 | ++keyNumber; |
376 | } |
377 | } |
378 | } |
379 | |
380 | void PieView::resizeEvent(QResizeEvent * /* event */) |
381 | { |
382 | updateGeometries(); |
383 | } |
384 | |
385 | int PieView::rows(const QModelIndex &index) const |
386 | { |
387 | return model()->rowCount(parent: model()->parent(child: index)); |
388 | } |
389 | |
390 | void PieView::rowsInserted(const QModelIndex &parent, int start, int end) |
391 | { |
392 | for (int row = start; row <= end; ++row) { |
393 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
394 | double value = model()->data(index).toDouble(); |
395 | |
396 | if (value > 0.0) { |
397 | totalValue += value; |
398 | ++validItems; |
399 | } |
400 | } |
401 | |
402 | QAbstractItemView::rowsInserted(parent, start, end); |
403 | } |
404 | |
405 | void PieView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) |
406 | { |
407 | for (int row = start; row <= end; ++row) { |
408 | QModelIndex index = model()->index(row, column: 1, parent: rootIndex()); |
409 | double value = model()->data(index).toDouble(); |
410 | if (value > 0.0) { |
411 | totalValue -= value; |
412 | --validItems; |
413 | } |
414 | } |
415 | |
416 | QAbstractItemView::rowsAboutToBeRemoved(parent, start, end); |
417 | } |
418 | |
419 | void PieView::scrollContentsBy(int dx, int dy) |
420 | { |
421 | viewport()->scroll(dx, dy); |
422 | } |
423 | |
424 | void PieView::scrollTo(const QModelIndex &index, ScrollHint) |
425 | { |
426 | QRect area = viewport()->rect(); |
427 | QRect rect = visualRect(index); |
428 | |
429 | if (rect.left() < area.left()) { |
430 | horizontalScrollBar()->setValue( |
431 | horizontalScrollBar()->value() + rect.left() - area.left()); |
432 | } else if (rect.right() > area.right()) { |
433 | horizontalScrollBar()->setValue( |
434 | horizontalScrollBar()->value() + qMin( |
435 | a: rect.right() - area.right(), b: rect.left() - area.left())); |
436 | } |
437 | |
438 | if (rect.top() < area.top()) { |
439 | verticalScrollBar()->setValue( |
440 | verticalScrollBar()->value() + rect.top() - area.top()); |
441 | } else if (rect.bottom() > area.bottom()) { |
442 | verticalScrollBar()->setValue( |
443 | verticalScrollBar()->value() + qMin( |
444 | a: rect.bottom() - area.bottom(), b: rect.top() - area.top())); |
445 | } |
446 | |
447 | update(); |
448 | } |
449 | |
450 | /* |
451 | Find the indices corresponding to the extent of the selection. |
452 | */ |
453 | |
454 | void PieView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) |
455 | { |
456 | // Use content widget coordinates because we will use the itemRegion() |
457 | // function to check for intersections. |
458 | |
459 | QRect contentsRect = rect.translated( |
460 | dx: horizontalScrollBar()->value(), |
461 | dy: verticalScrollBar()->value()).normalized(); |
462 | |
463 | int rows = model()->rowCount(parent: rootIndex()); |
464 | int columns = model()->columnCount(parent: rootIndex()); |
465 | QModelIndexList indexes; |
466 | |
467 | for (int row = 0; row < rows; ++row) { |
468 | for (int column = 0; column < columns; ++column) { |
469 | QModelIndex index = model()->index(row, column, parent: rootIndex()); |
470 | QRegion region = itemRegion(index); |
471 | if (region.intersects(r: contentsRect)) |
472 | indexes.append(t: index); |
473 | } |
474 | } |
475 | |
476 | if (indexes.size() > 0) { |
477 | int firstRow = indexes.at(i: 0).row(); |
478 | int lastRow = firstRow; |
479 | int firstColumn = indexes.at(i: 0).column(); |
480 | int lastColumn = firstColumn; |
481 | |
482 | for (int i = 1; i < indexes.size(); ++i) { |
483 | firstRow = qMin(a: firstRow, b: indexes.at(i).row()); |
484 | lastRow = qMax(a: lastRow, b: indexes.at(i).row()); |
485 | firstColumn = qMin(a: firstColumn, b: indexes.at(i).column()); |
486 | lastColumn = qMax(a: lastColumn, b: indexes.at(i).column()); |
487 | } |
488 | |
489 | QItemSelection selection( |
490 | model()->index(row: firstRow, column: firstColumn, parent: rootIndex()), |
491 | model()->index(row: lastRow, column: lastColumn, parent: rootIndex())); |
492 | selectionModel()->select(selection, command); |
493 | } else { |
494 | QModelIndex noIndex; |
495 | QItemSelection selection(noIndex, noIndex); |
496 | selectionModel()->select(selection, command); |
497 | } |
498 | |
499 | update(); |
500 | } |
501 | |
502 | void PieView::updateGeometries() |
503 | { |
504 | horizontalScrollBar()->setPageStep(viewport()->width()); |
505 | horizontalScrollBar()->setRange(min: 0, max: qMax(a: 0, b: 2 * totalSize - viewport()->width())); |
506 | verticalScrollBar()->setPageStep(viewport()->height()); |
507 | verticalScrollBar()->setRange(min: 0, max: qMax(a: 0, b: totalSize - viewport()->height())); |
508 | } |
509 | |
510 | int PieView::verticalOffset() const |
511 | { |
512 | return verticalScrollBar()->value(); |
513 | } |
514 | |
515 | /* |
516 | Returns the position of the item in viewport coordinates. |
517 | */ |
518 | |
519 | QRect PieView::visualRect(const QModelIndex &index) const |
520 | { |
521 | QRect rect = itemRect(index); |
522 | if (!rect.isValid()) |
523 | return rect; |
524 | |
525 | return QRect(rect.left() - horizontalScrollBar()->value(), |
526 | rect.top() - verticalScrollBar()->value(), |
527 | rect.width(), rect.height()); |
528 | } |
529 | |
530 | /* |
531 | Returns a region corresponding to the selection in viewport coordinates. |
532 | */ |
533 | |
534 | QRegion PieView::visualRegionForSelection(const QItemSelection &selection) const |
535 | { |
536 | int ranges = selection.count(); |
537 | |
538 | if (ranges == 0) |
539 | return QRect(); |
540 | |
541 | QRegion region; |
542 | for (int i = 0; i < ranges; ++i) { |
543 | const QItemSelectionRange &range = selection.at(i); |
544 | for (int row = range.top(); row <= range.bottom(); ++row) { |
545 | for (int col = range.left(); col <= range.right(); ++col) { |
546 | QModelIndex index = model()->index(row, column: col, parent: rootIndex()); |
547 | region += visualRect(index); |
548 | } |
549 | } |
550 | } |
551 | return region; |
552 | } |
553 | |