source: branches/2.0x/qt/file-tree.cc @ 11476

Last change on this file since 11476 was 11476, checked in by charles, 11 years ago

(2.0x qt) backport r11306 for #3627 "off-by-one in FileTreeView? causes potential invalid memory read / crash"

  • 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 11476 2010-12-05 18:53:04Z charles $
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.