]> cloud.milkyroute.net Git - dolphin.git/blob - src/tagcloud/tagcloud.cpp
Implemented tagging of multiple files at the same time.
[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 = i18n( "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 QUrl 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( tag.resourceUri().toString() ),
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 if ( nodes.isEmpty() && !newTagButtonEnabled ) {
285 return;
286 }
287
288 // - Always try to be quadratic
289 // - Always prefer to expand horizontally
290 // - If we cannot fit everything into m_parent->contentsRect(), zoom
291 // - If alignment & Qt::AlignJustify insert spaces between tags
292
293 sortNodes();
294
295 QRect contentsRect = m_parent->contentsRect();
296
297 // initialize the nodes' sizes
298 // ----------------------------------------------
299 for ( QList<TagNode>::iterator it = nodes.begin();
300 it != nodes.end(); ++it ) {
301 TagNode& node = *it;
302 node.rect = QFontMetrics( node.font ).boundingRect( node.text );
303 }
304 if ( newTagButtonEnabled ) {
305 newTagNode.rect = QFontMetrics( newTagNode.font ).boundingRect( customNewTagAction ? customNewTagAction->text() : newTagNode.text );
306 }
307
308
309 // and position the nodes
310 // ----------------------------------------------
311 rows.clear();
312 if ( 0 ) { // FIXME: make it configurable
313 QRect lineRect;
314 QRect totalRect;
315 QList<TagNode*> row;
316 for ( QList<TagNode>::iterator it = nodes.begin();
317 it != nodes.end(); /* We do increment it below */ ) {
318 TagNode& node = *it;
319
320 int usedSpacing = row.isEmpty() ? 0 : s_hSpacing;
321 if ( lineRect.width() + usedSpacing + node.rect.width() <= contentsRect.width() ) {
322 node.rect.moveBottomLeft( QPoint( lineRect.right() + usedSpacing, lineRect.bottom() ) );
323 QRect newLineRect = lineRect.united( node.rect );
324 newLineRect.moveTopLeft( lineRect.topLeft() );
325 lineRect = newLineRect;
326 row.append( &node );
327
328 // update all other nodes in this line
329 Q_FOREACH( TagNode* n, row ) {
330 n->rect.moveBottom( lineRect.bottom() - ( lineRect.height() - n->rect.height() )/2 );
331 }
332
333 ++it;
334 }
335 else {
336 rows.append( row );
337 row.clear();
338 int newLineTop = lineRect.bottom() + s_vSpacing;
339 lineRect = QRect();
340 lineRect.moveTop( newLineTop );
341 }
342 }
343 rows.append( row );
344 }
345 else {
346 // initialize first row
347 rows.append( QList<TagNode*>() );
348 for ( QList<TagNode>::iterator it = nodes.begin();
349 it != nodes.end(); ++it ) {
350 TagNode& node = *it;
351 rows.first().append( &node );
352 }
353 if ( newTagButtonEnabled ) {
354 rows.first().append( &newTagNode );
355 }
356
357 // calculate the rows
358 QList<QList<TagNode*> > bestRows( rows );
359 QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
360 QSize bestSize( size );
361 while ( ( size.height() < size.width() ||
362 size.width() > contentsRect.width() ) &&
363 size.height() <= contentsRect.height() ) {
364 // find the longest row
365 int maxRow = 0;
366 int maxLen = 0;
367 for ( int i = 0; i < rows.count(); ++i ) {
368 int rowLen = rowLength( rows[i] );
369 if ( rowLen > maxLen ) {
370 maxLen = rowLen;
371 maxRow = i;
372 }
373 }
374
375 // move the last item from the longest row to the next row
376 TagNode* node = rows[maxRow].takeLast();
377 if ( rows.count() <= maxRow+1 ) {
378 rows.append( QList<TagNode*>() );
379 }
380 rows[maxRow+1].prepend( node );
381
382 // update the size
383 size = cloudSize( rows );
384
385 if ( size.width() < bestSize.width() &&
386 ( size.width() > size.height() ||
387 bestSize.width() > contentsRect.width() ) &&
388 size.height() <= contentsRect.height() ) {
389 bestSize = size;
390 bestRows = rows;
391 }
392 }
393 rows = bestRows;
394
395 // position the tags
396 int y = 0;
397 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
398 QList<TagNode*>& row = *rowIt;
399 int h = rowHeight( row );
400 int x = 0;
401 Q_FOREACH( TagNode* node, row ) {
402 node->rect.moveTop( y + ( h - node->rect.height() )/2 );
403 node->rect.moveLeft( x );
404 x += s_hSpacing + node->rect.width();
405 }
406 y += h + s_vSpacing;
407 }
408 }
409
410
411 // let's see if we have to zoom
412 // ----------------------------------------------
413 zoomMatrix = QMatrix();
414 int w = contentsRect.width();
415 if ( zoomEnabled ) {
416 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
417 QList<TagNode*>& row = *rowIt;
418 w = qMax( w, row.last()->rect.right() );
419 }
420 if ( w > contentsRect.width() ) {
421 double zoomFactor = ( double )contentsRect.width() / ( double )w;
422 zoomMatrix.scale( zoomFactor, zoomFactor );
423 }
424 }
425
426 // force horizontal alignment
427 // ----------------------------------------------
428 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
429 QList<TagNode*>& row = *rowIt;
430 int space = /*contentsRect.right()*/w - row.last()->rect.right();
431 if ( alignment & ( Qt::AlignRight|Qt::AlignHCenter ) ) {
432 Q_FOREACH( TagNode* node, row ) {
433 node->rect.moveLeft( node->rect.left() + ( alignment & Qt::AlignRight ? space : space/2 ) );
434 }
435 }
436 else if ( alignment & Qt::AlignJustify && row.count() > 1 ) {
437 space /= ( row.count()-1 );
438 int i = 0;
439 Q_FOREACH( TagNode* node, row ) {
440 node->rect.moveLeft( node->rect.left() + ( space * i++ ) );
441 }
442 }
443 }
444
445 // force vertical alignment
446 // ----------------------------------------------
447 int verticalSpace = contentsRect.bottom() - rows.last().first()->rect.bottom();
448 if ( alignment & ( Qt::AlignBottom|Qt::AlignVCenter ) ) {
449 for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
450 Q_FOREACH( TagNode* node, *rowIt ) {
451 node->rect.moveTop( node->rect.top() + ( alignment & Qt::AlignBottom ? verticalSpace : verticalSpace/2 ) );
452 }
453 }
454 }
455
456 for( QList<TagNode>::iterator it = nodes.begin(); it != nodes.end(); ++it ) {
457 it->zoomedRect = zoomMatrix.mapRect( it->rect );
458 }
459 newTagNode.zoomedRect = zoomMatrix.mapRect( newTagNode.rect );
460
461 m_parent->updateGeometry();
462 m_parent->update();
463 }
464
465
466 // binary search in row
467 TagNode* Nepomuk::TagCloud::Private::findTagInRow( const QList<TagNode*>& row, const QPoint& pos )
468 {
469 int x = row.count() * pos.x() / m_parent->width();
470 int i = 0;
471 while ( 1 ) {
472 if ( x-i >= 0 && x-i < row.count() && row[x-i]->zoomedRect.contains( pos ) ) {
473 return row[x-i];
474 }
475 else if ( x+i >= 0 && x+i < row.count() && row[x+i]->zoomedRect.contains( pos ) ) {
476 return row[x+i];
477 }
478 if ( x-i < 0 && x+i >= row.count() ) {
479 return 0;
480 }
481 ++i;
482 }
483 return 0;
484 }
485
486
487 // binary search in cloud
488 TagNode* Nepomuk::TagCloud::Private::tagAt( const QPoint& pos )
489 {
490 int y = rows.count() * pos.y() / m_parent->height();
491
492 int i = 0;
493 while ( 1 ) {
494 if ( y-i >= 0 && y-i < rows.count() ) {
495 if ( TagNode* node = findTagInRow( rows[y-i], pos ) ) {
496 return node;
497 }
498 }
499 if ( y+i >= 0 && y+i < rows.count() ) {
500 if ( TagNode* node = findTagInRow( rows[y+i], pos ) ) {
501 return node;
502 }
503 }
504 if ( y-i < 0 && y+i >= rows.count() ) {
505 return 0;
506 }
507 ++i;
508 }
509 return 0;
510 }
511
512
513 TagNode* Nepomuk::TagCloud::Private::nodeForTag( const Tag& tag )
514 {
515 for ( QList<TagNode>::iterator it = nodes.begin();
516 it != nodes.end(); ++it ) {
517 TagNode& node = *it;
518 if ( tag == node.tag ) {
519 return &node;
520 }
521 }
522 return 0;
523 }
524
525
526
527 Nepomuk::TagCloud::TagCloud( QWidget* parent )
528 : QFrame( parent ),
529 d( new Private(this) )
530 {
531 QSizePolicy policy( QSizePolicy::Preferred,
532 QSizePolicy::Preferred );
533 policy.setHeightForWidth( true );
534 setSizePolicy( policy );
535 setMouseTracking( true );
536
537 // Since signals are delivered in no particular order
538 // our slot might be called before the resources are updated
539 // Then, we would use invalid cached data.
540 // By using queued connections this problem should be solved.
541 connect( ResourceManager::instance()->mainModel(),
542 SIGNAL( statementAdded( const Soprano::Statement& ) ),
543 this,
544 SLOT( slotStatementAdded( const Soprano::Statement& ) ),
545 Qt::QueuedConnection );
546 connect( ResourceManager::instance()->mainModel(),
547 SIGNAL( statementRemoved( const Soprano::Statement& ) ),
548 this,
549 SLOT( slotStatementRemoved( const Soprano::Statement& ) ),
550 Qt::QueuedConnection );
551 }
552
553
554 Nepomuk::TagCloud::~TagCloud()
555 {
556 delete d;
557 }
558
559
560 void Nepomuk::TagCloud::setMaxFontSize( int size )
561 {
562 d->invalidateCachedValues();
563 d->maxFontSize = size;
564 d->updateNodeFonts();
565 d->rebuildCloud();
566 }
567
568
569 void Nepomuk::TagCloud::setMinFontSize( int size )
570 {
571 d->invalidateCachedValues();
572 d->minFontSize = size;
573 d->updateNodeFonts();
574 d->rebuildCloud();
575 }
576
577
578 void Nepomuk::TagCloud::setMaxNumberDisplayedTags( int n )
579 {
580 d->maxNumberDisplayedTags = n;
581 d->rebuildCloud();
582 }
583
584
585 void Nepomuk::TagCloud::setSelectionEnabled( bool enabled )
586 {
587 d->selectionEnabled = enabled;
588 update();
589 }
590
591
592 void Nepomuk::TagCloud::setNewTagButtonEnabled( bool enabled )
593 {
594 d->newTagButtonEnabled = enabled;
595 d->rebuildCloud();
596 }
597
598
599 bool Nepomuk::TagCloud::zoomEnabled() const
600 {
601 return d->zoomEnabled;
602 }
603
604
605 void Nepomuk::TagCloud::setZoomEnabled( bool zoom )
606 {
607 if ( d->zoomEnabled != zoom ) {
608 d->zoomEnabled = zoom;
609 d->rebuildCloud();
610 }
611 }
612
613
614 void Nepomuk::TagCloud::setContextMenuEnabled( bool enabled )
615 {
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( 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( i18n( "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 // FIXME: add zoom here
765
766 if ( d->cachedHfwWidth != contentsWidth ) {
767 // have to keep in mind the frame
768 contentsWidth -= frameWidth()*2;
769
770 QList<TagNode*> allNodes;
771 for ( QList<TagNode>::iterator it = d->nodes.begin();
772 it != d->nodes.end(); ++it ) {
773 TagNode& node = *it;
774 allNodes.append( &node );
775 }
776 if ( d->newTagButtonEnabled ) {
777 allNodes.append( &d->newTagNode );
778 }
779
780 int h = 0;
781 bool newRow = true;
782 int rowW = 0;
783 int rowH = 0;
784 for ( int i = 0; i < allNodes.count(); ++i ) {
785 int w = rowW;
786 if ( !newRow ) {
787 w += s_hSpacing;
788 }
789 newRow = false;
790 w += allNodes[i]->rect.width();
791 if ( w <= contentsWidth ) {
792 rowH = qMax( rowH, allNodes[i]->rect.height() );
793 rowW = w;
794 }
795 else {
796 if ( h > 0 ) {
797 h += s_vSpacing;
798 }
799 h += rowH;
800 rowH = allNodes[i]->rect.height();
801 rowW = allNodes[i]->rect.width();
802 }
803 }
804 if ( rowH > 0 ) {
805 h += s_vSpacing + rowH;
806 }
807
808 d->cachedHfwWidth = contentsWidth;
809 d->cachedHfwHeight = h;
810 }
811
812 return d->cachedHfwHeight + frameWidth()*2;
813 }
814
815
816 void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e )
817 {
818 QFrame::resizeEvent( e );
819 d->rebuildCloud();
820 update();
821 }
822
823
824 void Nepomuk::TagCloud::paintEvent( QPaintEvent* e )
825 {
826 QFrame::paintEvent( e );
827
828 KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText );
829 KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText );
830 KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText );
831
832 QPainter p( this );
833 QRegion paintRegion = e->region();
834
835 if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
836 p.drawText( contentsRect(), d->alignment, i18n( "No Tags" ) );
837 }
838 else {
839 p.save();
840 p.setMatrix( d->zoomMatrix );
841
842 for ( QList<TagNode>::iterator it = d->nodes.begin();
843 it != d->nodes.end(); ++it ) {
844 TagNode& node = *it;
845
846 if ( paintRegion.contains( node.zoomedRect ) ) {
847 p.setFont( node.font );
848
849 if ( &node == d->hoverTag ) {
850 p.setPen( hoverTextBrush.brush( this ).color() );
851 }
852 else if ( d->selectionEnabled && node.selected ) {
853 p.setPen( activeTextBrush.brush( this ).color() );
854 }
855 else {
856 p.setPen( normalTextBrush.brush( this ).color() );
857 }
858 p.drawText( node.rect, Qt::AlignCenter, node.text );
859 }
860 }
861
862 if ( d->newTagButtonEnabled ) {
863 p.setFont( d->newTagNode.font );
864 if ( &d->newTagNode == d->hoverTag ) {
865 p.setPen( hoverTextBrush.brush( this ).color() );
866 }
867 else {
868 p.setPen( normalTextBrush.brush( this ).color() );
869 }
870 p.drawText( d->newTagNode.rect, Qt::AlignCenter, d->customNewTagAction ? d->customNewTagAction->text() : d->newTagNode.text );
871 }
872
873 p.restore();
874 }
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"