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

Last change on this file since 13810 was 13810, checked in by jordan, 8 years ago

(trunk) #1220 'change top folder names' -- add file-renaming to the Qt client

  • Property svn:keywords set to Date Rev Author Id
File size: 20.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 13810 2013-01-20 01:31:58Z 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_NAME )
322        i |= Qt::ItemIsEditable;
323
324    if( index.column( ) == COL_WANTED )
325        i |= Qt::ItemIsUserCheckable | Qt::ItemIsTristate;
326
327    return (Qt::ItemFlags)i;
328}
329
330bool
331FileTreeModel :: setData (const QModelIndex& index, const QVariant& newname, int role)
332{
333  if (role == Qt::EditRole)
334    {
335      QString oldpath;
336      QModelIndex walk = index;
337      FileTreeItem * item = static_cast<FileTreeItem*>(index.internalPointer());
338
339      while (item && !item->name().isEmpty())
340        {
341          if (oldpath.isEmpty())
342            oldpath = item->name();
343          else
344            oldpath = item->name() + "/" + oldpath;
345          item = item->parent ();
346        }
347
348      emit pathEdited (oldpath, newname.toString());
349    }
350
351  return false; // don't update the view until the session confirms the change
352}
353
354QVariant
355FileTreeModel :: headerData( int column, Qt::Orientation orientation, int role ) const
356{
357    QVariant data;
358
359    if( orientation==Qt::Horizontal && role==Qt::DisplayRole ) {
360        switch( column ) {
361            case COL_NAME:     data.setValue( tr( "File" ) ); break;
362            case COL_PROGRESS: data.setValue( tr( "Progress" ) ); break;
363            case COL_WANTED:   data.setValue( tr( "Download" ) ); break;
364            case COL_PRIORITY: data.setValue( tr( "Priority" ) ); break;
365            default: break;
366        }
367    }
368
369    return data;
370}
371
372QModelIndex
373FileTreeModel :: index( int row, int column, const QModelIndex& parent ) const
374{
375    QModelIndex i;
376
377    if( !hasIndex( row, column, parent ) )
378    {
379        std::cerr << " I don't have this index " << std::endl;
380    }
381    else
382    {
383        FileTreeItem * parentItem;
384
385        if( !parent.isValid( ) )
386            parentItem = rootItem;
387        else
388            parentItem = static_cast<FileTreeItem*>(parent.internalPointer());
389
390        FileTreeItem * childItem = parentItem->child( row );
391
392        if( childItem )
393            i = createIndex( row, column, childItem );
394
395//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;
396    }
397
398    return i;
399}
400
401QModelIndex
402FileTreeModel :: parent( const QModelIndex& child ) const
403{
404    return parent( child, 0 ); // QAbstractItemModel::parent() wants col 0
405}
406
407QModelIndex
408FileTreeModel :: parent( const QModelIndex& child, int column ) const
409{
410    if( !child.isValid( ) )
411        return QModelIndex( );
412
413    FileTreeItem * childItem = static_cast<FileTreeItem*>(child.internalPointer());
414
415    return indexOf( childItem->parent( ), column );
416}
417
418int
419FileTreeModel :: rowCount( const QModelIndex& parent ) const
420{
421    FileTreeItem * parentItem;
422
423    if( !parent.isValid( ) )
424        parentItem = rootItem;
425    else
426        parentItem = static_cast<FileTreeItem*>(parent.internalPointer());
427
428    return parentItem->childCount();
429}
430
431int
432FileTreeModel :: columnCount( const QModelIndex &parent ) const
433{
434    Q_UNUSED( parent );
435
436    return 4;
437}
438
439QModelIndex
440FileTreeModel :: indexOf( FileTreeItem * item, int column ) const
441{
442    if( !item || item==rootItem )
443        return QModelIndex( );
444
445    return createIndex( item->row( ), column, item );
446}
447
448void
449FileTreeModel :: clearSubtree( const QModelIndex& top )
450{
451    size_t i = rowCount( top );
452
453    while( i > 0 )
454        clearSubtree( index( --i, 0, top ) );
455
456    delete static_cast<FileTreeItem*>(top.internalPointer());
457}
458
459void
460FileTreeModel :: clear( )
461{
462    clearSubtree( QModelIndex( ) );
463
464    reset( );
465}
466
467void
468FileTreeModel :: addFile( int                   index,
469                          const QString       & filename,
470                          bool                  wanted,
471                          int                   priority,
472                          uint64_t              size,
473                          uint64_t              have,
474                          QList<QModelIndex>  & rowsAdded,
475                          bool                  torrentChanged )
476{
477    FileTreeItem * i( rootItem );
478
479    foreach( QString token, filename.split( QChar::fromAscii('/') ) )
480    {
481        FileTreeItem * child( i->child( token ) );
482        if( !child )
483        {
484            QModelIndex parentIndex( indexOf( i, 0 ) );
485            const int n( i->childCount( ) );
486            beginInsertRows( parentIndex, n, n );
487            i->appendChild(( child = new FileTreeItem( -1, token )));
488            endInsertRows( );
489            rowsAdded.append( indexOf( child, 0 ) );
490        }
491        i = child;
492    }
493
494    if( i != rootItem )
495        if( i->update( index, wanted, priority, size, have, torrentChanged ) )
496            dataChanged( indexOf( i, 0 ), indexOf( i, NUM_COLUMNS-1 ) );
497}
498
499void
500FileTreeModel :: parentsChanged( const QModelIndex& index, int column )
501{
502    QModelIndex walk = index;
503
504    for( ;; ) {
505        walk = parent( walk, column );
506        if( !walk.isValid( ) )
507            break;
508        dataChanged( walk, walk );
509    }
510}
511
512void
513FileTreeModel :: subtreeChanged( const QModelIndex& index, int column )
514{
515    const int childCount = rowCount( index );
516    if( !childCount )
517        return;
518
519    // tell everyone that this tier changed
520    dataChanged( index.child(0,column), index.child(childCount-1,column) );
521
522    // walk the subtiers
523    for( int i=0; i<childCount; ++i )
524        subtreeChanged( index.child(i,column), column );
525}
526
527void
528FileTreeModel :: clicked( const QModelIndex& index )
529{
530    const int column( index.column( ) );
531
532    if( !index.isValid( ) )
533        return;
534
535    if( column == COL_WANTED )
536    {
537        FileTreeItem * item( static_cast<FileTreeItem*>(index.internalPointer()));
538        bool want;
539        QSet<int> fileIds;
540        item->twiddleWanted( fileIds, want );
541        emit wantedChanged( fileIds, want );
542
543        dataChanged( index, index );
544        parentsChanged( index, column );
545        subtreeChanged( index, column );
546    }
547    else if( column == COL_PRIORITY )
548    {
549        FileTreeItem * item( static_cast<FileTreeItem*>(index.internalPointer()));
550        int priority;
551        QSet<int>fileIds;
552        item->twiddlePriority( fileIds, priority );
553        emit priorityChanged( fileIds, priority );
554
555        dataChanged( index, index );
556        parentsChanged( index, column );
557        subtreeChanged( index, column );
558    }
559}
560
561/****
562*****
563****/
564
565QSize
566FileTreeDelegate :: sizeHint( const QStyleOptionViewItem& item, const QModelIndex& index ) const
567{
568    QSize size;
569
570    switch( index.column( ) )
571    {
572        case COL_NAME: {
573            const QFontMetrics fm( item.font );
574            const QString text = index.data().toString();
575            const int iconSize = QApplication::style()->pixelMetric( QStyle::PM_SmallIconSize );
576            size.rwidth() = HIG::PAD_SMALL + iconSize;
577            size.rheight() = std::max( iconSize, fm.height( ) );
578            break;
579        }
580
581        case COL_PROGRESS:
582        case COL_WANTED:
583            size = QSize( 20, 1 );
584            break;
585
586        default: {
587            const QFontMetrics fm( item.font );
588            const QString text = index.data().toString();
589            size = fm.size( 0, text );
590            break;
591        }
592    }
593
594    size.rheight() += 8; // make the spacing a little nicer
595    return size;
596}
597
598void
599FileTreeDelegate :: paint( QPainter                    * painter,
600                           const QStyleOptionViewItem  & option,
601                           const QModelIndex           & index ) const
602{
603    const int column( index.column( ) );
604
605    if( ( column != COL_PROGRESS ) && ( column != COL_WANTED ) && ( column != COL_NAME ) )
606    {
607        QItemDelegate::paint(painter, option, index);
608        return;
609    }
610
611    QStyle * style( QApplication :: style( ) );
612    if( option.state & QStyle::State_Selected )
613        painter->fillRect( option.rect, option.palette.highlight( ) );
614    painter->save();
615    if( option.state & QStyle::State_Selected )
616         painter->setBrush(option.palette.highlightedText());
617
618    if( column == COL_NAME )
619    {
620        // draw the file icon
621        static const int iconSize( style->pixelMetric( QStyle :: PM_SmallIconSize ) );
622        const QRect iconArea( option.rect.x(),
623                              option.rect.y() + (option.rect.height()-iconSize)/2,
624                              iconSize, iconSize );
625        QIcon icon;
626        if( index.model()->hasChildren( index ) )
627            icon = style->standardIcon( QStyle::StandardPixmap( QStyle::SP_DirOpenIcon ) );
628        else
629        {
630            QString name = index.data().toString();
631            icon = Utils :: guessMimeIcon( name.left( name.lastIndexOf( " (" ) ) );
632        }
633        icon.paint( painter, iconArea, Qt::AlignCenter, QIcon::Normal, QIcon::On );
634
635        // draw the name
636        QStyleOptionViewItem tmp( option );
637        tmp.rect.setWidth( option.rect.width( ) - iconArea.width( ) - HIG::PAD_SMALL );
638        tmp.rect.moveRight( option.rect.right( ) );
639        QItemDelegate::paint( painter, tmp, index );
640    }
641    else if( column == COL_PROGRESS )
642    {
643        QStyleOptionProgressBar p;
644        p.state = option.state | QStyle::State_Small;
645        p.direction = QApplication::layoutDirection();
646        p.rect = option.rect;
647        p.rect.setSize( QSize( option.rect.width()-2, option.rect.height()-8 ) );
648        p.rect.moveCenter( option.rect.center( ) );
649        p.fontMetrics = QApplication::fontMetrics();
650        p.minimum = 0;
651        p.maximum = 100;
652        p.textAlignment = Qt::AlignCenter;
653        p.textVisible = true;
654        p.progress = (int)(100.0*index.data().toDouble());
655        p.text = QString( ).sprintf( "%d%%", p.progress );
656        style->drawControl( QStyle::CE_ProgressBar, &p, painter );
657    }
658    else if( column == COL_WANTED )
659    {
660        QStyleOptionButton o;
661        o.state = option.state;
662        o.direction = QApplication::layoutDirection();
663        o.rect.setSize( QSize( 20, option.rect.height( ) ) );
664        o.rect.moveCenter( option.rect.center( ) );
665        o.fontMetrics = QApplication::fontMetrics();
666        switch( index.data().toInt() ) {
667            case Qt::Unchecked: o.state |= QStyle::State_Off; break;
668            case Qt::Checked:   o.state |= QStyle::State_On; break;
669            default:            o.state |= QStyle::State_NoChange;break;
670        }
671        style->drawControl( QStyle::CE_CheckBox, &o, painter );
672    }
673
674    painter->restore( );
675}
676
677/****
678*****
679*****
680*****
681****/
682
683FileTreeView :: FileTreeView( QWidget * parent ):
684    QTreeView( parent ),
685    myModel( this ),
686    myProxy( new QSortFilterProxyModel( ) ),
687    myDelegate( this )
688{
689    setSortingEnabled( true );
690    setAlternatingRowColors( true );
691    setSelectionBehavior( QAbstractItemView::SelectRows );
692    setSelectionMode( QAbstractItemView::ExtendedSelection );
693    myProxy->setSourceModel( &myModel );
694    setModel( myProxy );
695    setItemDelegate( &myDelegate );
696    setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
697    sortByColumn( COL_NAME, Qt::AscendingOrder );
698    installEventFilter( this );
699
700    for( int i=0; i<NUM_COLUMNS; ++i )
701        header()->setResizeMode( i, QHeaderView::Interactive );
702
703    connect( this, SIGNAL(clicked(const QModelIndex&)),
704             this, SLOT(onClicked(const QModelIndex&)) );
705
706    connect( &myModel, SIGNAL(priorityChanged(const QSet<int>&, int)),
707             this,     SIGNAL(priorityChanged(const QSet<int>&, int)));
708
709    connect( &myModel, SIGNAL(wantedChanged(const QSet<int>&, bool)),
710             this,     SIGNAL(wantedChanged(const QSet<int>&, bool)));
711
712    connect( &myModel, SIGNAL(pathEdited(const QString&, const QString&)),
713             this,     SIGNAL(pathEdited(const QString&, const QString&)));
714}
715
716FileTreeView :: ~FileTreeView( )
717{
718    myProxy->deleteLater();
719}
720
721void
722FileTreeView :: onClicked( const QModelIndex& proxyIndex )
723{
724    const QModelIndex modelIndex = myProxy->mapToSource( proxyIndex );
725    myModel.clicked( modelIndex );
726}
727
728bool
729FileTreeView :: eventFilter( QObject * o, QEvent * event )
730{
731    if( o != this )
732        return false;
733
734    // this is kind of a hack to get the last three columns be the
735    // right size, and to have the filename column use whatever
736    // space is left over...
737    if( event->type() == QEvent::Resize )
738    {
739        QResizeEvent * r = dynamic_cast<QResizeEvent*>(event);
740        int left = r->size().width();
741        const QFontMetrics fontMetrics( font( ) );
742        for( int column=0; column<NUM_COLUMNS; ++column ) {
743            if( column == COL_NAME )
744                continue;
745            if( isColumnHidden( column ) )
746                continue;
747            const QString header = myModel.headerData( column, Qt::Horizontal ).toString( ) + "    ";
748            const int width = fontMetrics.size( 0, header ).width( );
749            setColumnWidth( column, width );
750            left -= width;
751        }
752        left -= 20; // not sure why this is necessary.  it works in different themes + font sizes though...
753        setColumnWidth( COL_NAME, std::max(left,0) );
754        return false;
755    }
756
757    // handle using the keyboard to toggle the
758    // wanted/unwanted state or the file priority
759    else if( event->type() == QEvent::KeyPress )
760    {
761        switch( dynamic_cast<QKeyEvent*>(event)->key() )
762        {
763            case Qt::Key_Space:
764                foreach( QModelIndex i, selectionModel()->selectedRows(COL_WANTED) )
765                    clicked( i );
766                return false;
767
768            case Qt::Key_Enter:
769            case Qt::Key_Return:
770                foreach( QModelIndex i, selectionModel()->selectedRows(COL_PRIORITY) )
771                    clicked( i );
772                return false;
773        }
774    }
775
776    return false;
777}
778
779void
780FileTreeView :: update( const FileList& files )
781{
782    update( files, true );
783}
784
785void
786FileTreeView :: update( const FileList& files, bool torrentChanged )
787{
788    foreach( const TrFile file, files ) {
789        QList<QModelIndex> added;
790        myModel.addFile( file.index, file.filename, file.wanted, file.priority, file.size, file.have, added, torrentChanged );
791        foreach( QModelIndex i, added )
792            expand( myProxy->mapFromSource( i ) );
793    }
794}
795
796void
797FileTreeView :: clear( )
798{
799    myModel.clear( );
800}
Note: See TracBrowser for help on using the repository browser.