~hendrik-grewe/transmission/private-patch

« back to all changes in this revision

Viewing changes to qt/file-tree.cc

  • Committer: charles
  • Date: 2009-04-09 17:55:47 UTC
  • Revision ID: svn-v4:f4695dd4-2c0a-0410-b89c-da849a56a58e:trunk:8188
(trunk) add the Qt beta into svn 

Show diffs side-by-side

added added

removed removed

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