PieView
package PieView;
use strict;
use warnings;
use List::Util qw(min max);
use Math::Trig;
use QtCore4;
use QtGui4;
use QtCore4::isa qw(Qt::AbstractItemView);
use QtCore4::slots
dataChanged => ['const Qt::ModelIndex&', 'const Qt::ModelIndex&'],
rowsInserted => ['const Qt::ModelIndex&', 'int', 'int'],
rowsAboutToBeRemoved => ['const Qt::ModelIndex&', 'int', 'int'];
use constant { M_PI => 3.1415927 };
sub NEW {
my ( $class, $parent ) = @_;
$class->SUPER::NEW( $parent );
this->horizontalScrollBar()->setRange(0, 0);
this->verticalScrollBar()->setRange(0, 0);
my $margin = 8;
this->{margin} = $margin;
my $totalSize = 300;
this->{totalSize} = $totalSize;
this->{pieSize} = $totalSize - 2 * $margin;
this->{validItems} = 0;
this->{totalValue} = 0.0;
this->{rubberBand} = 0;
}
sub dataChanged {
my ($topLeft, $bottomRight) = @_;
this->SUPER::dataChanged($topLeft, $bottomRight);
my $validItems = 0;
my $totalValue = 0.0;
foreach my $row (0..this->model()->rowCount(this->rootIndex())) {
my $index = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($index)->toDouble();
if ($value > 0.0) {
$totalValue += $value;
$validItems++;
}
}
this->{validItems} = $validItems;
this->{totalValue} = $totalValue;
this->viewport()->update();
}
sub edit {
my ($index, $trigger, $event) = @_;
if ($index->column() == 0) {
return this->SUPER::edit($index, $trigger, $event);
}
else {
return 0;
}
}
sub indexAt {
my ($point) = @_;
my $totalSize = this->{totalSize};
if (this->{validItems} == 0) {
return Qt::ModelIndex();
}
# Transform the view coordinates into contents widget coordinates.
my $wx = $point->x() + this->horizontalScrollBar()->value();
my $wy = $point->y() + this->verticalScrollBar()->value();
if ($wx < $totalSize) {
my $cx = $wx - $totalSize/2;
my $cy = $totalSize/2 - $wy; # positive cy for items above the center
# Determine the distance from the center point of the pie chart.
my $d = (($cx**2) + ($cy**2))**0.5;
if ($d == 0 || $d > this->{pieSize}/2) {
return Qt::ModelIndex();
}
# Determine the angle of the point.
my $angle = (180 / M_PI) * acos($cx/$d);
if ($cy < 0) {
$angle = 360 - $angle;
}
# Find the relevant slice of the pie.
my $startAngle = 0.0;
foreach my $row (0..this->model()->rowCount(this->rootIndex())) {
my $index = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($index)->toDouble();
if ($value > 0.0) {
my $sliceAngle = 360*$value/this->{totalValue};
if ($angle >= $startAngle && $angle < ($startAngle + $sliceAngle)) {
return this->model()->index($row, 1, this->rootIndex());
}
$startAngle += $sliceAngle;
}
}
} else {
my $itemHeight = Qt::FontMetrics(this->viewOptions()->font)->height();
my $listItem = int(($wy - this->{margin}) / $itemHeight);
my $validRow = 0;
foreach my $row (0..this->model()->rowCount(this->rootIndex())) {
my $index = this->model()->index($row, 1, this->rootIndex());
if (this->model()->data($index)->toDouble() > 0.0) {
if ($listItem == $validRow) {
return this->model()->index($row, 0, this->rootIndex());
}
# Update the list index that corresponds to the next valid row.
$validRow++;
}
}
}
return Qt::ModelIndex();
}
sub isIndexHidden {
return 0;
}
sub itemRect {
my ($index) = @_;
if (!$index->isValid()) {
return Qt::Rect();
}
# Check whether the index's row is in the list of rows represented
# by slices.
my $valueIndex;
if ($index->column() != 1) {
$valueIndex = this->model()->index($index->row(), 1, this->rootIndex());
}
else {
$valueIndex = $index;
}
if (this->model()->data($valueIndex)->toDouble() > 0.0) {
my $listItem = 0;
for (my $row = $index->row()-1; $row >= 0; --$row) {
if (this->model()->data(this->model()->index($row, 1, this->rootIndex()))->toDouble() > 0.0) {
$listItem++;
}
}
my $itemHeight;
if ($index->column() == 0) {
$itemHeight = Qt::FontMetrics(this->viewOptions()->font)->height();
return Qt::Rect(this->{totalSize},
int(this->{margin} + $listItem*$itemHeight),
this->{totalSize} - this->{margin}, int($itemHeight));
}
elsif ($index->column() == 1) {
return this->viewport()->rect();
}
}
return Qt::Rect();
}
sub itemRegion {
my ($index) = @_;
if (!$index->isValid()) {
return Qt::Region();
}
if ($index->column() != 1) {
return itemRect($index);
}
if (this->model()->data($index)->toDouble() <= 0.0) {
return Qt::Region();
}
my $startAngle = 0.0;
foreach my $row (0..this->model()->rowCount(this->rootIndex())) {
my $sliceIndex = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($sliceIndex)->toDouble();
if ($value > 0.0) {
my $angle = 360*$value/this->{totalValue};
if ($sliceIndex == $index) {
my $slicePath = Qt::PainterPath();
my $totalSize = this->{totalSize};
my $margin = this->{margin};
my $pieSize = this->{pieSize};
$slicePath->moveTo($totalSize/2, $totalSize/2);
$slicePath->arcTo($margin, $margin, $margin+$pieSize, $margin+$pieSize,
$startAngle, $angle);
$slicePath->closeSubpath();
return Qt::Region($slicePath->toFillPolygon()->toPolygon());
}
$startAngle += $angle;
}
}
return Qt::Region();
}
sub horizontalOffset {
return this->horizontalScrollBar()->value();
}
sub mousePressEvent {
my ($event) = @_;
this->SUPER::mousePressEvent($event);
my $origin = Qt::Point($event->pos());
this->{origin} = $origin;
my $rubberBand = this->{rubberBand};
if (!$rubberBand) {
$rubberBand = Qt::RubberBand(Qt::RubberBand::Rectangle(), this);
this->{rubberBand} = $rubberBand;
}
$rubberBand->setGeometry(Qt::Rect($origin, Qt::Size()));
$rubberBand->show();
}
sub mouseMoveEvent {
my ($event) = @_;
my $rubberBand = this->{rubberBand};
if ($rubberBand) {
$rubberBand->setGeometry(Qt::Rect(this->{origin}, $event->pos())->normalized());
}
this->SUPER::mouseMoveEvent($event);
}
sub mouseReleaseEvent {
my ($event) = @_;
this->SUPER::mouseReleaseEvent($event);
my $rubberBand = this->{rubberBand};
if ($rubberBand) {
$rubberBand->hide();
}
this->viewport()->update();
}
sub moveCursor {
my ($cursorAction) = @_;
my $current = this->currentIndex();
if ($cursorAction == Qt::AbstractItemView::MoveLeft() ||
$cursorAction == Qt::AbstractItemView::MoveUp() ) {
if ($current->row() > 0) {
$current = this->model()->index($current->row() - 1, $current->column(),
this->rootIndex());
}
else {
$current = this->model()->index(0, $current->column(), this->rootIndex());
}
}
elsif ($cursorAction == Qt::AbstractItemView::MoveRight() ||
$cursorAction == Qt::AbstractItemView::MoveDown() ) {
if ($current->row() < this->rows($current) - 1) {
$current = this->model()->index($current->row() + 1, $current->column(),
this->rootIndex());
}
else {
$current = this->model()->index(this->rows($current) - 1, $current->column(),
this->rootIndex());
}
}
this->viewport()->update();
this->{current} = $current;
return $current;
}
sub paintEvent {
my ($event) = @_;
my $selections = this->selectionModel();
my $option = this->viewOptions();
my $state = $option->state;
my $background = $option->palette()->base();
my $foreground = Qt::Pen($option->palette->color(Qt::Palette::WindowText()));
my $textPen = Qt::Pen($option->palette->color(Qt::Palette::Text()));
my $highlightedPen = Qt::Pen($option->palette->color(Qt::Palette::HighlightedText()));
my $painter = Qt::Painter(this->viewport());
$painter->setRenderHint(Qt::Painter::Antialiasing());
$painter->fillRect($event->rect(), $background);
$painter->setPen($foreground);
# Viewport rectangles
my $margin = this->{margin};
my $totalSize = this->{totalSize};
my $pieSize = this->{pieSize};
my $pieRect = Qt::Rect($margin, $margin, $pieSize, $pieSize);
my $keyPoint = Qt::Point($totalSize - this->horizontalScrollBar()->value(),
$margin - this->verticalScrollBar()->value());
if (this->{validItems} > 0) {
$painter->save();
$painter->translate($pieRect->x() - this->horizontalScrollBar()->value(),
$pieRect->y() - this->verticalScrollBar()->value());
$painter->drawEllipse(0, 0, $pieSize, $pieSize);
my $startAngle = 0.0;
my $row;
foreach my $row ( 0..this->model()->rowCount(this->rootIndex()) ) {
my $index = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($index)->toDouble();
if ($value > 0.0) {
my $angle = 360*$value/this->{totalValue};
my $colorIndex = this->model()->index($row, 0, this->rootIndex());
my $color = Qt::Color(Qt::String(this->model()->data($colorIndex,
Qt::DecorationRole())->toString()));
if (this->currentIndex() == $index) {
$painter->setBrush(Qt::Brush($color, Qt::Dense4Pattern()));
}
elsif ($selections->isSelected($index)) {
$painter->setBrush(Qt::Brush($color, Qt::Dense3Pattern()));
}
else {
$painter->setBrush(Qt::Brush($color));
}
$painter->drawPie(0, 0, $pieSize, $pieSize, int($startAngle*16),
int($angle*16));
$startAngle += $angle;
}
}
$painter->restore();
my $keyNumber = 0;
foreach my $row ( 0..this->model()->rowCount(this->rootIndex()) ) {
my $index = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($index)->toDouble();
if ($value > 0.0) {
my $labelIndex = this->model()->index($row, 0, this->rootIndex());
# TODO: Fix this. It should be able to do
# $option->rect = this->visualRect($labelIndex), etc.
my $option = this->viewOptions();
$option->setRect( this->visualRect($labelIndex) );
if ($selections->isSelected($labelIndex)) {
$option->setState( $option->state | Qt::Style::State_Selected() );
}
if (this->currentIndex() == $labelIndex) {
$option->setState( $option->state | Qt::Style::State_HasFocus() );
}
this->itemDelegate()->paint($painter, $option, $labelIndex);
$keyNumber++;
}
}
}
$painter->end();
}
sub resizeEvent {
this->updateGeometries();
}
sub rows {
my ($index) = @_;
return this->model()->rowCount(this->model()->parent($index));
}
sub rowsInserted {
my ($parent, $start, $end) = @_;
foreach my $row ($start..$end) {
my $index = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($index)->toDouble();
if ($value > 0.0) {
this->{totalValue} += $value;
this->{validItems}++;
}
}
this->SUPER::rowsInserted($parent, $start, $end);
}
sub rowsAboutToBeRemoved {
my ($parent, $start, $end) = @_;
foreach my $row ($start..$end) {
my $index = this->model()->index($row, 1, this->rootIndex());
my $value = this->model()->data($index)->toDouble();
if ($value > 0.0) {
this->{totalValue} -= $value;
this->{validItems}--;
}
}
this->SUPER::rowsAboutToBeRemoved($parent, $start, $end);
}
sub scrollContentsBy {
my ($dx, $dy) = @_;
this->viewport()->scroll($dx, $dy);
}
sub scrollTo {
my ($index) = @_;
my $area = this->viewport()->rect();
my $rect = this->visualRect($index);
if ($rect->left() < $area->left()) {
this->horizontalScrollBar()->setValue(
this->horizontalScrollBar()->value() + $rect->left() - $area->left());
}
elsif ($rect->right() > $area->right()) {
this->horizontalScrollBar()->setValue(
this->horizontalScrollBar()->value() + min(
$rect->right() - $area->right(), $rect->left() - $area->left()));
}
if ($rect->top() < $area->top()) {
this->verticalScrollBar()->setValue(
this->verticalScrollBar()->value() + $rect->top() - $area->top());
}
elsif ($rect->bottom() > $area->bottom()) {
this->verticalScrollBar()->setValue(
this->verticalScrollBar()->value() + min(
$rect->bottom() - $area->bottom(), $rect->top() - $area->top()));
}
this->update();
}
sub setSelection {
my ($rect, $command) = @_;
# Use content widget coordinates because we will use the itemRegion()
# function to check for intersections.
my $contentsRect = $rect->translated(
this->horizontalScrollBar()->value(),
this->verticalScrollBar()->value())->normalized();
my $rows = this->model()->rowCount(this->rootIndex());
my $columns = this->model()->columnCount(this->rootIndex());
my $indexes;
foreach my $row (0..$rows) {
foreach my $column (0..$columns) {
my $index = this->model()->index($row, $column, this->rootIndex());
my $region = this->itemRegion($index);
if (!$region->intersect($contentsRect)->isEmpty()) {
push @{$indexes}, $index;
}
}
}
if ( ref $indexes eq 'ARRAY' && scalar @{$indexes} > 0) {
my $firstRow = $indexes->[0]->row();
my $lastRow = $indexes->[0]->row();
my $firstColumn = $indexes->[0]->column();
my $lastColumn = $indexes->[0]->column();
foreach my $i (1..$#{$indexes}) {
$firstRow = min($firstRow, $indexes->[$i]->row());
$lastRow = max($lastRow, $indexes->[$i]->row());
$firstColumn = min($firstColumn, $indexes->[$i]->column());
$lastColumn = max($lastColumn, $indexes->[$i]->column());
}
my $selection = Qt::ItemSelection(
this->model()->index($firstRow, $firstColumn, this->rootIndex()),
this->model()->index($lastRow, $lastColumn, this->rootIndex()));
this->selectionModel()->select($selection, $command);
} else {
my $noIndex = Qt::ModelIndex();
my $selection = Qt::ItemSelection($noIndex, $noIndex);
this->selectionModel()->select($selection, $command);
}
this->update();
}
sub updateGeometries {
this->horizontalScrollBar()->setPageStep(this->viewport()->width());
this->horizontalScrollBar()->setRange(0, max(0, 2*this->{totalSize} - this->viewport()->width()));
this->verticalScrollBar()->setPageStep(this->viewport()->height());
this->verticalScrollBar()->setRange(0, max(0, this->{totalSize} - this->viewport()->height()));
}
sub verticalOffset {
return this->verticalScrollBar()->value();
}
sub visualRect {
my ($index) = @_;
my $rect = this->itemRect($index);
if ($rect->isValid()) {
return Qt::Rect($rect->left() - this->horizontalScrollBar()->value(),
$rect->top() - this->verticalScrollBar()->value(),
$rect->width(), $rect->height());
}
else {
return $rect;
}
}
sub visualRegionForSelection {
my ($selection) = @_;
my $ranges = ref $selection eq 'ARRAY' ? scalar @{$selection} : 0;
if ($ranges == 0) {
return Qt::Region( Qt::Rect() );
}
my $region;
foreach my $i (0..$ranges) {
my $range = $selection->at($i);
foreach my $row ($range->top()..$range->bottom()) {
foreach my $col ($range->left()..$range->right()) {
my $index = this->model()->index($row, $col, this->rootIndex());
$region += visualRect($index);
}
}
}
return $region;
}
1;