source: trunk/qt/file-tree.cc @ 8188

Last change on this file since 8188 was 8188, checked in by charles, 13 years ago

(trunk) add the Qt beta into svn

File size: 16.5 KB
Line 
1/*
2 * This file Copyright (C) 2009 Charles Kerr <charles@transmissionbt.com>
3 *
4 * This file is licensed by the GPL version 2.  Works owned by the
5 * Transmission project are granted a special exemption to clause 2(b)
6 * so that the bulk of its code can remain under the MIT license.
7 * This exemption does not extend to derived works not owned by
8 * the Transmission project.
9 *
10 * $Id:$
11 */
12
13#include <cassert>
14#include <iostream>
15
16#include <QApplication>
17#include <QPainter>
18#include <QResizeEvent>
19#include <QHeaderView>
20#include <QStringList>
21
22#include <libtransmission/transmission.h> // priorities
23
24#include "file-tree.h"
25#include "hig.h"
26#include "torrent.h" // FileList
27#include "utils.h" // mime icons
28
29enum
30{
31    COL_NAME,
32    COL_PROGRESS,
33    COL_WANTED,
34    COL_PRIORITY,
35    NUM_COLUMNS
36};
37
38/****
39*****
40****/
41
42FileTreeItem :: ~FileTreeItem( )
43{
44    assert( myChildren.isEmpty( ) );
45
46    if( myParent )
47        if( !myParent->myChildren.removeOne( this ) )
48            assert( 0 && "failed to remove" );
49}
50
51void
52FileTreeItem :: appendChild( FileTreeItem * child )
53{
54    child->myParent = this;
55    myChildren.append( child );
56}
57
58FileTreeItem *
59FileTreeItem :: child( const QString& filename )
60{
61    foreach( FileTreeItem * c, myChildren )
62        if( c->name() == filename )
63            return c;
64
65    return 0;
66}
67
68int
69FileTreeItem :: row( ) const
70{
71    int i(0);
72
73    if( myParent )
74        i = myParent->myChildren.indexOf( const_cast<FileTreeItem*>(this) );
75
76    return i;
77}
78
79QVariant
80FileTreeItem :: data( int column ) const
81{
82    QVariant value;
83
84    switch( column ) {
85        case COL_NAME: value.setValue( name() ); break;
86        case COL_PROGRESS: value.setValue( progress( ) ); break;
87        case COL_WANTED: value.setValue( isSubtreeWanted( ) ); break;
88        case COL_PRIORITY: value.setValue( priorityString( ) ); break;
89    }
90
91    return value;
92}
93
94void
95FileTreeItem :: getSubtreeSize( uint64_t& have, uint64_t& total ) const
96{
97    have += myHaveSize;
98    total += myTotalSize;
99
100    foreach( const FileTreeItem * i, myChildren )
101        i->getSubtreeSize( have, total );
102}
103
104double
105FileTreeItem :: progress( ) const
106{
107    double d(0);
108    uint64_t have(0), total(0);
109    getSubtreeSize( have, total );
110    if( total )
111        d = have / (double)total;
112    return d;
113}
114
115bool
116FileTreeItem :: update( int index, bool wanted, int priority, uint64_t totalSize, uint64_t haveSize )
117{
118    bool changed = false;
119
120    if( myIndex != index )
121    {
122        myIndex = index;
123        changed = true;
124    }
125    if( myIsWanted != wanted )
126    {
127        myIsWanted = wanted;
128        changed = true;
129    }
130    if( myPriority != priority )
131    {
132        myPriority = priority;
133        changed = true;
134    }
135    if( myTotalSize != totalSize )
136    {
137        myTotalSize = totalSize;
138        changed = true;
139    }
140    if( myHaveSize != haveSize )
141    {
142        myHaveSize = haveSize;
143        changed = true;
144    }
145
146    return changed;
147}
148
149QString
150FileTreeItem :: priorityString( ) const
151{
152    const int i( priority( ) );
153    if( i == LOW ) return tr( "Low" );
154    if( i == HIGH ) return tr( "High" );
155    if( i == NORMAL ) return tr( "Normal" );
156    return tr( "Mixed" );
157}
158
159int
160FileTreeItem :: priority( ) const
161{
162    int i( 0 );
163
164    if( myChildren.isEmpty( ) ) switch( myPriority ) {
165        case TR_PRI_LOW:  i |= LOW; break;
166        case TR_PRI_HIGH: i |= HIGH; break;
167        default:          i |= NORMAL; break;
168    }
169
170    foreach( const FileTreeItem * child, myChildren )
171        i |= child->priority( );
172
173    return i;
174}
175
176void
177FileTreeItem :: setSubtreePriority( int i, QSet<int>& ids )
178{
179    if( myPriority != i ) {
180        myPriority = i;
181        if( myIndex >= 0 )
182            ids.insert( myIndex );
183    }
184
185    foreach( FileTreeItem * child, myChildren )
186        child->setSubtreePriority( i, ids );
187}
188
189void
190FileTreeItem :: twiddlePriority( QSet<int>& ids, int& p )
191{
192    const int old( priority( ) );
193
194    if     ( old & LOW )    p = TR_PRI_NORMAL;
195    else if( old & NORMAL ) p = TR_PRI_HIGH;
196    else                    p = TR_PRI_LOW;
197
198    setSubtreePriority( p, ids );
199}
200
201int
202FileTreeItem :: isSubtreeWanted( ) const
203{
204    if( myChildren.isEmpty( ) )
205        return myIsWanted ? Qt::Checked : Qt::Unchecked;
206
207    int wanted( -1 );
208    foreach( const FileTreeItem * child, myChildren ) {
209        const int childWanted = child->isSubtreeWanted( );
210        if( wanted == -1 )
211            wanted = childWanted;
212        if( wanted != childWanted )
213            wanted = Qt::PartiallyChecked;
214        if( wanted == Qt::PartiallyChecked )
215            return wanted;
216    }
217
218    return wanted;
219}
220
221void
222FileTreeItem :: setSubtreeWanted( bool b, QSet<int>& ids )
223{
224    if( myIsWanted != b ) {
225        myIsWanted = b;
226        if( myIndex >= 0 )
227            ids.insert( myIndex );
228    }
229
230    foreach( FileTreeItem * child, myChildren )
231        child->setSubtreeWanted( b, ids );
232}
233
234void
235FileTreeItem :: twiddleWanted( QSet<int>& ids, bool& wanted )
236{
237    wanted = isSubtreeWanted( ) != Qt::Checked;
238    setSubtreeWanted( wanted, ids );
239}
240
241/***
242****
243****
244***/
245
246FileTreeModel :: FileTreeModel( QObject *parent ):
247    QAbstractItemModel(parent)
248{
249    rootItem = new FileTreeItem( -1 );
250}
251     
252FileTreeModel :: ~FileTreeModel( )
253{
254    clear( );
255
256    delete rootItem;
257}
258
259QVariant
260FileTreeModel :: data( const QModelIndex &index, int role ) const
261{
262    QVariant value;
263
264    if( index.isValid() && role==Qt::DisplayRole )
265    {
266        FileTreeItem *item = static_cast<FileTreeItem*>(index.internalPointer());
267        value = item->data( index.column( ) );
268    }
269
270    return value;
271}
272
273Qt::ItemFlags
274FileTreeModel :: flags( const QModelIndex& index ) const
275{
276    int i( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
277
278    if( index.column( ) == COL_WANTED )
279        i |= Qt::ItemIsUserCheckable | Qt::ItemIsTristate;
280
281    return (Qt::ItemFlags)i;
282}
283
284QVariant
285FileTreeModel :: headerData( int column, Qt::Orientation orientation, int role ) const
286{
287    QVariant data;
288
289    if( orientation==Qt::Horizontal && role==Qt::DisplayRole ) {
290        switch( column ) {
291            case COL_NAME:     data.setValue( tr( "File" ) ); break;
292            case COL_PROGRESS: data.setValue( tr( "Progress" ) ); break;
293            case COL_WANTED:   data.setValue( tr( "Download" ) ); break;
294            case COL_PRIORITY: data.setValue( tr( "Priority" ) ); break;
295            default: break;
296        }
297    }
298               
299    return data;
300}
301
302QModelIndex
303FileTreeModel :: index( int row, int column, const QModelIndex& parent ) const
304{
305    QModelIndex i;
306
307    if( !hasIndex( row, column, parent ) )
308    {
309        std::cerr << " I don't have this index " << std::endl;
310    }
311    else
312    {
313        FileTreeItem * parentItem;
314
315        if( !parent.isValid( ) )
316            parentItem = rootItem;
317        else
318            parentItem = static_cast<FileTreeItem*>(parent.internalPointer());
319
320        FileTreeItem * childItem = parentItem->child( row );
321
322        if( childItem )
323            i = createIndex( row, column, childItem );
324
325//std::cerr << "FileTreeModel::index(row("<<row<<"),col("<<column<<"),parent("<<qPrintable(parentItem->name())<<")) is returning " << qPrintable(childItem->name()) << ": internalPointer " << i.internalPointer() << " row " << i.row() << " col " << i.column() << std::endl;
326    }
327
328    return i;
329}
330
331QModelIndex
332FileTreeModel :: parent( const QModelIndex& child ) const
333{
334    return parent( child, 0 ); // QAbstractItemModel::parent() wants col 0
335}
336
337QModelIndex
338FileTreeModel :: parent( const QModelIndex& child, int column ) const
339{
340    if( !child.isValid( ) )
341        return QModelIndex( );
342
343    FileTreeItem * childItem = static_cast<FileTreeItem*>(child.internalPointer());
344
345    return indexOf( childItem->parent( ), column );
346}
347
348int
349FileTreeModel :: rowCount( const QModelIndex& parent ) const
350{
351    FileTreeItem * parentItem;
352
353    if( !parent.isValid( ) )
354        parentItem = rootItem;
355    else
356        parentItem = static_cast<FileTreeItem*>(parent.internalPointer());
357
358    return parentItem->childCount();
359}
360
361int
362FileTreeModel :: columnCount( const QModelIndex &parent ) const
363{
364    Q_UNUSED( parent );
365
366    return 4;
367}
368
369QModelIndex
370FileTreeModel :: indexOf( FileTreeItem * item, int column ) const
371{
372    if( !item || item==rootItem )
373        return QModelIndex( );
374
375    return createIndex( item->row( ), column, item );
376}
377
378void
379FileTreeModel :: clearSubtree( const QModelIndex& top )
380{
381    while( hasChildren( top ) )
382        clearSubtree( index( 0, 0, top ) );
383
384    delete static_cast<FileTreeItem*>(top.internalPointer());
385}
386
387void
388FileTreeModel :: clear( )
389{
390    clearSubtree( QModelIndex( ) );
391
392    reset( );
393}
394
395void
396FileTreeModel :: addFile( int                   index,
397                          const QString       & filename,
398                          bool                  wanted,
399                          int                   priority,
400                          uint64_t              size,
401                          uint64_t              have,
402                          QList<QModelIndex>  & rowsAdded )
403{
404    FileTreeItem * i( rootItem );
405
406    foreach( QString token, filename.split( "/" ) )
407    {
408        FileTreeItem * child( i->child( token ) );
409        if( !child )
410        {
411            QModelIndex parentIndex( indexOf( i, 0 ) );
412            const int n( i->childCount( ) );
413            beginInsertRows( parentIndex, n, n );
414            i->appendChild(( child = new FileTreeItem( -1, token )));
415            endInsertRows( );
416            rowsAdded.append( indexOf( child, 0 ) );
417        }
418        i = child;
419    }
420
421    if( i != rootItem )
422        if( i->update( index, wanted, priority, size, have ) )
423            dataChanged( indexOf( i, 0 ), indexOf( i, NUM_COLUMNS-1 ) );
424}
425
426void
427FileTreeModel :: parentsChanged( const QModelIndex& index, int column )
428{
429    QModelIndex walk = index;
430
431    for( ;; ) {
432        walk = parent( walk, column );
433        if( !walk.isValid( ) )
434            break;
435        dataChanged( walk, walk );
436    }
437}
438
439void
440FileTreeModel :: subtreeChanged( const QModelIndex& index, int column )
441{
442    const int childCount = rowCount( index );
443    if( !childCount )
444        return;
445
446    // tell everyone that this tier changed
447    dataChanged( index.child(0,column), index.child(childCount-1,column) );
448
449    // walk the subtiers
450    for( int i=0; i<childCount; ++i )
451        subtreeChanged( index.child(i,column), column );
452}
453
454void
455FileTreeModel :: clicked( const QModelIndex& index )
456{
457    const int column( index.column( ) );
458
459    if( !index.isValid( ) )
460        return;
461
462    if( column == COL_WANTED )
463    {
464        FileTreeItem * item( static_cast<FileTreeItem*>(index.internalPointer()));
465        bool want;
466        QSet<int> fileIds;
467        item->twiddleWanted( fileIds, want );
468        emit wantedChanged( fileIds, want );
469
470        dataChanged( index, index );
471        parentsChanged( index, column );
472        subtreeChanged( index, column );
473    }
474    else if( column == COL_PRIORITY )
475    {
476        FileTreeItem * item( static_cast<FileTreeItem*>(index.internalPointer()));
477        int priority;
478        QSet<int>fileIds;
479        item->twiddlePriority( fileIds, priority );
480        emit priorityChanged( fileIds, priority );
481
482        dataChanged( index, index );
483        parentsChanged( index, column );
484        subtreeChanged( index, column );
485    }
486}
487
488/****
489*****
490****/
491
492void
493FileTreeDelegate :: paint( QPainter                    * painter,
494                           const QStyleOptionViewItem  & option,
495                           const QModelIndex           & index ) const
496{
497    const int column( index.column( ) );
498
499
500    if( ( column != COL_PROGRESS ) && ( column != COL_WANTED ) && ( column != COL_NAME ) )
501    {
502        QItemDelegate::paint(painter, option, index);
503        return;
504    }
505
506    QStyle * style( QApplication :: style( ) );
507    if( option.state & QStyle::State_Selected )
508        painter->fillRect( option.rect, option.palette.highlight( ) );
509    painter->save();
510    if( option.state & QStyle::State_Selected )
511         painter->setBrush(option.palette.highlightedText());
512
513    if( column == COL_NAME )
514    {
515        // draw the file icon
516        static const int iconSize( style->pixelMetric( QStyle :: PM_SmallIconSize ) );
517        const QRect iconArea( option.rect.x(),
518                              option.rect.y() + (option.rect.height()-iconSize)/2,
519                              iconSize, iconSize );
520        QIcon icon;
521        if( index.model()->hasChildren( index ) )
522            icon = style->standardIcon( QStyle::StandardPixmap( QStyle::SP_DirOpenIcon ) );
523        else
524            icon = Utils :: guessMimeIcon( index.model()->data(index).toString( ) );
525        icon.paint( painter, iconArea, Qt::AlignCenter, QIcon::Normal, QIcon::On );
526
527        // draw the name
528        QStyleOptionViewItem tmp( option );
529        tmp.rect.setWidth( option.rect.width( ) - iconArea.width( ) - HIG::PAD_SMALL );
530        tmp.rect.moveRight( option.rect.right( ) );
531        QItemDelegate::paint( painter, tmp, index );
532    }
533    else if( column == COL_PROGRESS )
534    {
535        QStyleOptionProgressBar p;
536        p.state = QStyle::State_Enabled;
537        p.direction = QApplication::layoutDirection();
538        p.rect = option.rect;
539        p.rect.setSize( QSize( option.rect.width()-2, option.rect.height()-2 ) );
540        p.rect.moveCenter( option.rect.center( ) );
541        p.fontMetrics = QApplication::fontMetrics();
542        p.minimum = 0;
543        p.maximum = 100;
544        p.textAlignment = Qt::AlignCenter;
545        p.textVisible = true;
546        p.progress = (int)(100.0*index.model()->data(index).toDouble());
547        p.text = QString( ).sprintf( "%d%%", p.progress );
548        style->drawControl( QStyle::CE_ProgressBar, &p, painter );
549    }
550    else if( column == COL_WANTED )
551    {
552        QStyleOptionButton o;
553        o.state = QStyle::State_Enabled;
554        o.direction = QApplication::layoutDirection();
555        o.rect.setSize( QSize( 20, option.rect.height( ) ) );
556        o.rect.moveCenter( option.rect.center( ) );
557        o.fontMetrics = QApplication::fontMetrics();
558        switch( index.model()->data(index).toInt() ) {
559            case Qt::Unchecked: o.state |= QStyle::State_Off; break;
560            case Qt::Checked:   o.state |= QStyle::State_On; break;
561            default:            o.state |= QStyle::State_NoChange;break;
562        }
563        style->drawControl( QStyle::CE_CheckBox, &o, painter );
564    }
565
566    painter->restore( );
567}
568
569/****
570*****
571*****
572*****
573****/
574
575FileTreeView :: FileTreeView( QWidget * parent ):
576    QTreeView( parent ),
577    myModel( this ),
578    myDelegate( this )
579{
580    setAlternatingRowColors( true );
581    setSelectionBehavior( QAbstractItemView::SelectRows );
582    setSelectionMode( QAbstractItemView::ExtendedSelection );
583    setModel( &myModel );
584    setItemDelegate( &myDelegate );
585    setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
586    installEventFilter( this );
587
588    for( int i=0; i<=NUM_COLUMNS; ++i )
589        header()->setResizeMode( i, QHeaderView::Fixed );
590
591    connect( this,     SIGNAL(clicked(const QModelIndex&)),
592             &myModel,   SLOT(clicked(const QModelIndex&)));
593
594    connect( &myModel, SIGNAL(priorityChanged(const QSet<int>&, int)),
595             this,     SIGNAL(priorityChanged(const QSet<int>&, int)));
596
597    connect( &myModel, SIGNAL(wantedChanged(const QSet<int>&, bool)),
598             this,     SIGNAL(wantedChanged(const QSet<int>&, bool)));
599}
600
601bool 
602FileTreeView :: eventFilter( QObject * o, QEvent * event )
603{
604    if( o != this )
605        return false;
606
607    // this is kind of a hack to get the last three columns be the
608    // right size, and to have the filename column use whatever
609    // space is left over...
610    if( event->type() == QEvent::Resize )
611    {
612        QResizeEvent * r = dynamic_cast<QResizeEvent*>(event);
613        int left = r->size().width();
614        const QFontMetrics fontMetrics( font( ) );
615        for( int column=0; column<NUM_COLUMNS; ++column ) {
616            if( column == COL_NAME )
617                continue;
618            if( isColumnHidden( column ) )
619                continue;
620            const QString header = myModel.headerData( column, Qt::Horizontal ).toString( ) + "    ";
621            const int width = fontMetrics.size( 0, header ).width( );
622            setColumnWidth( column, width );
623            left -= width;
624        }
625        left -= 20; // not sure why this is necessary.  it works in different themes + font sizes though...
626        setColumnWidth( COL_NAME, std::max(left,0) );
627        return false;
628    }
629
630    return false;
631}
632
633void
634FileTreeView :: update( const FileList& files )
635{
636    foreach( const TrFile file, files ) {
637        QList<QModelIndex> added;
638        myModel.addFile( file.index, file.filename, file.wanted, file.priority, file.size, file.have, added );
639        foreach( QModelIndex i, added )
640            expand( i );
641    }
642}
643
644void
645FileTreeView :: clear( )
646{
647    myModel.clear( );
648}
Note: See TracBrowser for help on using the repository browser.