You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
609 lines
15 KiB
609 lines
15 KiB
/*
|
|
* File name: ktreemaptile.cpp
|
|
* Summary: High level classes for KDirStat
|
|
* License: LGPL - See file COPYING.LIB for details.
|
|
* Author: Stefan Hundhammer <sh@suse.de>
|
|
*
|
|
* Updated: 2003-01-30
|
|
*/
|
|
|
|
|
|
#include <math.h>
|
|
#include <algorithm>
|
|
|
|
#include <kapp.h>
|
|
#include <klocale.h>
|
|
#include <kglobal.h>
|
|
#include <tqimage.h>
|
|
#include <tqpainter.h>
|
|
|
|
#include "ktreemaptile.h"
|
|
#include "ktreemapview.h"
|
|
#include "kdirtreeiterators.h"
|
|
#include "kdirtreeview.h"
|
|
|
|
|
|
using namespace KDirStat;
|
|
using std::max;
|
|
using std::min;
|
|
|
|
|
|
KTreemapTile::KTreemapTile( KTreemapView * parentView,
|
|
KTreemapTile * parentTile,
|
|
KFileInfo * orig,
|
|
const TQRect & rect,
|
|
KOrientation orientation )
|
|
: TQCanvasRectangle( rect, parentView->canvas() )
|
|
, _parentView( parentView )
|
|
, _parentTile( parentTile )
|
|
, _orig( orig )
|
|
{
|
|
init();
|
|
|
|
if ( parentTile )
|
|
_cushionSurface = parentTile->cushionSurface();
|
|
|
|
createChildren( rect, orientation );
|
|
}
|
|
|
|
|
|
KTreemapTile::KTreemapTile( KTreemapView * parentView,
|
|
KTreemapTile * parentTile,
|
|
KFileInfo * orig,
|
|
const TQRect & rect,
|
|
const KCushionSurface & cushionSurface,
|
|
KOrientation orientation )
|
|
: TQCanvasRectangle( rect, parentView->canvas() )
|
|
, _parentView( parentView )
|
|
, _parentTile( parentTile )
|
|
, _orig( orig )
|
|
, _cushionSurface( cushionSurface )
|
|
{
|
|
init();
|
|
|
|
// Intentionally not copying the parent's cushion surface!
|
|
|
|
createChildren( rect, orientation );
|
|
}
|
|
|
|
|
|
KTreemapTile::~KTreemapTile()
|
|
{
|
|
// NOP
|
|
}
|
|
|
|
|
|
void
|
|
KTreemapTile::init()
|
|
{
|
|
// Set up height (z coordinate) - one level higher than the parent so this
|
|
// will be closer to the foreground.
|
|
//
|
|
// Note that this must happen before any children are created.
|
|
// I found that out the hard way. ;-)
|
|
|
|
setZ( _parentTile ? ( _parentTile->z() + 1.0 ) : 0.0 );
|
|
|
|
setBrush( TQColor( 0x60, 0x60, 0x60 ) );
|
|
setPen( Qt::NoPen );
|
|
|
|
show(); // TQCanvasItems are invisible by default!
|
|
|
|
// kdDebug() << "Creating treemap tile for " << orig << " " << rect << " size " << orig->totalSize() << endl;
|
|
}
|
|
|
|
|
|
void
|
|
KTreemapTile::createChildren( const TQRect & rect,
|
|
KOrientation orientation )
|
|
{
|
|
if ( _orig->totalSize() == 0 ) // Prevent division by zero
|
|
return;
|
|
|
|
if ( _parentView->squarify() )
|
|
createSquarifiedChildren( rect );
|
|
else
|
|
createChildrenSimple( rect, orientation );
|
|
}
|
|
|
|
|
|
void
|
|
KTreemapTile::createChildrenSimple( const TQRect & rect,
|
|
KOrientation orientation )
|
|
{
|
|
|
|
KOrientation dir = orientation;
|
|
KOrientation childDir = orientation;
|
|
|
|
if ( dir == KTreemapAuto )
|
|
dir = rect.width() > rect.height() ? KTreemapHorizontal : KTreemapVertical;
|
|
|
|
if ( orientation == KTreemapHorizontal ) childDir = KTreemapVertical;
|
|
if ( orientation == KTreemapVertical ) childDir = KTreemapHorizontal;
|
|
|
|
int offset = 0;
|
|
int size = dir == KTreemapHorizontal ? rect.width() : rect.height();
|
|
int count = 0;
|
|
double scale = (double) size / (double) _orig->totalSize();
|
|
|
|
_cushionSurface.addRidge( childDir, _cushionSurface.height(), rect );
|
|
|
|
KFileInfoSortedBySizeIterator it( _orig,
|
|
(KFileSize) ( _parentView->minTileSize() / scale ),
|
|
KDotEntryAsSubDir );
|
|
|
|
while ( *it )
|
|
{
|
|
int childSize = 0;
|
|
|
|
childSize = (int) ( scale * (*it)->totalSize() );
|
|
|
|
if ( childSize >= _parentView->minTileSize() )
|
|
{
|
|
TQRect childRect;
|
|
|
|
if ( dir == KTreemapHorizontal )
|
|
childRect = TQRect( rect.x() + offset, rect.y(), childSize, rect.height() );
|
|
else
|
|
childRect = TQRect( rect.x(), rect.y() + offset, rect.width(), childSize );
|
|
|
|
KTreemapTile * tile = new KTreemapTile( _parentView, this, *it, childRect, childDir );
|
|
CHECK_PTR( tile );
|
|
|
|
tile->cushionSurface().addRidge( dir,
|
|
_cushionSurface.height() * _parentView->heightScaleFactor(),
|
|
childRect );
|
|
|
|
offset += childSize;
|
|
}
|
|
|
|
++count;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
KTreemapTile::createSquarifiedChildren( const TQRect & rect )
|
|
{
|
|
if ( _orig->totalSize() == 0 )
|
|
{
|
|
kdError() << k_funcinfo << "Zero totalSize()" << endl;
|
|
return;
|
|
}
|
|
|
|
double scale = rect.width() * (double) rect.height() / _orig->totalSize();
|
|
KFileSize minSize = (KFileSize) ( _parentView->minTileSize() / scale );
|
|
|
|
#if 0
|
|
if ( _orig->hasChildren() )
|
|
{
|
|
_cushionSurface.addRidge( KTreemapHorizontal, _cushionSurface.height(), rect );
|
|
_cushionSurface.addRidge( KTreemapVertical, _cushionSurface.height(), rect );
|
|
}
|
|
#endif
|
|
|
|
KFileInfoSortedBySizeIterator it( _orig, minSize, KDotEntryAsSubDir );
|
|
TQRect childrenRect = rect;
|
|
|
|
while ( *it )
|
|
{
|
|
KFileInfoList row = squarify( childrenRect, scale, it );
|
|
childrenRect = layoutRow( childrenRect, scale, row );
|
|
}
|
|
}
|
|
|
|
|
|
KFileInfoList
|
|
KTreemapTile::squarify( const TQRect & rect,
|
|
double scale,
|
|
KFileInfoSortedBySizeIterator & it )
|
|
{
|
|
// kdDebug() << "squarify() " << _orig << " " << rect << endl;
|
|
|
|
KFileInfoList row;
|
|
int length = max( rect.width(), rect.height() );
|
|
|
|
if ( length == 0 ) // Sanity check
|
|
{
|
|
kdWarning() << k_funcinfo << "Zero length" << endl;
|
|
|
|
if ( *it ) // Prevent endless loop in case of error:
|
|
++it; // Advance iterator.
|
|
|
|
return row;
|
|
}
|
|
|
|
|
|
bool improvingAspectRatio = true;
|
|
double lastWorstAspectRatio = -1.0;
|
|
double sum = 0;
|
|
|
|
// This is a bit ugly, but doing all calculations in the 'size' dimension
|
|
// is more efficient here since that requires only one scaling before
|
|
// doing all other calculations in the loop.
|
|
const double scaledLengthSquare = length * (double) length / scale;
|
|
|
|
while ( *it && improvingAspectRatio )
|
|
{
|
|
sum += (*it)->totalSize();
|
|
|
|
if ( ! row.isEmpty() && sum != 0 && (*it)->totalSize() != 0 )
|
|
{
|
|
double sumSquare = sum * sum;
|
|
double worstAspectRatio = max( scaledLengthSquare * row.first()->totalSize() / sumSquare,
|
|
sumSquare / ( scaledLengthSquare * (*it)->totalSize() ) );
|
|
|
|
if ( lastWorstAspectRatio >= 0.0 &&
|
|
worstAspectRatio > lastWorstAspectRatio )
|
|
{
|
|
improvingAspectRatio = false;
|
|
}
|
|
|
|
lastWorstAspectRatio = worstAspectRatio;
|
|
}
|
|
|
|
if ( improvingAspectRatio )
|
|
{
|
|
// kdDebug() << "Adding " << *it << " size " << (*it)->totalSize() << endl;
|
|
row.append( *it );
|
|
++it;
|
|
}
|
|
else
|
|
{
|
|
// kdDebug() << "Getting worse after adding " << *it << " size " << (*it)->totalSize() << endl;
|
|
}
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
|
|
|
|
TQRect
|
|
KTreemapTile::layoutRow( const TQRect & rect,
|
|
double scale,
|
|
KFileInfoList & row )
|
|
{
|
|
if ( row.isEmpty() )
|
|
return rect;
|
|
|
|
// Determine the direction in which to subdivide.
|
|
// We always use the longer side of the rectangle.
|
|
KOrientation dir = rect.width() > rect.height() ? KTreemapHorizontal : KTreemapVertical;
|
|
|
|
// This row's primary length is the longer one.
|
|
int primary = max( rect.width(), rect.height() );
|
|
|
|
// This row's secondary length is determined by the area (the number of
|
|
// pixels) to be allocated for all of the row's items.
|
|
KFileSize sum = row.sumTotalSizes();
|
|
int secondary = (int) ( sum * scale / primary );
|
|
|
|
if ( sum == 0 ) // Prevent division by zero.
|
|
return rect;
|
|
|
|
if ( secondary < _parentView->minTileSize() ) // We don't want tiles that small.
|
|
return rect;
|
|
|
|
|
|
// Set up a cushion surface for this layout row:
|
|
// Add another ridge perpendicular to the row's direction
|
|
// that optically groups this row's tiles together.
|
|
|
|
KCushionSurface rowCushionSurface = _cushionSurface;
|
|
|
|
rowCushionSurface.addRidge( dir == KTreemapHorizontal ? KTreemapVertical : KTreemapHorizontal,
|
|
_cushionSurface.height() * _parentView->heightScaleFactor(),
|
|
rect );
|
|
|
|
int offset = 0;
|
|
int remaining = primary;
|
|
KFileInfoListIterator it( row );
|
|
|
|
while ( *it )
|
|
{
|
|
int childSize = (int) ( (*it)->totalSize() / (double) sum * primary + 0.5 );
|
|
|
|
if ( childSize > remaining ) // Prevent overflow because of accumulated rounding errors
|
|
childSize = remaining;
|
|
|
|
remaining -= childSize;
|
|
|
|
if ( childSize >= _parentView->minTileSize() )
|
|
{
|
|
TQRect childRect;
|
|
|
|
if ( dir == KTreemapHorizontal )
|
|
childRect = TQRect( rect.x() + offset, rect.y(), childSize, secondary );
|
|
else
|
|
childRect = TQRect( rect.x(), rect.y() + offset, secondary, childSize );
|
|
|
|
KTreemapTile * tile = new KTreemapTile( _parentView, this, *it, childRect, rowCushionSurface );
|
|
CHECK_PTR( tile );
|
|
|
|
tile->cushionSurface().addRidge( dir,
|
|
rowCushionSurface.height() * _parentView->heightScaleFactor(),
|
|
childRect );
|
|
offset += childSize;
|
|
}
|
|
|
|
++it;
|
|
}
|
|
|
|
|
|
// Subtract the layouted area from the rectangle.
|
|
|
|
TQRect newRect;
|
|
|
|
if ( dir == KTreemapHorizontal )
|
|
newRect = TQRect( rect.x(), rect.y() + secondary, rect.width(), rect.height() - secondary );
|
|
else
|
|
newRect = TQRect( rect.x() + secondary, rect.y(), rect.width() - secondary, rect.height() );
|
|
|
|
// kdDebug() << "Left over:" << " " << newRect << " " << _orig << endl;
|
|
|
|
return newRect;
|
|
}
|
|
|
|
|
|
void
|
|
KTreemapTile::drawShape( TQPainter & painter )
|
|
{
|
|
// kdDebug() << k_funcinfo << endl;
|
|
|
|
TQSize size = rect().size();
|
|
|
|
if ( size.height() < 1 || size.width() < 1 )
|
|
return;
|
|
|
|
|
|
if ( _parentView->doCushionShading() )
|
|
{
|
|
if ( _orig->isDir() || _orig->isDotEntry() )
|
|
{
|
|
TQCanvasRectangle::drawShape( painter );
|
|
}
|
|
else
|
|
{
|
|
if ( _cushion.isNull() )
|
|
_cushion = renderCushion();
|
|
|
|
TQRect rect = TQCanvasRectangle::rect();
|
|
|
|
if ( ! _cushion.isNull() )
|
|
painter.drawPixmap( rect, _cushion );
|
|
|
|
if ( _parentView->forceCushionGrid() )
|
|
{
|
|
// Draw a clearly visible boundary
|
|
|
|
painter.setPen( TQPen( _parentView->cushionGridColor(), 1 ) );
|
|
|
|
if ( rect.x() > 0 )
|
|
painter.drawLine( rect.topLeft(), rect.bottomLeft() + TQPoint( 0, 1 ) );
|
|
|
|
if ( rect.y() > 0 )
|
|
painter.drawLine( rect.topLeft(), rect.topRight() + TQPoint( 1, 0 ) );
|
|
}
|
|
}
|
|
}
|
|
else // No cushion shading, use plain tiles
|
|
{
|
|
painter.setPen( TQPen( _parentView->outlineColor(), 1 ) );
|
|
|
|
if ( _orig->isDir() || _orig->isDotEntry() )
|
|
painter.setBrush( _parentView->dirFillColor() );
|
|
else
|
|
{
|
|
painter.setBrush( _parentView->tileColor( _orig ) );
|
|
#if 0
|
|
painter.setBrush( _parentView->fileFillColor() );
|
|
#endif
|
|
}
|
|
|
|
TQCanvasRectangle::drawShape( painter );
|
|
}
|
|
}
|
|
|
|
|
|
TQPixmap
|
|
KTreemapTile::renderCushion()
|
|
{
|
|
TQRect rect = TQCanvasRectangle::rect();
|
|
|
|
if ( rect.width() < 1 || rect.height() < 1 )
|
|
return TQPixmap();
|
|
|
|
// kdDebug() << k_funcinfo << endl;
|
|
|
|
double nx;
|
|
double ny;
|
|
double cosa;
|
|
int x, y;
|
|
int red, green, blue;
|
|
|
|
|
|
// Cache some values. They are used for each loop iteration, so let's try
|
|
// to keep multiple indirect references down.
|
|
|
|
int ambientLight = parentView()->ambientLight();
|
|
double lightX = parentView()->lightX();
|
|
double lightY = parentView()->lightY();
|
|
double lightZ = parentView()->lightZ();
|
|
|
|
double xx2 = cushionSurface().xx2();
|
|
double xx1 = cushionSurface().xx1();
|
|
double yy2 = cushionSurface().yy2();
|
|
double yy1 = cushionSurface().yy1();
|
|
|
|
int x0 = rect.x();
|
|
int y0 = rect.y();
|
|
|
|
TQColor color = parentView()->tileColor( _orig );
|
|
int maxRed = max( 0, color.red() - ambientLight );
|
|
int maxGreen = max( 0, color.green() - ambientLight );
|
|
int maxBlue = max( 0, color.blue() - ambientLight );
|
|
|
|
TQImage image( rect.width(), rect.height(), 32 );
|
|
|
|
for ( y = 0; y < rect.height(); y++ )
|
|
{
|
|
for ( x = 0; x < rect.width(); x++ )
|
|
{
|
|
nx = 2.0 * xx2 * (x+x0) + xx1;
|
|
ny = 2.0 * yy2 * (y+y0) + yy1;
|
|
cosa = ( nx * lightX + ny * lightY + lightZ ) / sqrt( nx*nx + ny*ny + 1.0 );
|
|
|
|
red = (int) ( maxRed * cosa + 0.5 );
|
|
green = (int) ( maxGreen * cosa + 0.5 );
|
|
blue = (int) ( maxBlue * cosa + 0.5 );
|
|
|
|
if ( red < 0 ) red = 0;
|
|
if ( green < 0 ) green = 0;
|
|
if ( blue < 0 ) blue = 0;
|
|
|
|
red += ambientLight;
|
|
green += ambientLight;
|
|
blue += ambientLight;
|
|
|
|
image.setPixel( x, y, tqRgb( red, green, blue) );
|
|
}
|
|
}
|
|
|
|
if ( _parentView->ensureContrast() )
|
|
ensureContrast( image );
|
|
|
|
return TQPixmap( image );
|
|
}
|
|
|
|
|
|
void
|
|
KTreemapTile::ensureContrast( TQImage & image )
|
|
{
|
|
if ( image.width() > 5 )
|
|
{
|
|
// Check contrast along the right image boundary:
|
|
//
|
|
// Compare samples from the outmost boundary to samples a few pixels to
|
|
// the inside and count identical pixel values. A number of identical
|
|
// pixels are tolerated, but not too many.
|
|
|
|
int x1 = image.width() - 6;
|
|
int x2 = image.width() - 1;
|
|
int interval = max( image.height() / 10, 5 );
|
|
int sameColorCount = 0;
|
|
|
|
|
|
// Take samples
|
|
|
|
for ( int y = interval; y < image.height(); y+= interval )
|
|
{
|
|
if ( image.pixel( x1, y ) == image.pixel( x2, y ) )
|
|
sameColorCount++;
|
|
}
|
|
|
|
if ( sameColorCount * 10 > image.height() )
|
|
{
|
|
// Add a line at the right boundary
|
|
|
|
TQRgb val = contrastingColor( image.pixel( x2, image.height() / 2 ) );
|
|
|
|
for ( int y = 0; y < image.height(); y++ )
|
|
image.setPixel( x2, y, val );
|
|
}
|
|
}
|
|
|
|
|
|
if ( image.height() > 5 )
|
|
{
|
|
// Check contrast along the bottom boundary
|
|
|
|
int y1 = image.height() - 6;
|
|
int y2 = image.height() - 1;
|
|
int interval = max( image.width() / 10, 5 );
|
|
int sameColorCount = 0;
|
|
|
|
for ( int x = interval; x < image.width(); x += interval )
|
|
{
|
|
if ( image.pixel( x, y1 ) == image.pixel( x, y2 ) )
|
|
sameColorCount++;
|
|
}
|
|
|
|
if ( sameColorCount * 10 > image.height() )
|
|
{
|
|
// Add a grey line at the bottom boundary
|
|
|
|
TQRgb val = contrastingColor( image.pixel( image.width() / 2, y2 ) );
|
|
|
|
for ( int x = 0; x < image.width(); x++ )
|
|
image.setPixel( x, y2, val );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
TQRgb
|
|
KTreemapTile::contrastingColor( TQRgb col )
|
|
{
|
|
if ( tqGray( col ) < 128 )
|
|
return tqRgb( tqRed( col ) * 2, tqGreen( col ) * 2, tqBlue( col ) * 2 );
|
|
else
|
|
return tqRgb( tqRed( col ) / 2, tqGreen( col ) / 2, tqBlue( col ) / 2 );
|
|
}
|
|
|
|
|
|
|
|
|
|
KCushionSurface::KCushionSurface()
|
|
{
|
|
_xx2 = 0.0;
|
|
_xx1 = 0.0;
|
|
_yy2 = 0.0;
|
|
_yy1 = 0.0;
|
|
_height = CushionHeight;
|
|
}
|
|
|
|
|
|
void
|
|
KCushionSurface::addRidge( KOrientation dim, double height, const TQRect & rect )
|
|
{
|
|
_height = height;
|
|
|
|
if ( dim == KTreemapHorizontal )
|
|
{
|
|
_xx2 = squareRidge( _xx2, _height, rect.left(), rect.right() );
|
|
_xx1 = linearRidge( _xx1, _height, rect.left(), rect.right() );
|
|
}
|
|
else
|
|
{
|
|
_yy2 = squareRidge( _yy2, _height, rect.top(), rect.bottom() );
|
|
_yy1 = linearRidge( _yy1, _height, rect.top(), rect.bottom() );
|
|
}
|
|
}
|
|
|
|
|
|
double
|
|
KCushionSurface::squareRidge( double squareCoefficient, double height, int x1, int x2 )
|
|
{
|
|
if ( x2 != x1 ) // Avoid division by zero
|
|
squareCoefficient -= 4.0 * height / ( x2 - x1 );
|
|
|
|
return squareCoefficient;
|
|
}
|
|
|
|
|
|
double
|
|
KCushionSurface::linearRidge( double linearCoefficient, double height, int x1, int x2 )
|
|
{
|
|
if ( x2 != x1 ) // Avoid division by zero
|
|
linearCoefficient += 4.0 * height * ( x2 + x1 ) / ( x2 - x1 );
|
|
|
|
return linearCoefficient;
|
|
}
|
|
|
|
|
|
|
|
|
|
// EOF
|