]> cloud.milkyroute.net Git - dolphin.git/blob - src/tagcloud/tagcloud.cpp
respect the inline-renaming setting also for the Folder Panel (= treeview)
[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 = row.count() * pos.x() / m_parent->width();
468 int i = 0;
469 while ( 1 ) {
470 if ( x-i >= 0 && x-i < row.count() && row[x-i]->zoomedRect.contains( pos ) ) {
471 return row[x-i];
472 }
473 else if ( x+i >= 0 && x+i < row.count() && row[x+i]->zoomedRect.contains( pos ) ) {
474 return row[x+i];
475 }
476 if ( x-i < 0 && x+i >= row.count() ) {
477 return 0;
478 }
479 ++i;
480 }
481 return 0;
482 }
483
484
485 // binary search in cloud
486 TagNode* Nepomuk::TagCloud::Private::tagAt( const QPoint& pos )
487 {
488 int y = rows.count() * pos.y() / m_parent->height();
489
490 int i = 0;
491 while ( 1 ) {
492 if ( y-i >= 0 && y-i < rows.count() ) {
493 if ( TagNode* node = findTagInRow( rows[y-i], pos ) ) {
494 return node;
495 }
496 }
497 if ( y+i >= 0 && y+i < rows.count() ) {
498 if ( TagNode* node = findTagInRow( rows[y+i], pos ) ) {
499 return node;
500 }
501 }
502 if ( y-i < 0 && y+i >= rows.count() ) {
503 return 0;
504 }
505 ++i;
506 }
507 return 0;
508 }
509
510
511 TagNode* Nepomuk::TagCloud::Private::nodeForTag( const Tag& tag )
512 {
513 for ( QList<TagNode>::iterator it = nodes.begin();
514 it != nodes.end(); ++it ) {
515 TagNode& node = *it;
516 if ( tag == node.tag ) {
517 return &node;
518 }
519 }
520 return 0;
521 }
522
523
524
525 Nepomuk::TagCloud::TagCloud( QWidget* parent )
526 : QFrame( parent ),
527 d( new Private(this) )
528 {
529 QSizePolicy policy( QSizePolicy::Preferred,
530 QSizePolicy::Preferred );
531 policy.setHeightForWidth( true );
532 setSizePolicy( policy );
533 setMouseTracking( true );
534
535 // Since signals are delivered in no particular order
536 // our slot might be called before the resources are updated
537 // Then, we would use invalid cached data.
538 // By using queued connections this problem should be solved.
539 connect( ResourceManager::instance()->mainModel(),
540 SIGNAL( statementAdded( const Soprano::Statement& ) ),
541 this,
542 SLOT( slotStatementAdded( const Soprano::Statement& ) ),
543 Qt::QueuedConnection );
544 connect( ResourceManager::instance()->mainModel(),
545 SIGNAL( statementRemoved( const Soprano::Statement& ) ),
546 this,
547 SLOT( slotStatementRemoved( const Soprano::Statement& ) ),
548 Qt::QueuedConnection );
549 }
550
551
552 Nepomuk::TagCloud::~TagCloud()
553 {
554 delete d;
555 }
556
557
558 void Nepomuk::TagCloud::setMaxFontSize( int size )
559 {
560 d->invalidateCachedValues();
561 d->maxFontSize = size;
562 d->updateNodeFonts();
563 d->rebuildCloud();
564 }
565
566
567 void Nepomuk::TagCloud::setMinFontSize( int size )
568 {
569 d->invalidateCachedValues();
570 d->minFontSize = size;
571 d->updateNodeFonts();
572 d->rebuildCloud();
573 }
574
575
576 void Nepomuk::TagCloud::setMaxNumberDisplayedTags( int n )
577 {
578 d->maxNumberDisplayedTags = n;
579 d->rebuildCloud();
580 }
581
582
583 void Nepomuk::TagCloud::setSelectionEnabled( bool enabled )
584 {
585 d->selectionEnabled = enabled;
586 update();
587 }
588
589
590 void Nepomuk::TagCloud::setNewTagButtonEnabled( bool enabled )
591 {
592 d->newTagButtonEnabled = enabled;
593 d->rebuildCloud();
594 }
595
596
597 bool Nepomuk::TagCloud::zoomEnabled() const
598 {
599 return d->zoomEnabled;
600 }
601
602
603 void Nepomuk::TagCloud::setZoomEnabled( bool zoom )
604 {
605 if ( d->zoomEnabled != zoom ) {
606 d->zoomEnabled = zoom;
607 d->rebuildCloud();
608 }
609 }
610
611
612 void Nepomuk::TagCloud::setContextMenuEnabled( bool enabled )
613 {
614 }
615
616
617 void Nepomuk::TagCloud::setAlignment( Qt::Alignment alignment )
618 {
619 d->alignment = alignment;
620 d->rebuildCloud();
621 }
622
623
624 void Nepomuk::TagCloud::setSorting( Sorting s )
625 {
626 d->invalidateCachedValues();
627 d->sorting = s;
628 d->rebuildCloud();
629 }
630
631
632 void Nepomuk::TagCloud::showAllTags()
633 {
634 showTags( Nepomuk::Tag::allTags() );
635 d->showAllTags = true;
636 }
637
638
639 void Nepomuk::TagCloud::showResourceTags( const Resource& resource )
640 {
641 showTags( resource.tags() );
642 d->resource = resource.uri();
643 }
644
645
646 void Nepomuk::TagCloud::showTags( const QList<Tag>& tags )
647 {
648 d->resource = QUrl();
649 d->showAllTags = false;
650 d->invalidateCachedValues();
651 d->nodes.clear();
652 Q_FOREACH( const Tag &tag, tags ) {
653 TagNode node;
654 node.tag = tag;
655 node.weight = d->calculateWeight( tag );
656 node.text = node.tag.genericLabel();
657
658 d->nodes.append( node );
659 }
660 d->updateNodeFonts();
661 d->rebuildCloud();
662 }
663
664
665 void Nepomuk::TagCloud::setTagSelected( const Tag& tag, bool selected )
666 {
667 if ( TagNode* node = d->nodeForTag( tag ) ) {
668 node->selected = selected;
669 if ( d->selectionEnabled ) {
670 update( node->zoomedRect );
671 }
672 }
673 }
674
675
676 QSize Nepomuk::TagCloud::sizeHint() const
677 {
678 // If we have tags d->rebuildCloud() has been called at least once,
679 // thus, we have proper rects (i.e. needed sizes)
680
681 if ( !d->cachedSizeHint.isValid() ) {
682 QList<QList<TagNode*> > rows;
683 rows.append( QList<TagNode*>() );
684 for ( QList<TagNode>::iterator it = d->nodes.begin();
685 it != d->nodes.end(); ++it ) {
686 TagNode& node = *it;
687 rows.first().append( &node );
688 }
689 if ( d->newTagButtonEnabled ) {
690 rows.first().append( &d->newTagNode );
691 }
692
693 QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
694 QSize bestSize( size );
695 while ( size.height() < size.width() ) {
696 // find the longest row
697 int maxRow = 0;
698 int maxLen = 0;
699 for ( int i = 0; i < rows.count(); ++i ) {
700 int rowLen = rowLength( rows[i] );
701 if ( rowLen > maxLen ) {
702 maxLen = rowLen;
703 maxRow = i;
704 }
705 }
706
707 // move the last item from the longest row to the next row
708 TagNode* node = rows[maxRow].takeLast();
709 if ( rows.count() <= maxRow+1 ) {
710 rows.append( QList<TagNode*>() );
711 }
712 rows[maxRow+1].prepend( node );
713
714 // update the size
715 size = cloudSize( rows );
716
717 if ( size.width() < bestSize.width() &&
718 size.width() > size.height() ) {
719 bestSize = size;
720 }
721 }
722
723 d->cachedSizeHint = QSize( bestSize.width() + frameWidth()*2,
724 bestSize.height() + frameWidth()*2 );
725 }
726
727 return d->cachedSizeHint;
728 }
729
730
731 QSize Nepomuk::TagCloud::minimumSizeHint() const
732 {
733 return QFrame::minimumSizeHint();
734 // If we have tags d->rebuildCloud() has been called at least once,
735 // thus, we have proper rects (i.e. needed sizes)
736 if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
737 return QSize( fontMetrics().width( i18nc( "@label Indicator when no tags defined", "No Tags" ) ), fontMetrics().height() );
738 }
739 else {
740 QSize size;
741 for ( QList<TagNode>::iterator it = d->nodes.begin();
742 it != d->nodes.end(); ++it ) {
743 size.setWidth( qMax( size.width(), ( *it ).rect.width() ) );
744 size.setHeight( qMax( size.height(), ( *it ).rect.height() ) );
745 }
746 if ( d->newTagButtonEnabled ) {
747 size.setWidth( qMax( size.width(), d->newTagNode.rect.width() ) );
748 size.setHeight( qMax( size.height(), d->newTagNode.rect.height() ) );
749 }
750 size.setWidth( size.width() + frameWidth()*2 );
751 size.setHeight( size.height() + frameWidth()*2 );
752 return size;
753 }
754 }
755
756
757 int Nepomuk::TagCloud::heightForWidth( int contentsWidth ) const
758 {
759 // If we have tags d->rebuildCloud() has been called at least once,
760 // thus, we have proper rects (i.e. needed sizes)
761
762 if ( d->cachedHfwWidth != contentsWidth ) {
763 // have to keep in mind the frame
764 contentsWidth -= frameWidth()*2;
765
766 QList<TagNode*> allNodes;
767 for ( QList<TagNode>::iterator it = d->nodes.begin();
768 it != d->nodes.end(); ++it ) {
769 TagNode& node = *it;
770 allNodes.append( &node );
771 }
772 if ( d->newTagButtonEnabled ) {
773 allNodes.append( &d->newTagNode );
774 }
775
776 int h = 0;
777 bool newRow = true;
778 int rowW = 0;
779 int rowH = 0;
780 int maxW = 0;
781 for ( int i = 0; i < allNodes.count(); ++i ) {
782 int w = rowW;
783 if ( !newRow ) {
784 w += s_hSpacing;
785 }
786 newRow = false;
787 w += allNodes[i]->rect.width();
788 if ( w <= contentsWidth ) {
789 rowH = qMax( rowH, allNodes[i]->rect.height() );
790 rowW = w;
791 }
792 else {
793 if ( h > 0 ) {
794 h += s_vSpacing;
795 }
796 h += rowH;
797 rowH = allNodes[i]->rect.height();
798 rowW = allNodes[i]->rect.width();
799 }
800 maxW = qMax( maxW, rowW );
801 }
802 if ( rowH > 0 ) {
803 h += s_vSpacing + rowH;
804 }
805
806 d->cachedHfwWidth = contentsWidth;
807 d->cachedHfwHeight = h;
808
809 // zooming
810 if ( maxW > contentsWidth ) {
811 d->cachedHfwHeight = d->cachedHfwHeight * contentsWidth / maxW;
812 }
813 }
814
815 return d->cachedHfwHeight + frameWidth()*2;
816 }
817
818
819 void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e )
820 {
821 QFrame::resizeEvent( e );
822 d->rebuildCloud();
823 update();
824 }
825
826
827 void Nepomuk::TagCloud::paintEvent( QPaintEvent* e )
828 {
829 QFrame::paintEvent( e );
830
831 KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText );
832 KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText );
833 KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText );
834
835 QPainter p( this );
836 QRegion paintRegion = e->region();
837
838 p.save();
839 p.setMatrix( d->zoomMatrix );
840
841 for ( QList<TagNode>::iterator it = d->nodes.begin();
842 it != d->nodes.end(); ++it ) {
843 TagNode& node = *it;
844
845 if ( paintRegion.contains( node.zoomedRect ) ) {
846 p.setFont( node.font );
847
848 if ( &node == d->hoverTag ) {
849 p.setPen( hoverTextBrush.brush( this ).color() );
850 }
851 else if ( d->selectionEnabled && node.selected ) {
852 p.setPen( activeTextBrush.brush( this ).color() );
853 }
854 else {
855 p.setPen( normalTextBrush.brush( this ).color() );
856 }
857 p.drawText( node.rect, Qt::AlignCenter, node.text );
858 }
859 }
860
861 if ( d->newTagButtonEnabled ) {
862 p.setFont( d->newTagNode.font );
863 if ( &d->newTagNode == d->hoverTag ) {
864 p.setPen( hoverTextBrush.brush( this ).color() );
865 }
866 else {
867 p.setPen( normalTextBrush.brush( this ).color() );
868 }
869 p.drawText( d->newTagNode.rect, Qt::AlignCenter, d->customNewTagAction ? d->customNewTagAction->text() : d->newTagNode.text );
870 }
871
872 p.restore();
873 }
874
875
876 void Nepomuk::TagCloud::mousePressEvent( QMouseEvent* e )
877 {
878 if ( e->button() == Qt::LeftButton ) {
879 if ( TagNode* node = d->tagAt( e->pos() ) ) {
880 kDebug() << "clicked" << node->text;
881 if ( node == &d->newTagNode ) {
882 if ( d->customNewTagAction ) {
883 d->customNewTagAction->trigger();
884 }
885 else {
886 // FIXME: nicer gui
887 Tag newTag = NewTagDialog::createTag( this );
888 if ( newTag.isValid() ) {
889 emit tagAdded( newTag );
890 }
891 }
892 }
893 else {
894 emit tagClicked( node->tag );
895 if ( d->selectionEnabled ) {
896 kDebug() << "Toggleing tag" << node->text;
897 node->selected = !node->selected;
898 emit tagToggled( node->tag, node->selected );
899 update( node->zoomedRect );
900 }
901 }
902 }
903 }
904 }
905
906
907 void Nepomuk::TagCloud::mouseMoveEvent( QMouseEvent* e )
908 {
909 if ( e->buttons() == Qt::NoButton ) {
910
911 TagNode* oldHoverTag = d->hoverTag;
912
913 if ( ( d->hoverTag = d->tagAt( e->pos() ) ) &&
914 !d->selectionEnabled ) {
915 setCursor( Qt::PointingHandCursor );
916 }
917 else if ( d->newTagButtonEnabled &&
918 d->newTagNode.zoomedRect.contains( e->pos() ) ) {
919 d->hoverTag = &d->newTagNode;
920 setCursor( Qt::PointingHandCursor );
921 }
922 else {
923 unsetCursor();
924 }
925
926 if ( oldHoverTag || d->hoverTag ) {
927 QRect updateRect;
928 if ( d->hoverTag )
929 updateRect = updateRect.united( d->hoverTag->zoomedRect );
930 if ( oldHoverTag )
931 updateRect = updateRect.united( oldHoverTag->zoomedRect );
932
933 update( updateRect );
934 }
935 }
936 }
937
938
939 void Nepomuk::TagCloud::leaveEvent( QEvent* )
940 {
941 unsetCursor();
942 if ( d->hoverTag ) {
943 QRect updateRect = d->hoverTag->zoomedRect;
944 d->hoverTag = 0;
945 update( updateRect );
946 }
947 }
948
949
950 void Nepomuk::TagCloud::slotStatementAdded( const Soprano::Statement& s )
951 {
952 if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
953 s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
954 // new tag created
955 if ( d->showAllTags ) {
956 showAllTags();
957 }
958 }
959 else if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
960 if ( s.subject().uri() == d->resource ) {
961 showResourceTags( d->resource );
962 }
963 else {
964 // weights might have changed
965 d->updateNodeWeights();
966 d->rebuildCloud();
967 }
968 }
969 }
970
971
972 void Nepomuk::TagCloud::slotStatementRemoved( const Soprano::Statement& s )
973 {
974 // FIXME: In theory might contain empty nodes as wildcards
975
976 if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
977 if ( d->resource.isValid() &&
978 d->resource == s.subject().uri() ) {
979 showResourceTags( d->resource );
980 }
981 else {
982 // weights might have changed
983 d->updateNodeWeights();
984 d->rebuildCloud();
985 }
986 }
987 else if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
988 s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
989 // tag deleted
990 if ( d->showAllTags ) {
991 showAllTags();
992 }
993 }
994 }
995
996
997 void Nepomuk::TagCloud::setCustomNewTagAction( QAction* action )
998 {
999 d->customNewTagAction = action;
1000 setNewTagButtonEnabled( action != 0 );
1001 }
1002
1003 #include "tagcloud.moc"