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

Last change on this file since 11065 was 11065, checked in by charles, 12 years ago

(trunk qt) fix a couple of small memory leaks detected by valgrind

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