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

Last change on this file since 10684 was 10684, checked in by Longinus00, 13 years ago

(qt) Fix regression in file icons caused by printing filesizes in the file tree

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