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

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

(trunk qt) #4980 "Resize headers in Files Tab in Torrent Properties Window" -- patch from samkpo

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