/* This file is part of the Nepomuk KDE project. Copyright (C) 2007 Sebastian Trueg 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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& 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& 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 >& 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 = i18nc( "@label", "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 nodes; // just a helper structure for speeding up things QList > 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& 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( QString::fromAscii( tag.resourceUri().toEncoded() ) ), Soprano::Query::QueryLanguageSparql ); int w = 0; while ( it.next() ) { ++w; } return w; } void Nepomuk::TagCloud::Private::updateNodeWeights() { bool changedWeights = false; for ( QList::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::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::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::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 row; for ( QList::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() ); for ( QList::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 > 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() ); } 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 >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { QList& 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 >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { QList& 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 >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { QList& 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 >::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::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& 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::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& tags ) { d->resource = QUrl(); d->showAllTags = false; d->invalidateCachedValues(); d->nodes.clear(); Q_FOREACH( const 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 > rows; rows.append( QList() ); for ( QList::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() ); } 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 { return QFrame::minimumSizeHint(); // 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( i18nc( "@label Indicator when no tags defined", "No Tags" ) ), fontMetrics().height() ); } else { QSize size; for ( QList::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) // FIXME: add zoom here if ( d->cachedHfwWidth != contentsWidth ) { // have to keep in mind the frame contentsWidth -= frameWidth()*2; QList allNodes; for ( QList::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(); p.save(); p.setMatrix( d->zoomMatrix ); for ( QList::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"