| 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 | |