┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/src/tagcloud/tagcloud.cpp
diff options
context:
space:
mode:
authorSebastian Trueg <[email protected]>2008-03-21 21:05:05 +0000
committerSebastian Trueg <[email protected]>2008-03-21 21:05:05 +0000
commitd3a04321886e8ca39ab91a647a9547ebe4d52154 (patch)
treeff4e8237ba07cc2201316c32c07f09e6e311eb33 /src/tagcloud/tagcloud.cpp
parent9ceab694e2454d283987d596f5f69e43adc6579e (diff)
This is the first step towards a better looking and more usable metadata GUI.
- A nicer comment widget shows a popup to edit the comment. - A tag cloud replaces the ugly tagwidget from libnepomuk. The plan is to use Dolphin as a testbed to optimize the look and then move at least the tagcloud to libnepomuk to make it available for all apps since this is a common feature. So please test it and provide feedback. The layout is still cluttered. So we also need feedback on that. And of course on the usability. Apart from the GUI Dolphin now uses the mass metadata update job to perform metadata updates on many files in an async KJob without blocking the GUI. This is another candidate for public API at some point. svn path=/trunk/KDE/kdebase/apps/; revision=788565
Diffstat (limited to 'src/tagcloud/tagcloud.cpp')
-rw-r--r--src/tagcloud/tagcloud.cpp1002
1 files changed, 1002 insertions, 0 deletions
diff --git a/src/tagcloud/tagcloud.cpp b/src/tagcloud/tagcloud.cpp
new file mode 100644
index 000000000..8fe5cba89
--- /dev/null
+++ b/src/tagcloud/tagcloud.cpp
@@ -0,0 +1,1002 @@
+/*
+ This file is part of the Nepomuk KDE project.
+ Copyright (C) 2007 Sebastian Trueg <[email protected]>
+
+ 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"