c501e09efa
viewOptions returned a QStyleOptionViewItem object. Such a method can never support newer versions of the option structure. Most styleable QWidget classes provide a virtual method initStyleOption that initializes the option object passed in as a pointer, e.g QFrame, QAbstractSpinBox, or QComboBox. Follow that API convention, but name it initViewItemOption, as the QStyleOptionViewItem struct contains information about the item as well as the widget itelf. This is a source incompatible change that will go unnoticed unless existing subclasses mark their overrides as 'override', or call the removed QAbstractItemView::viewOption virtual function. [ChangeLog][QtWidgets][QAbstractItemView] The virtual viewOptions method that previously returned a QStyleOptionViewItem object has been renamed to initViewItemOption, and initializes a QStyleOptionViewItem object that's passed in through a pointer. Change-Id: Ie058702aed42d77274fa3c4abb43ba302e57e348 Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
558 lines
17 KiB
C++
558 lines
17 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2016 The Qt Company Ltd.
|
|
** Contact: https://www.qt.io/licensing/
|
|
**
|
|
** This file is part of the examples of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:BSD$
|
|
** Commercial License Usage
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
** accordance with the commercial license agreement provided with the
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
**
|
|
** BSD License Usage
|
|
** Alternatively, you may use this file under the terms of the BSD license
|
|
** as follows:
|
|
**
|
|
** "Redistribution and use in source and binary forms, with or without
|
|
** modification, are permitted provided that the following conditions are
|
|
** met:
|
|
** * Redistributions of source code must retain the above copyright
|
|
** notice, this list of conditions and the following disclaimer.
|
|
** * Redistributions in binary form must reproduce the above copyright
|
|
** notice, this list of conditions and the following disclaimer in
|
|
** the documentation and/or other materials provided with the
|
|
** distribution.
|
|
** * Neither the name of The Qt Company Ltd nor the names of its
|
|
** contributors may be used to endorse or promote products derived
|
|
** from this software without specific prior written permission.
|
|
**
|
|
**
|
|
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
|
**
|
|
** $QT_END_LICENSE$
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "pieview.h"
|
|
|
|
#include <QtWidgets>
|
|
|
|
PieView::PieView(QWidget *parent)
|
|
: QAbstractItemView(parent)
|
|
{
|
|
horizontalScrollBar()->setRange(0, 0);
|
|
verticalScrollBar()->setRange(0, 0);
|
|
}
|
|
|
|
void PieView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
|
|
const QList<int> &roles)
|
|
{
|
|
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
|
|
|
|
if (!roles.contains(Qt::DisplayRole))
|
|
return;
|
|
|
|
validItems = 0;
|
|
totalValue = 0.0;
|
|
|
|
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
|
|
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(index, Qt::DisplayRole).toDouble();
|
|
|
|
if (value > 0.0) {
|
|
totalValue += value;
|
|
validItems++;
|
|
}
|
|
}
|
|
viewport()->update();
|
|
}
|
|
|
|
bool PieView::edit(const QModelIndex &index, EditTrigger trigger, QEvent *event)
|
|
{
|
|
if (index.column() == 0)
|
|
return QAbstractItemView::edit(index, trigger, event);
|
|
else
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Returns the item that covers the coordinate given in the view.
|
|
*/
|
|
|
|
QModelIndex PieView::indexAt(const QPoint &point) const
|
|
{
|
|
if (validItems == 0)
|
|
return QModelIndex();
|
|
|
|
// Transform the view coordinates into contents widget coordinates.
|
|
int wx = point.x() + horizontalScrollBar()->value();
|
|
int wy = point.y() + verticalScrollBar()->value();
|
|
|
|
if (wx < totalSize) {
|
|
double cx = wx - totalSize / 2;
|
|
double cy = totalSize / 2 - wy; // positive cy for items above the center
|
|
|
|
// Determine the distance from the center point of the pie chart.
|
|
double d = std::sqrt(std::pow(cx, 2) + std::pow(cy, 2));
|
|
|
|
if (d == 0 || d > pieSize / 2)
|
|
return QModelIndex();
|
|
|
|
// Determine the angle of the point.
|
|
double angle = qRadiansToDegrees(std::atan2(cy, cx));
|
|
if (angle < 0)
|
|
angle = 360 + angle;
|
|
|
|
// Find the relevant slice of the pie.
|
|
double startAngle = 0.0;
|
|
|
|
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
|
|
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(index).toDouble();
|
|
|
|
if (value > 0.0) {
|
|
double sliceAngle = 360 * value / totalValue;
|
|
|
|
if (angle >= startAngle && angle < (startAngle + sliceAngle))
|
|
return model()->index(row, 1, rootIndex());
|
|
|
|
startAngle += sliceAngle;
|
|
}
|
|
}
|
|
} else {
|
|
QStyleOptionViewItem option;
|
|
initViewItemOption(&option);
|
|
double itemHeight = QFontMetrics(option.font).height();
|
|
int listItem = int((wy - margin) / itemHeight);
|
|
int validRow = 0;
|
|
|
|
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
|
|
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
if (model()->data(index).toDouble() > 0.0) {
|
|
|
|
if (listItem == validRow)
|
|
return model()->index(row, 0, rootIndex());
|
|
|
|
// Update the list index that corresponds to the next valid row.
|
|
++validRow;
|
|
}
|
|
}
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
bool PieView::isIndexHidden(const QModelIndex & /*index*/) const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Returns the rectangle of the item at position \a index in the
|
|
model. The rectangle is in contents coordinates.
|
|
*/
|
|
|
|
QRect PieView::itemRect(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid())
|
|
return QRect();
|
|
|
|
// Check whether the index's row is in the list of rows represented
|
|
// by slices.
|
|
QModelIndex valueIndex;
|
|
|
|
if (index.column() != 1)
|
|
valueIndex = model()->index(index.row(), 1, rootIndex());
|
|
else
|
|
valueIndex = index;
|
|
|
|
if (model()->data(valueIndex).toDouble() <= 0.0)
|
|
return QRect();
|
|
|
|
int listItem = 0;
|
|
for (int row = index.row()-1; row >= 0; --row) {
|
|
if (model()->data(model()->index(row, 1, rootIndex())).toDouble() > 0.0)
|
|
listItem++;
|
|
}
|
|
|
|
switch (index.column()) {
|
|
case 0: {
|
|
QStyleOptionViewItem option;
|
|
initViewItemOption(&option);
|
|
const qreal itemHeight = QFontMetricsF(option.font).height();
|
|
return QRect(totalSize,
|
|
qRound(margin + listItem * itemHeight),
|
|
totalSize - margin, qRound(itemHeight));
|
|
}
|
|
case 1:
|
|
return viewport()->rect();
|
|
}
|
|
return QRect();
|
|
}
|
|
|
|
QRegion PieView::itemRegion(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid())
|
|
return QRegion();
|
|
|
|
if (index.column() != 1)
|
|
return itemRect(index);
|
|
|
|
if (model()->data(index).toDouble() <= 0.0)
|
|
return QRegion();
|
|
|
|
double startAngle = 0.0;
|
|
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
|
|
|
|
QModelIndex sliceIndex = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(sliceIndex).toDouble();
|
|
|
|
if (value > 0.0) {
|
|
double angle = 360 * value / totalValue;
|
|
|
|
if (sliceIndex == index) {
|
|
QPainterPath slicePath;
|
|
slicePath.moveTo(totalSize / 2, totalSize / 2);
|
|
slicePath.arcTo(margin, margin, margin + pieSize, margin + pieSize,
|
|
startAngle, angle);
|
|
slicePath.closeSubpath();
|
|
|
|
return QRegion(slicePath.toFillPolygon().toPolygon());
|
|
}
|
|
|
|
startAngle += angle;
|
|
}
|
|
}
|
|
|
|
return QRegion();
|
|
}
|
|
|
|
int PieView::horizontalOffset() const
|
|
{
|
|
return horizontalScrollBar()->value();
|
|
}
|
|
|
|
void PieView::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
QAbstractItemView::mousePressEvent(event);
|
|
origin = event->position().toPoint();
|
|
if (!rubberBand)
|
|
rubberBand = new QRubberBand(QRubberBand::Rectangle, viewport());
|
|
rubberBand->setGeometry(QRect(origin, QSize()));
|
|
rubberBand->show();
|
|
}
|
|
|
|
void PieView::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
if (rubberBand)
|
|
rubberBand->setGeometry(QRect(origin, event->position().toPoint()).normalized());
|
|
QAbstractItemView::mouseMoveEvent(event);
|
|
}
|
|
|
|
void PieView::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
QAbstractItemView::mouseReleaseEvent(event);
|
|
if (rubberBand)
|
|
rubberBand->hide();
|
|
viewport()->update();
|
|
}
|
|
|
|
QModelIndex PieView::moveCursor(QAbstractItemView::CursorAction cursorAction,
|
|
Qt::KeyboardModifiers /*modifiers*/)
|
|
{
|
|
QModelIndex current = currentIndex();
|
|
|
|
switch (cursorAction) {
|
|
case MoveLeft:
|
|
case MoveUp:
|
|
if (current.row() > 0)
|
|
current = model()->index(current.row() - 1, current.column(),
|
|
rootIndex());
|
|
else
|
|
current = model()->index(0, current.column(), rootIndex());
|
|
break;
|
|
case MoveRight:
|
|
case MoveDown:
|
|
if (current.row() < rows(current) - 1)
|
|
current = model()->index(current.row() + 1, current.column(),
|
|
rootIndex());
|
|
else
|
|
current = model()->index(rows(current) - 1, current.column(),
|
|
rootIndex());
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
viewport()->update();
|
|
return current;
|
|
}
|
|
|
|
void PieView::paintEvent(QPaintEvent *event)
|
|
{
|
|
QItemSelectionModel *selections = selectionModel();
|
|
QStyleOptionViewItem option;
|
|
initViewItemOption(&option);
|
|
|
|
QBrush background = option.palette.base();
|
|
QPen foreground(option.palette.color(QPalette::WindowText));
|
|
|
|
QPainter painter(viewport());
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
painter.fillRect(event->rect(), background);
|
|
painter.setPen(foreground);
|
|
|
|
// Viewport rectangles
|
|
QRect pieRect = QRect(margin, margin, pieSize, pieSize);
|
|
|
|
if (validItems <= 0)
|
|
return;
|
|
|
|
painter.save();
|
|
painter.translate(pieRect.x() - horizontalScrollBar()->value(),
|
|
pieRect.y() - verticalScrollBar()->value());
|
|
painter.drawEllipse(0, 0, pieSize, pieSize);
|
|
double startAngle = 0.0;
|
|
int row;
|
|
|
|
for (row = 0; row < model()->rowCount(rootIndex()); ++row) {
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(index).toDouble();
|
|
|
|
if (value > 0.0) {
|
|
double angle = 360 * value / totalValue;
|
|
|
|
QModelIndex colorIndex = model()->index(row, 0, rootIndex());
|
|
QColor color = QColor(model()->data(colorIndex, Qt::DecorationRole).toString());
|
|
|
|
if (currentIndex() == index)
|
|
painter.setBrush(QBrush(color, Qt::Dense4Pattern));
|
|
else if (selections->isSelected(index))
|
|
painter.setBrush(QBrush(color, Qt::Dense3Pattern));
|
|
else
|
|
painter.setBrush(QBrush(color));
|
|
|
|
painter.drawPie(0, 0, pieSize, pieSize, int(startAngle*16), int(angle*16));
|
|
|
|
startAngle += angle;
|
|
}
|
|
}
|
|
painter.restore();
|
|
|
|
int keyNumber = 0;
|
|
|
|
for (row = 0; row < model()->rowCount(rootIndex()); ++row) {
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(index).toDouble();
|
|
|
|
if (value > 0.0) {
|
|
QModelIndex labelIndex = model()->index(row, 0, rootIndex());
|
|
|
|
QStyleOptionViewItem option;
|
|
initViewItemOption(&option);
|
|
|
|
option.rect = visualRect(labelIndex);
|
|
if (selections->isSelected(labelIndex))
|
|
option.state |= QStyle::State_Selected;
|
|
if (currentIndex() == labelIndex)
|
|
option.state |= QStyle::State_HasFocus;
|
|
itemDelegate()->paint(&painter, option, labelIndex);
|
|
|
|
++keyNumber;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PieView::resizeEvent(QResizeEvent * /* event */)
|
|
{
|
|
updateGeometries();
|
|
}
|
|
|
|
int PieView::rows(const QModelIndex &index) const
|
|
{
|
|
return model()->rowCount(model()->parent(index));
|
|
}
|
|
|
|
void PieView::rowsInserted(const QModelIndex &parent, int start, int end)
|
|
{
|
|
for (int row = start; row <= end; ++row) {
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(index).toDouble();
|
|
|
|
if (value > 0.0) {
|
|
totalValue += value;
|
|
++validItems;
|
|
}
|
|
}
|
|
|
|
QAbstractItemView::rowsInserted(parent, start, end);
|
|
}
|
|
|
|
void PieView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
|
|
{
|
|
for (int row = start; row <= end; ++row) {
|
|
QModelIndex index = model()->index(row, 1, rootIndex());
|
|
double value = model()->data(index).toDouble();
|
|
if (value > 0.0) {
|
|
totalValue -= value;
|
|
--validItems;
|
|
}
|
|
}
|
|
|
|
QAbstractItemView::rowsAboutToBeRemoved(parent, start, end);
|
|
}
|
|
|
|
void PieView::scrollContentsBy(int dx, int dy)
|
|
{
|
|
viewport()->scroll(dx, dy);
|
|
}
|
|
|
|
void PieView::scrollTo(const QModelIndex &index, ScrollHint)
|
|
{
|
|
QRect area = viewport()->rect();
|
|
QRect rect = visualRect(index);
|
|
|
|
if (rect.left() < area.left()) {
|
|
horizontalScrollBar()->setValue(
|
|
horizontalScrollBar()->value() + rect.left() - area.left());
|
|
} else if (rect.right() > area.right()) {
|
|
horizontalScrollBar()->setValue(
|
|
horizontalScrollBar()->value() + qMin(
|
|
rect.right() - area.right(), rect.left() - area.left()));
|
|
}
|
|
|
|
if (rect.top() < area.top()) {
|
|
verticalScrollBar()->setValue(
|
|
verticalScrollBar()->value() + rect.top() - area.top());
|
|
} else if (rect.bottom() > area.bottom()) {
|
|
verticalScrollBar()->setValue(
|
|
verticalScrollBar()->value() + qMin(
|
|
rect.bottom() - area.bottom(), rect.top() - area.top()));
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
/*
|
|
Find the indices corresponding to the extent of the selection.
|
|
*/
|
|
|
|
void PieView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)
|
|
{
|
|
// Use content widget coordinates because we will use the itemRegion()
|
|
// function to check for intersections.
|
|
|
|
QRect contentsRect = rect.translated(
|
|
horizontalScrollBar()->value(),
|
|
verticalScrollBar()->value()).normalized();
|
|
|
|
int rows = model()->rowCount(rootIndex());
|
|
int columns = model()->columnCount(rootIndex());
|
|
QModelIndexList indexes;
|
|
|
|
for (int row = 0; row < rows; ++row) {
|
|
for (int column = 0; column < columns; ++column) {
|
|
QModelIndex index = model()->index(row, column, rootIndex());
|
|
QRegion region = itemRegion(index);
|
|
if (region.intersects(contentsRect))
|
|
indexes.append(index);
|
|
}
|
|
}
|
|
|
|
if (indexes.size() > 0) {
|
|
int firstRow = indexes.at(0).row();
|
|
int lastRow = firstRow;
|
|
int firstColumn = indexes.at(0).column();
|
|
int lastColumn = firstColumn;
|
|
|
|
for (int i = 1; i < indexes.size(); ++i) {
|
|
firstRow = qMin(firstRow, indexes.at(i).row());
|
|
lastRow = qMax(lastRow, indexes.at(i).row());
|
|
firstColumn = qMin(firstColumn, indexes.at(i).column());
|
|
lastColumn = qMax(lastColumn, indexes.at(i).column());
|
|
}
|
|
|
|
QItemSelection selection(
|
|
model()->index(firstRow, firstColumn, rootIndex()),
|
|
model()->index(lastRow, lastColumn, rootIndex()));
|
|
selectionModel()->select(selection, command);
|
|
} else {
|
|
QModelIndex noIndex;
|
|
QItemSelection selection(noIndex, noIndex);
|
|
selectionModel()->select(selection, command);
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void PieView::updateGeometries()
|
|
{
|
|
horizontalScrollBar()->setPageStep(viewport()->width());
|
|
horizontalScrollBar()->setRange(0, qMax(0, 2 * totalSize - viewport()->width()));
|
|
verticalScrollBar()->setPageStep(viewport()->height());
|
|
verticalScrollBar()->setRange(0, qMax(0, totalSize - viewport()->height()));
|
|
}
|
|
|
|
int PieView::verticalOffset() const
|
|
{
|
|
return verticalScrollBar()->value();
|
|
}
|
|
|
|
/*
|
|
Returns the position of the item in viewport coordinates.
|
|
*/
|
|
|
|
QRect PieView::visualRect(const QModelIndex &index) const
|
|
{
|
|
QRect rect = itemRect(index);
|
|
if (!rect.isValid())
|
|
return rect;
|
|
|
|
return QRect(rect.left() - horizontalScrollBar()->value(),
|
|
rect.top() - verticalScrollBar()->value(),
|
|
rect.width(), rect.height());
|
|
}
|
|
|
|
/*
|
|
Returns a region corresponding to the selection in viewport coordinates.
|
|
*/
|
|
|
|
QRegion PieView::visualRegionForSelection(const QItemSelection &selection) const
|
|
{
|
|
int ranges = selection.count();
|
|
|
|
if (ranges == 0)
|
|
return QRect();
|
|
|
|
QRegion region;
|
|
for (int i = 0; i < ranges; ++i) {
|
|
const QItemSelectionRange &range = selection.at(i);
|
|
for (int row = range.top(); row <= range.bottom(); ++row) {
|
|
for (int col = range.left(); col <= range.right(); ++col) {
|
|
QModelIndex index = model()->index(row, col, rootIndex());
|
|
region += visualRect(index);
|
|
}
|
|
}
|
|
}
|
|
return region;
|
|
}
|