]> cloud.milkyroute.net Git - dolphin.git/blob - src/tagcloud/tagcloud.cpp
0074d17968239a33e4bee172739b796ee65b7ee0
[dolphin.git] / src / tagcloud / tagcloud.cpp
1 /*
2 This file is part of the Nepomuk KDE project.
3 Copyright (C) 2007 Sebastian Trueg <trueg@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License version 2 as published by the Free Software Foundation.
8
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
18 */
19
20 #include "tagcloud.h"
21 #include "newtagdialog.h"
22
23 #include <QtGui/QFont>
24 #include <QtGui/QFontMetrics>
25 #include <QtCore/QList>
26 #include <QtGui/QPushButton>
27 #include <QtCore/Qt>
28 #include <QtCore/QTime>
29 #include <QtGui/QPainter>
30 #include <QtGui/QMouseEvent>
31 #include <QtGui/QPalette>
32 #include <QtGui/QInputDialog>
33 #include <QtGui/QAction>
34
35 #include <KRandomSequence>
36 #include <KLocale>
37 #include <KColorScheme>
38 #include <KDebug>
39
40 #include <Soprano/Client/DBusModel>
41 #include <Soprano/QueryResultIterator>
42 #include <Soprano/Vocabulary/RDF>
43 #include <Soprano/Vocabulary/NAO>
44
45 #include <nepomuk/resourcemanager.h>
46
47 #include <math.h>
48
49
50 namespace {
51 const int s_hSpacing = 10;
52 const int s_vSpacing = 5;
53
54 class TagNode {
55 public:
56 TagNode()
57 : weight( 0 ),
58 selected( false ) {
59 }
60
61 // fixed info
62 Nepomuk::Tag tag;
63 int weight;
64
65 // misc
66 bool selected;
67
68 // info generated by rebuildCloud
69 QFont font;
70 QRect rect;
71 QRect zoomedRect;
72 QString text;
73 };
74
75 bool tagNodeNameLessThan( const TagNode& n1, const TagNode& n2 ) {
76 return n1.text < n2.text;
77 }
78
79 bool tagNodeWeightLessThan( const TagNode& n1, const TagNode& n2 ) {
80 return n1.weight < n2.weight;
81 }
82
83 int rowLength( const QList<TagNode*>& row ) {
84 int rowLen = 0;
85 for ( int j = 0; j < row.count(); ++j ) {
86 rowLen += row[j]->rect.width();
87 if ( j < row.count()-1 ) {
88 rowLen += s_hSpacing;
89 }
90 }
91 return rowLen;
92 }
93
94 int rowHeight( const QList<TagNode*>& row ) {
95 int h = 0;
96 for ( int j = 0; j < row.count(); ++j ) {
97 h = qMax( row[j]->rect.height(), h );
98 }
99 return h;
100 }
101
102 QSize cloudSize( const QList<QList<TagNode*> >& rows ) {
103 int w = 0;
104 int h = 0;
105 for ( int i = 0; i < rows.count(); ++i ) {
106 w = qMax( w, rowLength( rows[i] ) );
107 h += rowHeight( rows[i] );
108 if ( i < rows.count()-1 ) {
109 h += s_vSpacing;
110 }
111 }
112 return QSize( w, h );
113 }
114 }
115
116
117 class Nepomuk::TagCloud::Private
118 {
119 public:
120 Private( TagCloud* parent )
121 : maxFontSize( 0 ),
122 minFontSize( 0 ),
123 maxNumberDisplayedTags( 0 ),
124 selectionEnabled( false ),
125 newTagButtonEnabled( false ),
126 alignment( Qt::AlignCenter ),
127 sorting( SortAlpabetically ),
128 zoomEnabled( true ),
129 showAllTags( false ),
130 customNewTagAction( 0 ),
131 hoverTag( 0 ),
132 cachedHfwWidth( -1 ),
133 m_parent( parent ) {
134 newTagNode.text = i18nc( "@label", "New Tag..." );
135 }
136
137 int maxFontSize;
138 int minFontSize;
139 int maxNumberDisplayedTags;
140 bool selectionEnabled;
141 bool newTagButtonEnabled;
142 Qt::Alignment alignment;
143 Sorting sorting;
144 bool zoomEnabled;
145
146 // The resource whose tags we are showing
147 // invalid if we show all tags or a selection
148 KUrl resource;
149 bool showAllTags;
150
151 // the actual nodes
152 QList<TagNode> nodes;
153
154 // just a helper structure for speeding up things
155 QList<QList<TagNode*> > rows;
156
157 TagNode newTagNode;
158 QAction* customNewTagAction;
159
160 TagNode* hoverTag;
161
162 QMatrix zoomMatrix;
163
164 QSize cachedSizeHint;
165 int cachedHfwWidth;
166 int cachedHfwHeight;
167
168 void invalidateCachedValues() {
169 cachedSizeHint = QSize();
170 cachedHfwWidth = -1;
171 }
172
173 int getMinFontSize() const;
174 int getMaxFontSize() const;
175 void updateNodeWeights();
176 void updateNodeFonts();
177 void sortNodes();
178 void rebuildCloud();
179 TagNode* tagAt( const QPoint& pos );
180 TagNode* findTagInRow( const QList<TagNode*>& row, const QPoint& pos );
181 TagNode* nodeForTag( const Tag& tag );
182 int calculateWeight( const Nepomuk::Tag& tag );
183
184 private:
185 TagCloud* m_parent;
186 };
187
188
189 int Nepomuk::TagCloud::Private::getMinFontSize() const
190 {
191 return minFontSize > 0 ? minFontSize : ( 8 * m_parent->font().pointSize() / 10 );
192 }
193
194
195 int Nepomuk::TagCloud::Private::getMaxFontSize() const
196 {
197 return maxFontSize > 0 ? maxFontSize : ( 22 * m_parent->font().pointSize() / 10 );
198 }
199
200
201 int Nepomuk::TagCloud::Private::calculateWeight( const Nepomuk::Tag& tag )
202 {
203 // stupid SPARQL has no functions such as count!
204 Soprano::QueryResultIterator it
205 = ResourceManager::instance()->mainModel()->executeQuery( QString( "select ?r where { ?r <%1> <%2> . }" )
206 .arg( Soprano::Vocabulary::NAO::hasTag().toString() )
207 .arg( QString::fromAscii( tag.resourceUri().toEncoded() ) ),
208 Soprano::Query::QueryLanguageSparql );
209 int w = 0;
210 while ( it.next() ) {
211 ++w;
212 }
213 return w;
214 }
215
216
217 void Nepomuk::TagCloud::Private::updateNodeWeights()
218 {
219 bool changedWeights = false;
220 for ( QList<TagNode>::iterator it = nodes.begin();
221 it != nodes.end(); ++it ) {
222 TagNode& node = *it;
223 int w = calculateWeight( node.tag );
224 if ( w != node.weight ) {
225 node.weight = w;
226 changedWeights = true;
227 }
228 }
229 if ( changedWeights ) {
230 updateNodeFonts();
231 }
232 }
233
234
235 void Nepomuk::TagCloud::Private::updateNodeFonts()
236 {
237 int maxWeight = 0;
238 int minWeight = 0;
239 for ( QList<TagNode>::iterator it = nodes.begin();
240 it != nodes.end(); ++it ) {
241 TagNode& node = *it;
242 minWeight = qMin( minWeight, node.weight );
243 maxWeight = qMax( maxWeight, node.weight );
244 }
245
246 // calculate font sizes
247 // ----------------------------------------------
248 int usedMinFontSize = getMinFontSize();
249 int usedMaxFontSize = getMaxFontSize();
250 for ( QList<TagNode>::iterator it = nodes.begin();
251 it != nodes.end(); ++it ) {
252 TagNode& node = *it;
253 double normalizedWeight = (double)(node.weight - minWeight) / (double)qMax(maxWeight - minWeight, 1);
254 node.font = m_parent->font();
255 node.font.setPointSize( usedMinFontSize + (int)((double)(usedMaxFontSize-usedMinFontSize) * normalizedWeight) );
256 if( normalizedWeight > 0.8 )
257 node.font.setBold( true );
258 }
259
260 if ( newTagButtonEnabled ) {
261 newTagNode.font = m_parent->font();
262 newTagNode.font.setPointSize( usedMinFontSize );
263 newTagNode.font.setUnderline( true );
264 }
265 }
266
267
268 void Nepomuk::TagCloud::Private::sortNodes()
269 {
270 if ( sorting == SortAlpabetically ) {
271 qSort( nodes.begin(), nodes.end(), tagNodeNameLessThan );
272 }
273 else if ( sorting == SortByWeight ) {
274 qSort( nodes.begin(), nodes.end(), tagNodeWeightLessThan );
275 }
276 else if ( sorting == SortRandom ) {
277 KRandomSequence().randomize( nodes );
278 }
279 }
280
281
282 void Nepomuk::TagCloud::Private::rebuildCloud()
283 {
284 // - Always try to be quadratic
285 // - Always prefer to expand horizontally
286 // - If we cannot fit everything into m_parent->contentsRect(), zoom
287 // - If alignment & Qt::AlignJustify insert spaces between tags
288
289 sortNodes();
290
291 QRect contentsRect = m_parent->contentsRect();
292
293 // initialize the nodes' sizes
294 // ----------------------------------------------
295 for ( QList<TagNode>::iterator it = nodes.begin();
296 it != nodes.end(); ++it ) {
297 TagNode& node = *it;
298 node.rect = QFontMetrics( node.font ).boundingRect( node.text );
299 }
300 if ( newTagButtonEnabled ) {
301 newTagNode.rect = QFontMetrics( newTagNode.font ).boundingRect( customNewTagAction ? customNewTagAction->text() : newTagNode.text );
302 }
303
304
305 // and position the nodes
306 // ----------------------------------------------
307 rows.clear();
308 if ( !nodes.isEmpty() || newTagButtonEnabled ) {
309 if ( 0 ) { // FIXME: make it configurable
310 QRect lineRect;
311 QRect totalRect;
312 QList<TagNode*> row;
313 for ( QList<TagNode>::iterator it = nodes.begin();
314 it != nodes.end(); /* We do increment it below */ ) {
315 TagNode& node = *it;
316
317 int usedSpacing = row.isEmpty() ? 0 : s_hSpacing;
318 if ( lineRect.width() + usedSpacing + node.rect.width() <= contentsRect.width() ) {
319 node.rect.moveBottomLeft( QPoint( lineRect.right() + usedSpacing, lineRect.bottom() ) );
320 QRect newLineRect = lineRect.united( node.rect );
321 newLineRect.moveTopLeft( lineRect.topLeft() );
322 lineRect = newLineRect;
323 row.append( &node );
324
325 // update all other nodes in this line
326 Q_FOREACH( TagNode* n, row ) {
327 n->rect.moveBottom( lineRect.bottom() - ( lineRect.height() - n->rect.height() )/2 );
328 }
329
330 ++it;
331 }
332 else {
333 rows.append( row );
334 row.clear();
335 int newLineTop = lineRect.bottom() + s_vSpacing;
336 lineRect = QRect();
337 lineRect.moveTop( newLineTop );
338 }
339 }
340 rows.append( row );
341 }
342 else {
343 // initialize first row
344 rows.append( QList<TagNode*>() );
345 for ( QList<TagNode>::iterator it = nodes.begin();
346 it != nodes.end(); ++it ) {
347 TagNode& node = *it;
348 rows.first().append( &node );
349 }
350 if ( newTagButtonEnabled ) {
351 rows.first().append( &newTagNode );
352 }
353
354 // calculate the rows
355 QList<QList<TagNode*> > bestRows( rows );
356 QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
357 QSize bestSize( size );
358 while ( ( size.height() < size.width() ||
359 size.width() > contentsRect.width() ) &&
360 size.height() <= contentsRect.height() ) {
361 // find the longest row
362 int maxRow = 0;
363 int maxLen = 0;
364 for ( int i = 0; i < rows.count(); ++i ) {
365 int rowLen = rowLength( rows[i] );
366 if ( rowLen > maxLen ) {
367 maxLen = rowLen;
368 maxRow = i;
369 }
370 }
371
372 // move the last item from the longest row to the next row
373 TagNode* node = rows[maxRow].takeLast();
374 if ( rows.count() <= maxRow+1 ) {
375 rows.append( QList<TagNode*>() );
376 }
377 rows[maxRow+1].prepend( node );
378
379 // update the size
380 size = cloudSize( rows );
381
382 if ( size.width() < bestSize.width() &&
383 ( size.width() > size.height() ||
384 bestSize.width() > contentsRect.width() ) &&
385 size.height() <= contentsRect.height() ) {
386 bestSize = size;
387 bestRows = rows;
388 }
389 }
390 rows = bestRows;
391
392 // position the tags
393 int y = 0;
394 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
395 QList<TagNode*>& row = *rowIt;
396 int h = rowHeight( row );
397 int x = 0;
398 Q_FOREACH( TagNode* node, row ) {
399 node->rect.moveTop( y + ( h - node->rect.height() )/2 );
400 node->rect.moveLeft( x );
401 x += s_hSpacing + node->rect.width();
402 }
403 y += h + s_vSpacing;
404 }
405 }
406
407
408 // let's see if we have to zoom
409 // ----------------------------------------------
410 zoomMatrix = QMatrix();
411 int w = contentsRect.width();
412 if ( zoomEnabled ) {
413 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
414 QList<TagNode*>& row = *rowIt;
415 w = qMax( w, row.last()->rect.right() );
416 }
417 if ( w > contentsRect.width() ) {
418 double zoomFactor = ( double )contentsRect.width() / ( double )w;
419 zoomMatrix.scale( zoomFactor, zoomFactor );
420 }
421 }
422
423 // force horizontal alignment
424 // ----------------------------------------------
425 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
426 QList<TagNode*>& row = *rowIt;
427 int space = /*contentsRect.right()*/w - row.last()->rect.right();
428 if ( alignment & ( Qt::AlignRight|Qt::AlignHCenter ) ) {
429 Q_FOREACH( TagNode* node, row ) {
430 node->rect.moveLeft( node->rect.left() + ( alignment & Qt::AlignRight ? space : space/2 ) );
431 }
432 }
433 else if ( alignment & Qt::AlignJustify && row.count() > 1 ) {
434 space /= ( row.count()-1 );
435 int i = 0;
436 Q_FOREACH( TagNode* node, row ) {
437 node->rect.moveLeft( node->rect.left() + ( space * i++ ) );
438 }
439 }
440 }
441
442 // force vertical alignment
443 // ----------------------------------------------
444 int verticalSpace = contentsRect.bottom() - rows.last().first()->rect.bottom();
445 if ( alignment & ( Qt::AlignBottom|Qt::AlignVCenter ) ) {
446 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
447 Q_FOREACH( TagNode* node, *rowIt ) {
448 node->rect.moveTop( node->rect.top() + ( alignment & Qt::AlignBottom ? verticalSpace : verticalSpace/2 ) );
449 }
450 }
451 }
452
453 for( QList<TagNode>::iterator it = nodes.begin(); it != nodes.end(); ++it ) {
454 it->zoomedRect = zoomMatrix.mapRect( it->rect );
455 }
456 newTagNode.zoomedRect = zoomMatrix.mapRect( newTagNode.rect );
457 }
458
459 m_parent->updateGeometry();
460 m_parent->update();
461 }
462
463
464 // binary search in row
465 TagNode* Nepomuk::TagCloud::Private::findTagInRow( const QList<TagNode*>& row, const QPoint& pos )
466 {
467 int x = m_parent->width() ? row.count() * pos.x() / m_parent->width() : 0;
468
469 int i = 0;
470 while ( 1 ) {
471 if ( x-i >= 0 && x-i < row.count() && row[x-i]->zoomedRect.contains( pos ) ) {
472 return row[x-i];
473 }
474 else if ( x+i >= 0 && x+i < row.count() && row[x+i]->zoomedRect.contains( pos ) ) {
475 return row[x+i];
476 }
477 if ( x-i < 0 && x+i >= row.count() ) {
478 return 0;
479 }
480 ++i;
481 }
482 return 0;
483 }
484
485
486 // binary search in cloud
487 TagNode* Nepomuk::TagCloud::Private::tagAt( const QPoint& pos )
488 {
489 int y = m_parent->height() ? rows.count() * pos.y() / m_parent->height() : 0;
490
491 int i = 0;
492 while ( 1 ) {
493 if ( y-i >= 0 && y-i < rows.count() ) {
494 if ( TagNode* node = findTagInRow( rows[y-i], pos ) ) {
495 return node;
496 }
497 }
498 if ( y+i >= 0 && y+i < rows.count() ) {
499 if ( TagNode* node = findTagInRow( rows[y+i], pos ) ) {
500 return node;
501 }
502 }
503 if ( y-i < 0 && y+i >= rows.count() ) {
504 return 0;
505 }
506 ++i;
507 }
508 return 0;
509 }
510
511
512 TagNode* Nepomuk::TagCloud::Private::nodeForTag( const Tag& tag )
513 {
514 for ( QList<TagNode>::iterator it = nodes.begin();
515 it != nodes.end(); ++it ) {
516 TagNode& node = *it;
517 if ( tag == node.tag ) {
518 return &node;
519 }
520 }
521 return 0;
522 }
523
524
525
526 Nepomuk::TagCloud::TagCloud( QWidget* parent )
527 : QFrame( parent ),
528 d( new Private(this) )
529 {
530 QSizePolicy policy( QSizePolicy::Preferred,
531 QSizePolicy::Preferred );
532 policy.setHeightForWidth( true );
533 setSizePolicy( policy );
534 setMouseTracking( true );
535
536 // Since signals are delivered in no particular order
537 // our slot might be called before the resources are updated
538 // Then, we would use invalid cached data.
539 // By using queued connections this problem should be solved.
540 connect( ResourceManager::instance()->mainModel(),
541 SIGNAL( statementAdded( const Soprano::Statement& ) ),
542 this,
543 SLOT( slotStatementAdded( const Soprano::Statement& ) ),
544 Qt::QueuedConnection );
545 connect( ResourceManager::instance()->mainModel(),
546 SIGNAL( statementRemoved( const Soprano::Statement& ) ),
547 this,
548 SLOT( slotStatementRemoved( const Soprano::Statement& ) ),
549 Qt::QueuedConnection );
550 }
551
552
553 Nepomuk::TagCloud::~TagCloud()
554 {
555 delete d;
556 }
557
558
559 void Nepomuk::TagCloud::setMaxFontSize( int size )
560 {
561 d->invalidateCachedValues();
562 d->maxFontSize = size;
563 d->updateNodeFonts();
564 d->rebuildCloud();
565 }
566
567
568 void Nepomuk::TagCloud::setMinFontSize( int size )
569 {
570 d->invalidateCachedValues();
571 d->minFontSize = size;
572 d->updateNodeFonts();
573 d->rebuildCloud();
574 }
575
576
577 void Nepomuk::TagCloud::setMaxNumberDisplayedTags( int n )
578 {
579 d->maxNumberDisplayedTags = n;
580 d->rebuildCloud();
581 }
582
583
584 void Nepomuk::TagCloud::setSelectionEnabled( bool enabled )
585 {
586 d->selectionEnabled = enabled;
587 update();
588 }
589
590
591 void Nepomuk::TagCloud::setNewTagButtonEnabled( bool enabled )
592 {
593 d->newTagButtonEnabled = enabled;
594 d->rebuildCloud();
595 }
596
597
598 bool Nepomuk::TagCloud::zoomEnabled() const
599 {
600 return d->zoomEnabled;
601 }
602
603
604 void Nepomuk::TagCloud::setZoomEnabled( bool zoom )
605 {
606 if ( d->zoomEnabled != zoom ) {
607 d->zoomEnabled = zoom;
608 d->rebuildCloud();
609 }
610 }
611
612
613 void Nepomuk::TagCloud::setContextMenuEnabled( bool enabled )
614 {
615 Q_UNUSED(enabled);
616 }
617
618
619 void Nepomuk::TagCloud::setAlignment( Qt::Alignment alignment )
620 {
621 d->alignment = alignment;
622 d->rebuildCloud();
623 }
624
625
626 void Nepomuk::TagCloud::setSorting( Sorting s )
627 {
628 d->invalidateCachedValues();
629 d->sorting = s;
630 d->rebuildCloud();
631 }
632
633
634 void Nepomuk::TagCloud::showAllTags()
635 {
636 showTags( Nepomuk::Tag::allTags() );
637 d->showAllTags = true;
638 }
639
640
641 void Nepomuk::TagCloud::showResourceTags( const Resource& resource )
642 {
643 showTags( resource.tags() );
644 d->resource = resource.uri();
645 }
646
647
648 void Nepomuk::TagCloud::showTags( const QList<Tag>& tags )
649 {
650 d->resource = QUrl();
651 d->showAllTags = false;
652 d->invalidateCachedValues();
653 d->nodes.clear();
654 Q_FOREACH( const Tag &tag, tags ) {
655 TagNode node;
656 node.tag = tag;
657 node.weight = d->calculateWeight( tag );
658 node.text = node.tag.genericLabel();
659
660 d->nodes.append( node );
661 }
662 d->updateNodeFonts();
663 d->rebuildCloud();
664 }
665
666
667 void Nepomuk::TagCloud::setTagSelected( const Tag& tag, bool selected )
668 {
669 if ( TagNode* node = d->nodeForTag( tag ) ) {
670 node->selected = selected;
671 if ( d->selectionEnabled ) {
672 update( node->zoomedRect );
673 }
674 }
675 }
676
677
678 QSize Nepomuk::TagCloud::sizeHint() const
679 {
680 // If we have tags d->rebuildCloud() has been called at least once,
681 // thus, we have proper rects (i.e. needed sizes)
682
683 if ( !d->cachedSizeHint.isValid() ) {
684 QList<QList<TagNode*> > rows;
685 rows.append( QList<TagNode*>() );
686 for ( QList<TagNode>::iterator it = d->nodes.begin();
687 it != d->nodes.end(); ++it ) {
688 TagNode& node = *it;
689 rows.first().append( &node );
690 }
691 if ( d->newTagButtonEnabled ) {
692 rows.first().append( &d->newTagNode );
693 }
694
695 QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
696 QSize bestSize( size );
697 while ( size.height() < size.width() ) {
698 // find the longest row
699 int maxRow = 0;
700 int maxLen = 0;
701 for ( int i = 0; i < rows.count(); ++i ) {
702 int rowLen = rowLength( rows[i] );
703 if ( rowLen > maxLen ) {
704 maxLen = rowLen;
705 maxRow = i;
706 }
707 }
708
709 // move the last item from the longest row to the next row
710 TagNode* node = rows[maxRow].takeLast();
711 if ( rows.count() <= maxRow+1 ) {
712 rows.append( QList<TagNode*>() );
713 }
714 rows[maxRow+1].prepend( node );
715
716 // update the size
717 size = cloudSize( rows );
718
719 if ( size.width() < bestSize.width() &&
720 size.width() > size.height() ) {
721 bestSize = size;
722 }
723 }
724
725 d->cachedSizeHint = QSize( bestSize.width() + frameWidth()*2,
726 bestSize.height() + frameWidth()*2 );
727 }
728
729 return d->cachedSizeHint;
730 }
731
732
733 QSize Nepomuk::TagCloud::minimumSizeHint() const
734 {
735 return QFrame::minimumSizeHint();
736 // If we have tags d->rebuildCloud() has been called at least once,
737 // thus, we have proper rects (i.e. needed sizes)
738 if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
739 return QSize( fontMetrics().width( i18nc( "@label Indicator when no tags defined", "No Tags" ) ), fontMetrics().height() );
740 }
741 else {
742 QSize size;
743 for ( QList<TagNode>::iterator it = d->nodes.begin();
744 it != d->nodes.end(); ++it ) {
745 size.setWidth( qMax( size.width(), ( *it ).rect.width() ) );
746 size.setHeight( qMax( size.height(), ( *it ).rect.height() ) );
747 }
748 if ( d->newTagButtonEnabled ) {
749 size.setWidth( qMax( size.width(), d->newTagNode.rect.width() ) );
750 size.setHeight( qMax( size.height(), d->newTagNode.rect.height() ) );
751 }
752 size.setWidth( size.width() + frameWidth()*2 );
753 size.setHeight( size.height() + frameWidth()*2 );
754 return size;
755 }
756 }
757
758
759 int Nepomuk::TagCloud::heightForWidth( int contentsWidth ) const
760 {
761 // If we have tags d->rebuildCloud() has been called at least once,
762 // thus, we have proper rects (i.e. needed sizes)
763
764 if ( d->cachedHfwWidth != contentsWidth ) {
765 // have to keep in mind the frame
766 contentsWidth -= frameWidth()*2;
767
768 QList<TagNode*> allNodes;
769 for ( QList<TagNode>::iterator it = d->nodes.begin();
770 it != d->nodes.end(); ++it ) {
771 TagNode& node = *it;
772 allNodes.append( &node );
773 }
774 if ( d->newTagButtonEnabled ) {
775 allNodes.append( &d->newTagNode );
776 }
777
778 int h = 0;
779 bool newRow = true;
780 int rowW = 0;
781 int rowH = 0;
782 int maxW = 0;
783 for ( int i = 0; i < allNodes.count(); ++i ) {
784 int w = rowW;
785 if ( !newRow ) {
786 w += s_hSpacing;
787 }
788 newRow = false;
789 w += allNodes[i]->rect.width();
790 if ( w <= contentsWidth ) {
791 rowH = qMax( rowH, allNodes[i]->rect.height() );
792 rowW = w;
793 }
794 else {
795 if ( h > 0 ) {
796 h += s_vSpacing;
797 }
798 h += rowH;
799 rowH = allNodes[i]->rect.height();
800 rowW = allNodes[i]->rect.width();
801 }
802 maxW = qMax( maxW, rowW );
803 }
804 if ( rowH > 0 ) {
805 h += s_vSpacing + rowH;
806 }
807
808 d->cachedHfwWidth = contentsWidth;
809 d->cachedHfwHeight = h;
810
811 // zooming
812 if ( maxW > contentsWidth ) {
813 d->cachedHfwHeight = d->cachedHfwHeight * contentsWidth / maxW;
814 }
815 }
816
817 return d->cachedHfwHeight + frameWidth()*2;
818 }
819
820
821 void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e )
822 {
823 QFrame::resizeEvent( e );
824 d->rebuildCloud();
825 update();
826 }
827
828
829 void Nepomuk::TagCloud::paintEvent( QPaintEvent* e )
830 {
831 QFrame::paintEvent( e );
832
833 KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText );
834 KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText );
835 KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText );
836
837 QPainter p( this );
838 QRegion paintRegion = e->region();
839
840 p.save();
841 p.setMatrix( d->zoomMatrix );
842
843 for ( QList<TagNode>::iterator it = d->nodes.begin();
844 it != d->nodes.end(); ++it ) {
845 TagNode& node = *it;
846
847 if ( paintRegion.contains( node.zoomedRect ) ) {
848 p.setFont( node.font );
849
850 if ( &node == d->hoverTag ) {
851 p.setPen( hoverTextBrush.brush( this ).color() );
852 }
853 else if ( d->selectionEnabled && node.selected ) {
854 p.setPen( activeTextBrush.brush( this ).color() );
855 }
856 else {
857 p.setPen( normalTextBrush.brush( this ).color() );
858 }
859 p.drawText( node.rect, Qt::AlignCenter, node.text );
860 }
861 }
862
863 if ( d->newTagButtonEnabled ) {
864 p.setFont( d->newTagNode.font );
865 if ( &d->newTagNode == d->hoverTag ) {
866 p.setPen( hoverTextBrush.brush( this ).color() );
867 }
868 else {
869 p.setPen( normalTextBrush.brush( this ).color() );
870 }
871 p.drawText( d->newTagNode.rect, Qt::AlignCenter, d->customNewTagAction ? d->customNewTagAction->text() : d->newTagNode.text );
872 }
873
874 p.restore();
875 }
876
877
878 void Nepomuk::TagCloud::mousePressEvent( QMouseEvent* e )
879 {
880 if ( e->button() == Qt::LeftButton ) {
881 if ( TagNode* node = d->tagAt( e->pos() ) ) {
882 kDebug() << "clicked" << node->text;
883 if ( node == &d->newTagNode ) {
884 if ( d->customNewTagAction ) {
885 d->customNewTagAction->trigger();
886 }
887 else {
888 // FIXME: nicer gui
889 Tag newTag = NewTagDialog::createTag( this );
890 if ( newTag.isValid() ) {
891 emit tagAdded( newTag );
892 }
893 }
894 }
895 else {
896 emit tagClicked( node->tag );
897 if ( d->selectionEnabled ) {
898 kDebug() << "Toggleing tag" << node->text;
899 node->selected = !node->selected;
900 emit tagToggled( node->tag, node->selected );
901 update( node->zoomedRect );
902 }
903 }
904 }
905 }
906 }
907
908
909 void Nepomuk::TagCloud::mouseMoveEvent( QMouseEvent* e )
910 {
911 if ( e->buttons() == Qt::NoButton ) {
912
913 TagNode* oldHoverTag = d->hoverTag;
914
915 if ( ( d->hoverTag = d->tagAt( e->pos() ) ) &&
916 !d->selectionEnabled ) {
917 setCursor( Qt::PointingHandCursor );
918 }
919 else if ( d->newTagButtonEnabled &&
920 d->newTagNode.zoomedRect.contains( e->pos() ) ) {
921 d->hoverTag = &d->newTagNode;
922 setCursor( Qt::PointingHandCursor );
923 }
924 else {
925 unsetCursor();
926 }
927
928 if ( oldHoverTag || d->hoverTag ) {
929 QRect updateRect;
930 if ( d->hoverTag )
931 updateRect = updateRect.united( d->hoverTag->zoomedRect );
932 if ( oldHoverTag )
933 updateRect = updateRect.united( oldHoverTag->zoomedRect );
934
935 update( updateRect );
936 }
937 }
938 }
939
940
941 void Nepomuk::TagCloud::leaveEvent( QEvent* )
942 {
943 unsetCursor();
944 if ( d->hoverTag ) {
945 QRect updateRect = d->hoverTag->zoomedRect;
946 d->hoverTag = 0;
947 update( updateRect );
948 }
949 }
950
951
952 void Nepomuk::TagCloud::slotStatementAdded( const Soprano::Statement& s )
953 {
954 if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
955 s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
956 // new tag created
957 if ( d->showAllTags ) {
958 showAllTags();
959 }
960 }
961 else if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
962 if ( s.subject().uri() == d->resource ) {
963 showResourceTags( d->resource );
964 }
965 else {
966 // weights might have changed
967 d->updateNodeWeights();
968 d->rebuildCloud();
969 }
970 }
971 }
972
973
974 void Nepomuk::TagCloud::slotStatementRemoved( const Soprano::Statement& s )
975 {
976 // FIXME: In theory might contain empty nodes as wildcards
977
978 if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
979 if ( d->resource.isValid() &&
980 d->resource == s.subject().uri() ) {
981 showResourceTags( d->resource );
982 }
983 else {
984 // weights might have changed
985 d->updateNodeWeights();
986 d->rebuildCloud();
987 }
988 }
989 else if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
990 s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
991 // tag deleted
992 if ( d->showAllTags ) {
993 showAllTags();
994 }
995 }
996 }
997
998
999 void Nepomuk::TagCloud::setCustomNewTagAction( QAction* action )
1000 {
1001 d->customNewTagAction = action;
1002 setNewTagButtonEnabled( action != 0 );
1003 }
1004
1005 #include "tagcloud.moc"