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

Last change on this file since 13383 was 13383, checked in by jordan, 9 years ago

(trunk, qt) #4961 -- make the file list more responsive when a torrent has an extreme number of files.

Before this patch, the test torrent I had with ~10k files took 8 seconds to load. After this patch, it takes less than 1 second.

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