+/*
+ This file is part of the Nepomuk KDE project.
+ Copyright (C) 2007 Sebastian Trueg <trueg@kde.org>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License version 2 as published by the Free Software Foundation.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public License
+ along with this library; see the file COPYING.LIB. If not, write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA 02110-1301, USA.
+ */
+
+#include "tagcloud.h"
+#include "newtagdialog.h"
+
+#include <QtGui/QFont>
+#include <QtGui/QFontMetrics>
+#include <QtCore/QList>
+#include <QtGui/QPushButton>
+#include <QtCore/Qt>
+#include <QtCore/QTime>
+#include <QtGui/QPainter>
+#include <QtGui/QMouseEvent>
+#include <QtGui/QPalette>
+#include <QtGui/QInputDialog>
+#include <QtGui/QAction>
+
+#include <KRandomSequence>
+#include <KLocale>
+#include <KColorScheme>
+#include <KDebug>
+
+#include <Soprano/Client/DBusModel>
+#include <Soprano/QueryResultIterator>
+#include <Soprano/Vocabulary/RDF>
+#include <Soprano/Vocabulary/NAO>
+
+#include <nepomuk/resourcemanager.h>
+
+#include <math.h>
+
+
+namespace {
+ const int s_hSpacing = 10;
+ const int s_vSpacing = 5;
+
+ class TagNode {
+ public:
+ TagNode()
+ : weight( 0 ),
+ selected( false ) {
+ }
+
+ // fixed info
+ Nepomuk::Tag tag;
+ int weight;
+
+ // misc
+ bool selected;
+
+ // info generated by rebuildCloud
+ QFont font;
+ QRect rect;
+ QRect zoomedRect;
+ QString text;
+ };
+
+ bool tagNodeNameLessThan( const TagNode& n1, const TagNode& n2 ) {
+ return n1.text < n2.text;
+ }
+
+ bool tagNodeWeightLessThan( const TagNode& n1, const TagNode& n2 ) {
+ return n1.weight < n2.weight;
+ }
+
+ int rowLength( const QList<TagNode*>& row ) {
+ int rowLen = 0;
+ for ( int j = 0; j < row.count(); ++j ) {
+ rowLen += row[j]->rect.width();
+ if ( j < row.count()-1 ) {
+ rowLen += s_hSpacing;
+ }
+ }
+ return rowLen;
+ }
+
+ int rowHeight( const QList<TagNode*>& row ) {
+ int h = 0;
+ for ( int j = 0; j < row.count(); ++j ) {
+ h = qMax( row[j]->rect.height(), h );
+ }
+ return h;
+ }
+
+ QSize cloudSize( const QList<QList<TagNode*> >& rows ) {
+ int w = 0;
+ int h = 0;
+ for ( int i = 0; i < rows.count(); ++i ) {
+ w = qMax( w, rowLength( rows[i] ) );
+ h += rowHeight( rows[i] );
+ if ( i < rows.count()-1 ) {
+ h += s_vSpacing;
+ }
+ }
+ return QSize( w, h );
+ }
+}
+
+
+class Nepomuk::TagCloud::Private
+{
+public:
+ Private( TagCloud* parent )
+ : maxFontSize( 0 ),
+ minFontSize( 0 ),
+ maxNumberDisplayedTags( 0 ),
+ selectionEnabled( false ),
+ newTagButtonEnabled( false ),
+ alignment( Qt::AlignCenter ),
+ sorting( SortAlpabetically ),
+ zoomEnabled( true ),
+ showAllTags( false ),
+ customNewTagAction( 0 ),
+ hoverTag( 0 ),
+ cachedHfwWidth( -1 ),
+ m_parent( parent ) {
+ newTagNode.text = i18n( "New Tag..." );
+ }
+
+ int maxFontSize;
+ int minFontSize;
+ int maxNumberDisplayedTags;
+ bool selectionEnabled;
+ bool newTagButtonEnabled;
+ Qt::Alignment alignment;
+ Sorting sorting;
+ bool zoomEnabled;
+
+ // The resource whose tags we are showing
+ // invalid if we show all tags or a selection
+ QUrl resource;
+ bool showAllTags;
+
+ // the actual nodes
+ QList<TagNode> nodes;
+
+ // just a helper structure for speeding up things
+ QList<QList<TagNode*> > rows;
+
+ TagNode newTagNode;
+ QAction* customNewTagAction;
+
+ TagNode* hoverTag;
+
+ QMatrix zoomMatrix;
+
+ QSize cachedSizeHint;
+ int cachedHfwWidth;
+ int cachedHfwHeight;
+
+ void invalidateCachedValues() {
+ cachedSizeHint = QSize();
+ cachedHfwWidth = -1;
+ }
+
+ int getMinFontSize() const;
+ int getMaxFontSize() const;
+ void updateNodeWeights();
+ void updateNodeFonts();
+ void sortNodes();
+ void rebuildCloud();
+ TagNode* tagAt( const QPoint& pos );
+ TagNode* findTagInRow( const QList<TagNode*>& row, const QPoint& pos );
+ TagNode* nodeForTag( const Tag& tag );
+ int calculateWeight( const Nepomuk::Tag& tag );
+
+private:
+ TagCloud* m_parent;
+};
+
+
+int Nepomuk::TagCloud::Private::getMinFontSize() const
+{
+ return minFontSize > 0 ? minFontSize : ( 8 * m_parent->font().pointSize() / 10 );
+}
+
+
+int Nepomuk::TagCloud::Private::getMaxFontSize() const
+{
+ return maxFontSize > 0 ? maxFontSize : ( 22 * m_parent->font().pointSize() / 10 );
+}
+
+
+int Nepomuk::TagCloud::Private::calculateWeight( const Nepomuk::Tag& tag )
+{
+ // stupid SPARQL has no functions such as count!
+ Soprano::QueryResultIterator it
+ = ResourceManager::instance()->mainModel()->executeQuery( QString( "select ?r where { ?r <%1> <%2> . }" )
+ .arg( Soprano::Vocabulary::NAO::hasTag().toString() )
+ .arg( tag.resourceUri().toString() ),
+ Soprano::Query::QueryLanguageSparql );
+ int w = 0;
+ while ( it.next() ) {
+ ++w;
+ }
+ return w;
+}
+
+
+void Nepomuk::TagCloud::Private::updateNodeWeights()
+{
+ bool changedWeights = false;
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); ++it ) {
+ TagNode& node = *it;
+ int w = calculateWeight( node.tag );
+ if ( w != node.weight ) {
+ node.weight = w;
+ changedWeights = true;
+ }
+ }
+ if ( changedWeights ) {
+ updateNodeFonts();
+ }
+}
+
+
+void Nepomuk::TagCloud::Private::updateNodeFonts()
+{
+ int maxWeight = 0;
+ int minWeight = 0;
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); ++it ) {
+ TagNode& node = *it;
+ minWeight = qMin( minWeight, node.weight );
+ maxWeight = qMax( maxWeight, node.weight );
+ }
+
+ // calculate font sizes
+ // ----------------------------------------------
+ int usedMinFontSize = getMinFontSize();
+ int usedMaxFontSize = getMaxFontSize();
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); ++it ) {
+ TagNode& node = *it;
+ double normalizedWeight = (double)(node.weight - minWeight) / (double)qMax(maxWeight - minWeight, 1);
+ node.font = m_parent->font();
+ node.font.setPointSize( usedMinFontSize + (int)((double)(usedMaxFontSize-usedMinFontSize) * normalizedWeight) );
+ if( normalizedWeight > 0.8 )
+ node.font.setBold( true );
+ }
+
+ if ( newTagButtonEnabled ) {
+ newTagNode.font = m_parent->font();
+ newTagNode.font.setPointSize( usedMinFontSize );
+ newTagNode.font.setUnderline( true );
+ }
+}
+
+
+void Nepomuk::TagCloud::Private::sortNodes()
+{
+ if ( sorting == SortAlpabetically ) {
+ qSort( nodes.begin(), nodes.end(), tagNodeNameLessThan );
+ }
+ else if ( sorting == SortByWeight ) {
+ qSort( nodes.begin(), nodes.end(), tagNodeWeightLessThan );
+ }
+ else if ( sorting == SortRandom ) {
+ KRandomSequence().randomize( nodes );
+ }
+}
+
+
+void Nepomuk::TagCloud::Private::rebuildCloud()
+{
+ if ( nodes.isEmpty() && !newTagButtonEnabled ) {
+ return;
+ }
+
+ // - Always try to be quadratic
+ // - Always prefer to expand horizontally
+ // - If we cannot fit everything into m_parent->contentsRect(), zoom
+ // - If alignment & Qt::AlignJustify insert spaces between tags
+
+ sortNodes();
+
+ QRect contentsRect = m_parent->contentsRect();
+
+ // initialize the nodes' sizes
+ // ----------------------------------------------
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); ++it ) {
+ TagNode& node = *it;
+ node.rect = QFontMetrics( node.font ).boundingRect( node.text );
+ }
+ if ( newTagButtonEnabled ) {
+ newTagNode.rect = QFontMetrics( newTagNode.font ).boundingRect( customNewTagAction ? customNewTagAction->text() : newTagNode.text );
+ }
+
+
+ // and position the nodes
+ // ----------------------------------------------
+ rows.clear();
+ if ( 0 ) { // FIXME: make it configurable
+ QRect lineRect;
+ QRect totalRect;
+ QList<TagNode*> row;
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); /* We do increment it below */ ) {
+ TagNode& node = *it;
+
+ int usedSpacing = row.isEmpty() ? 0 : s_hSpacing;
+ if ( lineRect.width() + usedSpacing + node.rect.width() <= contentsRect.width() ) {
+ node.rect.moveBottomLeft( QPoint( lineRect.right() + usedSpacing, lineRect.bottom() ) );
+ QRect newLineRect = lineRect.united( node.rect );
+ newLineRect.moveTopLeft( lineRect.topLeft() );
+ lineRect = newLineRect;
+ row.append( &node );
+
+ // update all other nodes in this line
+ Q_FOREACH( TagNode* n, row ) {
+ n->rect.moveBottom( lineRect.bottom() - ( lineRect.height() - n->rect.height() )/2 );
+ }
+
+ ++it;
+ }
+ else {
+ rows.append( row );
+ row.clear();
+ int newLineTop = lineRect.bottom() + s_vSpacing;
+ lineRect = QRect();
+ lineRect.moveTop( newLineTop );
+ }
+ }
+ rows.append( row );
+ }
+ else {
+ // initialize first row
+ rows.append( QList<TagNode*>() );
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); ++it ) {
+ TagNode& node = *it;
+ rows.first().append( &node );
+ }
+ if ( newTagButtonEnabled ) {
+ rows.first().append( &newTagNode );
+ }
+
+ // calculate the rows
+ QList<QList<TagNode*> > bestRows( rows );
+ QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
+ QSize bestSize( size );
+ while ( ( size.height() < size.width() ||
+ size.width() > contentsRect.width() ) &&
+ size.height() <= contentsRect.height() ) {
+ // find the longest row
+ int maxRow = 0;
+ int maxLen = 0;
+ for ( int i = 0; i < rows.count(); ++i ) {
+ int rowLen = rowLength( rows[i] );
+ if ( rowLen > maxLen ) {
+ maxLen = rowLen;
+ maxRow = i;
+ }
+ }
+
+ // move the last item from the longest row to the next row
+ TagNode* node = rows[maxRow].takeLast();
+ if ( rows.count() <= maxRow+1 ) {
+ rows.append( QList<TagNode*>() );
+ }
+ rows[maxRow+1].prepend( node );
+
+ // update the size
+ size = cloudSize( rows );
+
+ if ( size.width() < bestSize.width() &&
+ ( size.width() > size.height() ||
+ bestSize.width() > contentsRect.width() ) &&
+ size.height() <= contentsRect.height() ) {
+ bestSize = size;
+ bestRows = rows;
+ }
+ }
+ rows = bestRows;
+
+ // position the tags
+ int y = 0;
+ for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
+ QList<TagNode*>& row = *rowIt;
+ int h = rowHeight( row );
+ int x = 0;
+ Q_FOREACH( TagNode* node, row ) {
+ node->rect.moveTop( y + ( h - node->rect.height() )/2 );
+ node->rect.moveLeft( x );
+ x += s_hSpacing + node->rect.width();
+ }
+ y += h + s_vSpacing;
+ }
+ }
+
+
+ // let's see if we have to zoom
+ // ----------------------------------------------
+ zoomMatrix = QMatrix();
+ int w = contentsRect.width();
+ if ( zoomEnabled ) {
+ for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
+ QList<TagNode*>& row = *rowIt;
+ w = qMax( w, row.last()->rect.right() );
+ }
+ if ( w > contentsRect.width() ) {
+ double zoomFactor = ( double )contentsRect.width() / ( double )w;
+ zoomMatrix.scale( zoomFactor, zoomFactor );
+ }
+ }
+
+ // force horizontal alignment
+ // ----------------------------------------------
+ for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
+ QList<TagNode*>& row = *rowIt;
+ int space = /*contentsRect.right()*/w - row.last()->rect.right();
+ if ( alignment & ( Qt::AlignRight|Qt::AlignHCenter ) ) {
+ Q_FOREACH( TagNode* node, row ) {
+ node->rect.moveLeft( node->rect.left() + ( alignment & Qt::AlignRight ? space : space/2 ) );
+ }
+ }
+ else if ( alignment & Qt::AlignJustify && row.count() > 1 ) {
+ space /= ( row.count()-1 );
+ int i = 0;
+ Q_FOREACH( TagNode* node, row ) {
+ node->rect.moveLeft( node->rect.left() + ( space * i++ ) );
+ }
+ }
+ }
+
+ // force vertical alignment
+ // ----------------------------------------------
+ int verticalSpace = contentsRect.bottom() - rows.last().first()->rect.bottom();
+ if ( alignment & ( Qt::AlignBottom|Qt::AlignVCenter ) ) {
+ for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
+ Q_FOREACH( TagNode* node, *rowIt ) {
+ node->rect.moveTop( node->rect.top() + ( alignment & Qt::AlignBottom ? verticalSpace : verticalSpace/2 ) );
+ }
+ }
+ }
+
+ for( QList<TagNode>::iterator it = nodes.begin(); it != nodes.end(); ++it ) {
+ it->zoomedRect = zoomMatrix.mapRect( it->rect );
+ }
+ newTagNode.zoomedRect = zoomMatrix.mapRect( newTagNode.rect );
+
+ m_parent->updateGeometry();
+ m_parent->update();
+}
+
+
+// binary search in row
+TagNode* Nepomuk::TagCloud::Private::findTagInRow( const QList<TagNode*>& row, const QPoint& pos )
+{
+ int x = row.count() * pos.x() / m_parent->width();
+ int i = 0;
+ while ( 1 ) {
+ if ( x-i >= 0 && x-i < row.count() && row[x-i]->zoomedRect.contains( pos ) ) {
+ return row[x-i];
+ }
+ else if ( x+i >= 0 && x+i < row.count() && row[x+i]->zoomedRect.contains( pos ) ) {
+ return row[x+i];
+ }
+ if ( x-i < 0 && x+i >= row.count() ) {
+ return 0;
+ }
+ ++i;
+ }
+ return 0;
+}
+
+
+// binary search in cloud
+TagNode* Nepomuk::TagCloud::Private::tagAt( const QPoint& pos )
+{
+ int y = rows.count() * pos.y() / m_parent->height();
+
+ int i = 0;
+ while ( 1 ) {
+ if ( y-i >= 0 && y-i < rows.count() ) {
+ if ( TagNode* node = findTagInRow( rows[y-i], pos ) ) {
+ return node;
+ }
+ }
+ if ( y+i >= 0 && y+i < rows.count() ) {
+ if ( TagNode* node = findTagInRow( rows[y+i], pos ) ) {
+ return node;
+ }
+ }
+ if ( y-i < 0 && y+i >= rows.count() ) {
+ return 0;
+ }
+ ++i;
+ }
+ return 0;
+}
+
+
+TagNode* Nepomuk::TagCloud::Private::nodeForTag( const Tag& tag )
+{
+ for ( QList<TagNode>::iterator it = nodes.begin();
+ it != nodes.end(); ++it ) {
+ TagNode& node = *it;
+ if ( tag == node.tag ) {
+ return &node;
+ }
+ }
+ return 0;
+}
+
+
+
+Nepomuk::TagCloud::TagCloud( QWidget* parent )
+ : QFrame( parent ),
+ d( new Private(this) )
+{
+ QSizePolicy policy( QSizePolicy::Preferred,
+ QSizePolicy::Preferred );
+ policy.setHeightForWidth( true );
+ setSizePolicy( policy );
+ setMouseTracking( true );
+
+ // Since signals are delivered in no particular order
+ // our slot might be called before the resources are updated
+ // Then, we would use invalid cached data.
+ // By using queued connections this problem should be solved.
+ connect( ResourceManager::instance()->mainModel(),
+ SIGNAL( statementAdded( const Soprano::Statement& ) ),
+ this,
+ SLOT( slotStatementAdded( const Soprano::Statement& ) ),
+ Qt::QueuedConnection );
+ connect( ResourceManager::instance()->mainModel(),
+ SIGNAL( statementRemoved( const Soprano::Statement& ) ),
+ this,
+ SLOT( slotStatementRemoved( const Soprano::Statement& ) ),
+ Qt::QueuedConnection );
+}
+
+
+Nepomuk::TagCloud::~TagCloud()
+{
+ delete d;
+}
+
+
+void Nepomuk::TagCloud::setMaxFontSize( int size )
+{
+ d->invalidateCachedValues();
+ d->maxFontSize = size;
+ d->updateNodeFonts();
+ d->rebuildCloud();
+}
+
+
+void Nepomuk::TagCloud::setMinFontSize( int size )
+{
+ d->invalidateCachedValues();
+ d->minFontSize = size;
+ d->updateNodeFonts();
+ d->rebuildCloud();
+}
+
+
+void Nepomuk::TagCloud::setMaxNumberDisplayedTags( int n )
+{
+ d->maxNumberDisplayedTags = n;
+ d->rebuildCloud();
+}
+
+
+void Nepomuk::TagCloud::setSelectionEnabled( bool enabled )
+{
+ d->selectionEnabled = enabled;
+ update();
+}
+
+
+void Nepomuk::TagCloud::setNewTagButtonEnabled( bool enabled )
+{
+ d->newTagButtonEnabled = enabled;
+ d->rebuildCloud();
+}
+
+
+bool Nepomuk::TagCloud::zoomEnabled() const
+{
+ return d->zoomEnabled;
+}
+
+
+void Nepomuk::TagCloud::setZoomEnabled( bool zoom )
+{
+ if ( d->zoomEnabled != zoom ) {
+ d->zoomEnabled = zoom;
+ d->rebuildCloud();
+ }
+}
+
+
+void Nepomuk::TagCloud::setContextMenuEnabled( bool enabled )
+{
+}
+
+
+void Nepomuk::TagCloud::setAlignment( Qt::Alignment alignment )
+{
+ d->alignment = alignment;
+ d->rebuildCloud();
+}
+
+
+void Nepomuk::TagCloud::setSorting( Sorting s )
+{
+ d->invalidateCachedValues();
+ d->sorting = s;
+ d->rebuildCloud();
+}
+
+
+void Nepomuk::TagCloud::showAllTags()
+{
+ showTags( Nepomuk::Tag::allTags() );
+ d->showAllTags = true;
+}
+
+
+void Nepomuk::TagCloud::showResourceTags( const Resource& resource )
+{
+ showTags( resource.tags() );
+ d->resource = resource.uri();
+}
+
+
+void Nepomuk::TagCloud::showTags( const QList<Tag>& tags )
+{
+ d->resource = QUrl();
+ d->showAllTags = false;
+ d->invalidateCachedValues();
+ d->nodes.clear();
+ Q_FOREACH( Tag tag, tags ) {
+ TagNode node;
+ node.tag = tag;
+ node.weight = d->calculateWeight( tag );
+ node.text = node.tag.genericLabel();
+
+ d->nodes.append( node );
+ }
+ d->updateNodeFonts();
+ d->rebuildCloud();
+}
+
+
+void Nepomuk::TagCloud::setTagSelected( const Tag& tag, bool selected )
+{
+ if ( TagNode* node = d->nodeForTag( tag ) ) {
+ node->selected = selected;
+ if ( d->selectionEnabled ) {
+ update( node->zoomedRect );
+ }
+ }
+}
+
+
+QSize Nepomuk::TagCloud::sizeHint() const
+{
+ // If we have tags d->rebuildCloud() has been called at least once,
+ // thus, we have proper rects (i.e. needed sizes)
+
+ if ( !d->cachedSizeHint.isValid() ) {
+ QList<QList<TagNode*> > rows;
+ rows.append( QList<TagNode*>() );
+ for ( QList<TagNode>::iterator it = d->nodes.begin();
+ it != d->nodes.end(); ++it ) {
+ TagNode& node = *it;
+ rows.first().append( &node );
+ }
+ if ( d->newTagButtonEnabled ) {
+ rows.first().append( &d->newTagNode );
+ }
+
+ QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
+ QSize bestSize( size );
+ while ( size.height() < size.width() ) {
+ // find the longest row
+ int maxRow = 0;
+ int maxLen = 0;
+ for ( int i = 0; i < rows.count(); ++i ) {
+ int rowLen = rowLength( rows[i] );
+ if ( rowLen > maxLen ) {
+ maxLen = rowLen;
+ maxRow = i;
+ }
+ }
+
+ // move the last item from the longest row to the next row
+ TagNode* node = rows[maxRow].takeLast();
+ if ( rows.count() <= maxRow+1 ) {
+ rows.append( QList<TagNode*>() );
+ }
+ rows[maxRow+1].prepend( node );
+
+ // update the size
+ size = cloudSize( rows );
+
+ if ( size.width() < bestSize.width() &&
+ size.width() > size.height() ) {
+ bestSize = size;
+ }
+ }
+
+ d->cachedSizeHint = QSize( bestSize.width() + frameWidth()*2,
+ bestSize.height() + frameWidth()*2 );
+ }
+
+ return d->cachedSizeHint;
+}
+
+
+QSize Nepomuk::TagCloud::minimumSizeHint() const
+{
+ // If we have tags d->rebuildCloud() has been called at least once,
+ // thus, we have proper rects (i.e. needed sizes)
+ if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
+ return QSize( fontMetrics().width( i18n( "No Tags" ) ), fontMetrics().height() );
+ }
+ else {
+ QSize size;
+ for ( QList<TagNode>::iterator it = d->nodes.begin();
+ it != d->nodes.end(); ++it ) {
+ size.setWidth( qMax( size.width(), ( *it ).rect.width() ) );
+ size.setHeight( qMax( size.height(), ( *it ).rect.height() ) );
+ }
+ if ( d->newTagButtonEnabled ) {
+ size.setWidth( qMax( size.width(), d->newTagNode.rect.width() ) );
+ size.setHeight( qMax( size.height(), d->newTagNode.rect.height() ) );
+ }
+ size.setWidth( size.width() + frameWidth()*2 );
+ size.setHeight( size.height() + frameWidth()*2 );
+ return size;
+ }
+}
+
+
+int Nepomuk::TagCloud::heightForWidth( int contentsWidth ) const
+{
+ // If we have tags d->rebuildCloud() has been called at least once,
+ // thus, we have proper rects (i.e. needed sizes)
+
+ if ( d->cachedHfwWidth != contentsWidth ) {
+ // have to keep in mind the frame
+ contentsWidth -= frameWidth()*2;
+
+ QList<TagNode*> allNodes;
+ for ( QList<TagNode>::iterator it = d->nodes.begin();
+ it != d->nodes.end(); ++it ) {
+ TagNode& node = *it;
+ allNodes.append( &node );
+ }
+ if ( d->newTagButtonEnabled ) {
+ allNodes.append( &d->newTagNode );
+ }
+
+ int h = 0;
+ bool newRow = true;
+ int rowW = 0;
+ int rowH = 0;
+ for ( int i = 0; i < allNodes.count(); ++i ) {
+ int w = rowW;
+ if ( !newRow ) {
+ w += s_hSpacing;
+ }
+ newRow = false;
+ w += allNodes[i]->rect.width();
+ if ( w <= contentsWidth ) {
+ rowH = qMax( rowH, allNodes[i]->rect.height() );
+ rowW = w;
+ }
+ else {
+ if ( h > 0 ) {
+ h += s_vSpacing;
+ }
+ h += rowH;
+ rowH = allNodes[i]->rect.height();
+ rowW = allNodes[i]->rect.width();
+ }
+ }
+ if ( rowH > 0 ) {
+ h += s_vSpacing + rowH;
+ }
+
+ d->cachedHfwWidth = contentsWidth;
+ d->cachedHfwHeight = h;
+ }
+
+ return d->cachedHfwHeight + frameWidth()*2;
+}
+
+
+void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e )
+{
+ QFrame::resizeEvent( e );
+ d->rebuildCloud();
+ update();
+}
+
+
+void Nepomuk::TagCloud::paintEvent( QPaintEvent* e )
+{
+ QFrame::paintEvent( e );
+
+ KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText );
+ KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText );
+ KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText );
+
+ QPainter p( this );
+ QRegion paintRegion = e->region();
+
+ if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
+ p.drawText( contentsRect(), d->alignment, i18n( "No Tags" ) );
+ }
+ else {
+ p.save();
+ p.setMatrix( d->zoomMatrix );
+
+ for ( QList<TagNode>::iterator it = d->nodes.begin();
+ it != d->nodes.end(); ++it ) {
+ TagNode& node = *it;
+
+ if ( paintRegion.contains( node.zoomedRect ) ) {
+ p.setFont( node.font );
+
+ if ( &node == d->hoverTag ) {
+ p.setPen( hoverTextBrush.brush( this ).color() );
+ }
+ else if ( d->selectionEnabled && node.selected ) {
+ p.setPen( activeTextBrush.brush( this ).color() );
+ }
+ else {
+ p.setPen( normalTextBrush.brush( this ).color() );
+ }
+ p.drawText( node.rect, Qt::AlignCenter, node.text );
+ }
+ }
+
+ if ( d->newTagButtonEnabled ) {
+ p.setFont( d->newTagNode.font );
+ if ( &d->newTagNode == d->hoverTag ) {
+ p.setPen( hoverTextBrush.brush( this ).color() );
+ }
+ else {
+ p.setPen( normalTextBrush.brush( this ).color() );
+ }
+ p.drawText( d->newTagNode.rect, Qt::AlignCenter, d->customNewTagAction ? d->customNewTagAction->text() : d->newTagNode.text );
+ }
+
+ p.restore();
+ }
+}
+
+
+void Nepomuk::TagCloud::mousePressEvent( QMouseEvent* e )
+{
+ if ( e->button() == Qt::LeftButton ) {
+ if ( TagNode* node = d->tagAt( e->pos() ) ) {
+ kDebug() << "clicked" << node->text;
+ if ( node == &d->newTagNode ) {
+ if ( d->customNewTagAction ) {
+ d->customNewTagAction->trigger();
+ }
+ else {
+ // FIXME: nicer gui
+ Tag newTag = NewTagDialog::createTag( this );
+ if ( newTag.isValid() ) {
+ emit tagAdded( newTag );
+ }
+ }
+ }
+ else {
+ emit tagClicked( node->tag );
+ if ( d->selectionEnabled ) {
+ kDebug() << "Toggleing tag" << node->text;
+ node->selected = !node->selected;
+ emit tagToggled( node->tag, node->selected );
+ update( node->zoomedRect );
+ }
+ }
+ }
+ }
+}
+
+
+void Nepomuk::TagCloud::mouseMoveEvent( QMouseEvent* e )
+{
+ if ( e->buttons() == Qt::NoButton ) {
+
+ TagNode* oldHoverTag = d->hoverTag;
+
+ if ( ( d->hoverTag = d->tagAt( e->pos() ) ) &&
+ !d->selectionEnabled ) {
+ setCursor( Qt::PointingHandCursor );
+ }
+ else if ( d->newTagButtonEnabled &&
+ d->newTagNode.zoomedRect.contains( e->pos() ) ) {
+ d->hoverTag = &d->newTagNode;
+ setCursor( Qt::PointingHandCursor );
+ }
+ else {
+ unsetCursor();
+ }
+
+ if ( oldHoverTag || d->hoverTag ) {
+ QRect updateRect;
+ if ( d->hoverTag )
+ updateRect = updateRect.united( d->hoverTag->zoomedRect );
+ if ( oldHoverTag )
+ updateRect = updateRect.united( oldHoverTag->zoomedRect );
+
+ update( updateRect );
+ }
+ }
+}
+
+
+void Nepomuk::TagCloud::leaveEvent( QEvent* )
+{
+ unsetCursor();
+ if ( d->hoverTag ) {
+ QRect updateRect = d->hoverTag->zoomedRect;
+ d->hoverTag = 0;
+ update( updateRect );
+ }
+}
+
+
+void Nepomuk::TagCloud::slotStatementAdded( const Soprano::Statement& s )
+{
+ if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
+ s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
+ // new tag created
+ if ( d->showAllTags ) {
+ showAllTags();
+ }
+ }
+ else if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
+ if ( s.subject().uri() == d->resource ) {
+ showResourceTags( d->resource );
+ }
+ else {
+ // weights might have changed
+ d->updateNodeWeights();
+ d->rebuildCloud();
+ }
+ }
+}
+
+
+void Nepomuk::TagCloud::slotStatementRemoved( const Soprano::Statement& s )
+{
+ // FIXME: In theory might contain empty nodes as wildcards
+
+ if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
+ if ( d->resource.isValid() &&
+ d->resource == s.subject().uri() ) {
+ showResourceTags( d->resource );
+ }
+ else {
+ // weights might have changed
+ d->updateNodeWeights();
+ d->rebuildCloud();
+ }
+ }
+ else if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
+ s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
+ // tag deleted
+ if ( d->showAllTags ) {
+ showAllTags();
+ }
+ }
+}
+
+
+void Nepomuk::TagCloud::setCustomNewTagAction( QAction* action )
+{
+ d->customNewTagAction = action;
+ setNewTagButtonEnabled( action != 0 );
+}
+
+#include "tagcloud.moc"