]> cloud.milkyroute.net Git - dolphin.git/blob - src/tagcloud/tagcloud.cpp
Provide functionality for auto-expanding folders (the whole patch has been provided...
[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 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( 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 }
616
617
618 void Nepomuk::TagCloud::setAlignment( Qt::Alignment alignment )
619 {
620 d->alignment = alignment;
621 d->rebuildCloud();
622 }
623
624
625 void Nepomuk::TagCloud::setSorting( Sorting s )
626 {
627 d->invalidateCachedValues();
628 d->sorting = s;
629 d->rebuildCloud();
630 }
631
632
633 void Nepomuk::TagCloud::showAllTags()
634 {
635 showTags( Nepomuk::Tag::allTags() );
636 d->showAllTags = true;
637 }
638
639
640 void Nepomuk::TagCloud::showResourceTags( const Resource& resource )
641 {
642 showTags( resource.tags() );
643 d->resource = resource.uri();
644 }
645
646
647 void Nepomuk::TagCloud::showTags( const QList<Tag>& tags )
648 {
649 d->resource = QUrl();
650 d->showAllTags = false;
651 d->invalidateCachedValues();
652 d->nodes.clear();
653 Q_FOREACH( const Tag &tag, tags ) {
654 TagNode node;
655 node.tag = tag;
656 node.weight = d->calculateWeight( tag );
657 node.text = node.tag.genericLabel();
658
659 d->nodes.append( node );
660 }
661 d->updateNodeFonts();
662 d->rebuildCloud();
663 }
664
665
666 void Nepomuk::TagCloud::setTagSelected( const Tag& tag, bool selected )
667 {
668 if ( TagNode* node = d->nodeForTag( tag ) ) {
669 node->selected = selected;
670 if ( d->selectionEnabled ) {
671 update( node->zoomedRect );
672 }
673 }
674 }
675
676
677 QSize Nepomuk::TagCloud::sizeHint() const
678 {
679 // If we have tags d->rebuildCloud() has been called at least once,
680 // thus, we have proper rects (i.e. needed sizes)
681
682 if ( !d->cachedSizeHint.isValid() ) {
683 QList<QList<TagNode*> > rows;
684 rows.append( QList<TagNode*>() );
685 for ( QList<TagNode>::iterator it = d->nodes.begin();
686 it != d->nodes.end(); ++it ) {
687 TagNode& node = *it;
688 rows.first().append( &node );
689 }
690 if ( d->newTagButtonEnabled ) {
691 rows.first().append( &d->newTagNode );
692 }
693
694 QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
695 QSize bestSize( size );
696 while ( size.height() < size.width() ) {
697 // find the longest row
698 int maxRow = 0;
699 int maxLen = 0;
700 for ( int i = 0; i < rows.count(); ++i ) {
701 int rowLen = rowLength( rows[i] );
702 if ( rowLen > maxLen ) {
703 maxLen = rowLen;
704 maxRow = i;
705 }
706 }
707
708 // move the last item from the longest row to the next row
709 TagNode* node = rows[maxRow].takeLast();
710 if ( rows.count() <= maxRow+1 ) {
711 rows.append( QList<TagNode*>() );
712 }
713 rows[maxRow+1].prepend( node );
714
715 // update the size
716 size = cloudSize( rows );
717
718 if ( size.width() < bestSize.width() &&
719 size.width() > size.height() ) {
720 bestSize = size;
721 }
722 }
723
724 d->cachedSizeHint = QSize( bestSize.width() + frameWidth()*2,
725 bestSize.height() + frameWidth()*2 );
726 }
727
728 return d->cachedSizeHint;
729 }
730
731
732 QSize Nepomuk::TagCloud::minimumSizeHint() const
733 {
734 return QFrame::minimumSizeHint();
735 // If we have tags d->rebuildCloud() has been called at least once,
736 // thus, we have proper rects (i.e. needed sizes)
737 if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
738 return QSize( fontMetrics().width( i18nc( "@label Indicator when no tags defined", "No Tags" ) ), fontMetrics().height() );
739 }
740 else {
741 QSize size;
742 for ( QList<TagNode>::iterator it = d->nodes.begin();
743 it != d->nodes.end(); ++it ) {
744 size.setWidth( qMax( size.width(), ( *it ).rect.width() ) );
745 size.setHeight( qMax( size.height(), ( *it ).rect.height() ) );
746 }
747 if ( d->newTagButtonEnabled ) {
748 size.setWidth( qMax( size.width(), d->newTagNode.rect.width() ) );
749 size.setHeight( qMax( size.height(), d->newTagNode.rect.height() ) );
750 }
751 size.setWidth( size.width() + frameWidth()*2 );
752 size.setHeight( size.height() + frameWidth()*2 );
753 return size;
754 }
755 }
756
757
758 int Nepomuk::TagCloud::heightForWidth( int contentsWidth ) const
759 {
760 // If we have tags d->rebuildCloud() has been called at least once,
761 // thus, we have proper rects (i.e. needed sizes)
762
763 if ( d->cachedHfwWidth != contentsWidth ) {
764 // have to keep in mind the frame
765 contentsWidth -= frameWidth()*2;
766
767 QList<TagNode*> allNodes;
768 for ( QList<TagNode>::iterator it = d->nodes.begin();
769 it != d->nodes.end(); ++it ) {
770 TagNode& node = *it;
771 allNodes.append( &node );
772 }
773 if ( d->newTagButtonEnabled ) {
774 allNodes.append( &d->newTagNode );
775 }
776
777 int h = 0;
778 bool newRow = true;
779 int rowW = 0;
780 int rowH = 0;
781 int maxW = 0;
782 for ( int i = 0; i < allNodes.count(); ++i ) {
783 int w = rowW;
784 if ( !newRow ) {
785 w += s_hSpacing;
786 }
787 newRow = false;
788 w += allNodes[i]->rect.width();
789 if ( w <= contentsWidth ) {
790 rowH = qMax( rowH, allNodes[i]->rect.height() );
791 rowW = w;
792 }
793 else {
794 if ( h > 0 ) {
795 h += s_vSpacing;
796 }
797 h += rowH;
798 rowH = allNodes[i]->rect.height();
799 rowW = allNodes[i]->rect.width();
800 }
801 maxW = qMax( maxW, rowW );
802 }
803 if ( rowH > 0 ) {
804 h += s_vSpacing + rowH;
805 }
806
807 d->cachedHfwWidth = contentsWidth;
808 d->cachedHfwHeight = h;
809
810 // zooming
811 if ( maxW > contentsWidth ) {
812 d->cachedHfwHeight = d->cachedHfwHeight * contentsWidth / maxW;
813 }
814 }
815
816 return d->cachedHfwHeight + frameWidth()*2;
817 }
818
819
820 void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e )
821 {
822 QFrame::resizeEvent( e );
823 d->rebuildCloud();
824 update();
825 }
826
827
828 void Nepomuk::TagCloud::paintEvent( QPaintEvent* e )
829 {
830 QFrame::paintEvent( e );
831
832 KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText );
833 KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText );
834 KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText );
835
836 QPainter p( this );
837 QRegion paintRegion = e->region();
838
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 void Nepomuk::TagCloud::mousePressEvent( QMouseEvent* e )
878 {
879 if ( e->button() == Qt::LeftButton ) {
880 if ( TagNode* node = d->tagAt( e->pos() ) ) {
881 kDebug() << "clicked" << node->text;
882 if ( node == &d->newTagNode ) {
883 if ( d->customNewTagAction ) {
884 d->customNewTagAction->trigger();
885 }
886 else {
887 // FIXME: nicer gui
888 Tag newTag = NewTagDialog::createTag( this );
889 if ( newTag.isValid() ) {
890 emit tagAdded( newTag );
891 }
892 }
893 }
894 else {
895 emit tagClicked( node->tag );
896 if ( d->selectionEnabled ) {
897 kDebug() << "Toggleing tag" << node->text;
898 node->selected = !node->selected;
899 emit tagToggled( node->tag, node->selected );
900 update( node->zoomedRect );
901 }
902 }
903 }
904 }
905 }
906
907
908 void Nepomuk::TagCloud::mouseMoveEvent( QMouseEvent* e )
909 {
910 if ( e->buttons() == Qt::NoButton ) {
911
912 TagNode* oldHoverTag = d->hoverTag;
913
914 if ( ( d->hoverTag = d->tagAt( e->pos() ) ) &&
915 !d->selectionEnabled ) {
916 setCursor( Qt::PointingHandCursor );
917 }
918 else if ( d->newTagButtonEnabled &&
919 d->newTagNode.zoomedRect.contains( e->pos() ) ) {
920 d->hoverTag = &d->newTagNode;
921 setCursor( Qt::PointingHandCursor );
922 }
923 else {
924 unsetCursor();
925 }
926
927 if ( oldHoverTag || d->hoverTag ) {
928 QRect updateRect;
929 if ( d->hoverTag )
930 updateRect = updateRect.united( d->hoverTag->zoomedRect );
931 if ( oldHoverTag )
932 updateRect = updateRect.united( oldHoverTag->zoomedRect );
933
934 update( updateRect );
935 }
936 }
937 }
938
939
940 void Nepomuk::TagCloud::leaveEvent( QEvent* )
941 {
942 unsetCursor();
943 if ( d->hoverTag ) {
944 QRect updateRect = d->hoverTag->zoomedRect;
945 d->hoverTag = 0;
946 update( updateRect );
947 }
948 }
949
950
951 void Nepomuk::TagCloud::slotStatementAdded( const Soprano::Statement& s )
952 {
953 if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
954 s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
955 // new tag created
956 if ( d->showAllTags ) {
957 showAllTags();
958 }
959 }
960 else if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
961 if ( s.subject().uri() == d->resource ) {
962 showResourceTags( d->resource );
963 }
964 else {
965 // weights might have changed
966 d->updateNodeWeights();
967 d->rebuildCloud();
968 }
969 }
970 }
971
972
973 void Nepomuk::TagCloud::slotStatementRemoved( const Soprano::Statement& s )
974 {
975 // FIXME: In theory might contain empty nodes as wildcards
976
977 if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
978 if ( d->resource.isValid() &&
979 d->resource == s.subject().uri() ) {
980 showResourceTags( d->resource );
981 }
982 else {
983 // weights might have changed
984 d->updateNodeWeights();
985 d->rebuildCloud();
986 }
987 }
988 else if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
989 s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
990 // tag deleted
991 if ( d->showAllTags ) {
992 showAllTags();
993 }
994 }
995 }
996
997
998 void Nepomuk::TagCloud::setCustomNewTagAction( QAction* action )
999 {
1000 d->customNewTagAction = action;
1001 setNewTagButtonEnabled( action != 0 );
1002 }
1003
1004 #include "tagcloud.moc"