2 * This file Copyright (C) Mnemosyne LLC
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.
8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10 * $Id: file-tree.cc 11405 2010-11-13 22:07:19Z charles $
16 #include <QApplication>
17 #include <QHeaderView>
19 #include <QResizeEvent>
20 #include <QSortFilterProxyModel>
21 #include <QStringList>
23 #include <libtransmission/transmission.h> // priorities
25 #include "file-tree.h"
26 #include "formatter.h"
28 #include "torrent.h" // FileList
29 #include "utils.h" // mime icons
44 FileTreeItem :: ~FileTreeItem( )
46 assert( myChildren
.isEmpty( ) );
49 const int pos
= myParent
->myChildren
.indexOf( this );
51 myParent
->myChildren
.removeAt( pos
);
53 assert( 0 && "failed to remove" );
58 FileTreeItem :: appendChild( FileTreeItem
* child
)
60 child
->myParent
= this;
61 myChildren
.append( child
);
65 FileTreeItem :: child( const QString
& filename
)
67 foreach( FileTreeItem
* c
, myChildren
)
68 if( c
->name() == filename
)
75 FileTreeItem :: row( ) const
80 i
= myParent
->myChildren
.indexOf( const_cast<FileTreeItem
*>(this) );
86 FileTreeItem :: data( int column
) const
91 case COL_NAME
: value
.setValue( fileSizeName( ) ); break;
92 case COL_PROGRESS
: value
.setValue( progress( ) ); break;
93 case COL_WANTED
: value
.setValue( isSubtreeWanted( ) ); break;
94 case COL_PRIORITY
: value
.setValue( priorityString( ) ); break;
101 FileTreeItem :: getSubtreeSize( uint64_t& have
, uint64_t& total
) const
104 total
+= myTotalSize
;
106 foreach( const FileTreeItem
* i
, myChildren
)
107 i
->getSubtreeSize( have
, total
);
111 FileTreeItem :: progress( ) const
114 uint64_t have(0), total(0);
115 getSubtreeSize( have
, total
);
117 d
= have
/ (double)total
;
122 FileTreeItem :: fileSizeName( ) const
124 uint64_t have(0), total(0);
126 getSubtreeSize( have
, total
);
127 str
= QString( name() + " (%1)" ).arg( Formatter::sizeToString( total
) );
132 FileTreeItem :: update( int index
, bool wanted
, int priority
, uint64_t totalSize
, uint64_t haveSize
, bool torrentChanged
)
134 bool changed
= false;
136 if( myIndex
!= index
)
141 if( torrentChanged
&& myIsWanted
!= wanted
)
146 if( torrentChanged
&& myPriority
!= priority
)
148 myPriority
= priority
;
151 if( myTotalSize
!= totalSize
)
153 myTotalSize
= totalSize
;
156 if( myHaveSize
!= haveSize
)
158 myHaveSize
= haveSize
;
166 FileTreeItem :: priorityString( ) const
168 const int i( priority( ) );
169 if( i
== LOW
) return tr( "Low" );
170 if( i
== HIGH
) return tr( "High" );
171 if( i
== NORMAL
) return tr( "Normal" );
172 return tr( "Mixed" );
176 FileTreeItem :: priority( ) const
180 if( myChildren
.isEmpty( ) ) switch( myPriority
) {
181 case TR_PRI_LOW
: i
|= LOW
; break;
182 case TR_PRI_HIGH
: i
|= HIGH
; break;
183 default: i
|= NORMAL
; break;
186 foreach( const FileTreeItem
* child
, myChildren
)
187 i
|= child
->priority( );
193 FileTreeItem :: setSubtreePriority( int i
, QSet
<int>& ids
)
195 if( myPriority
!= i
) {
198 ids
.insert( myIndex
);
201 foreach( FileTreeItem
* child
, myChildren
)
202 child
->setSubtreePriority( i
, ids
);
206 FileTreeItem :: twiddlePriority( QSet
<int>& ids
, int& p
)
208 const int old( priority( ) );
210 if ( old
& LOW
) p
= TR_PRI_NORMAL
;
211 else if( old
& NORMAL
) p
= TR_PRI_HIGH
;
214 setSubtreePriority( p
, ids
);
218 FileTreeItem :: isSubtreeWanted( ) const
220 if( myChildren
.isEmpty( ) )
221 return myIsWanted
? Qt::Checked
: Qt::Unchecked
;
224 foreach( const FileTreeItem
* child
, myChildren
) {
225 const int childWanted
= child
->isSubtreeWanted( );
227 wanted
= childWanted
;
228 if( wanted
!= childWanted
)
229 wanted
= Qt::PartiallyChecked
;
230 if( wanted
== Qt::PartiallyChecked
)
238 FileTreeItem :: setSubtreeWanted( bool b
, QSet
<int>& ids
)
240 if( myIsWanted
!= b
) {
243 ids
.insert( myIndex
);
246 foreach( FileTreeItem
* child
, myChildren
)
247 child
->setSubtreeWanted( b
, ids
);
251 FileTreeItem :: twiddleWanted( QSet
<int>& ids
, bool& wanted
)
253 wanted
= isSubtreeWanted( ) != Qt::Checked
;
254 setSubtreeWanted( wanted
, ids
);
262 FileTreeModel :: FileTreeModel( QObject
*parent
):
263 QAbstractItemModel(parent
)
265 rootItem
= new FileTreeItem( -1 );
268 FileTreeModel :: ~FileTreeModel( )
276 FileTreeModel :: data( const QModelIndex
&index
, int role
) const
280 if( index
.isValid() && role
==Qt::DisplayRole
)
282 FileTreeItem
*item
= static_cast<FileTreeItem
*>(index
.internalPointer());
283 value
= item
->data( index
.column( ) );
290 FileTreeModel :: flags( const QModelIndex
& index
) const
292 int i( Qt::ItemIsSelectable
| Qt::ItemIsEnabled
);
294 if( index
.column( ) == COL_WANTED
)
295 i
|= Qt::ItemIsUserCheckable
| Qt::ItemIsTristate
;
297 return (Qt::ItemFlags
)i
;
301 FileTreeModel :: headerData( int column
, Qt::Orientation orientation
, int role
) const
305 if( orientation
==Qt::Horizontal
&& role
==Qt::DisplayRole
) {
307 case COL_NAME
: data
.setValue( tr( "File" ) ); break;
308 case COL_PROGRESS
: data
.setValue( tr( "Progress" ) ); break;
309 case COL_WANTED
: data
.setValue( tr( "Download" ) ); break;
310 case COL_PRIORITY
: data
.setValue( tr( "Priority" ) ); break;
319 FileTreeModel :: index( int row
, int column
, const QModelIndex
& parent
) const
323 if( !hasIndex( row
, column
, parent
) )
325 std::cerr
<< " I don't have this index " << std::endl
;
329 FileTreeItem
* parentItem
;
331 if( !parent
.isValid( ) )
332 parentItem
= rootItem
;
334 parentItem
= static_cast<FileTreeItem
*>(parent
.internalPointer());
336 FileTreeItem
* childItem
= parentItem
->child( row
);
339 i
= createIndex( row
, column
, childItem
);
341 //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;
348 FileTreeModel :: parent( const QModelIndex
& child
) const
350 return parent( child
, 0 ); // QAbstractItemModel::parent() wants col 0
354 FileTreeModel :: parent( const QModelIndex
& child
, int column
) const
356 if( !child
.isValid( ) )
357 return QModelIndex( );
359 FileTreeItem
* childItem
= static_cast<FileTreeItem
*>(child
.internalPointer());
361 return indexOf( childItem
->parent( ), column
);
365 FileTreeModel :: rowCount( const QModelIndex
& parent
) const
367 FileTreeItem
* parentItem
;
369 if( !parent
.isValid( ) )
370 parentItem
= rootItem
;
372 parentItem
= static_cast<FileTreeItem
*>(parent
.internalPointer());
374 return parentItem
->childCount();
378 FileTreeModel :: columnCount( const QModelIndex
&parent
) const
386 FileTreeModel :: indexOf( FileTreeItem
* item
, int column
) const
388 if( !item
|| item
==rootItem
)
389 return QModelIndex( );
391 return createIndex( item
->row( ), column
, item
);
395 FileTreeModel :: clearSubtree( const QModelIndex
& top
)
397 while( hasChildren( top
) )
398 clearSubtree( index( 0, 0, top
) );
400 delete static_cast<FileTreeItem
*>(top
.internalPointer());
404 FileTreeModel :: clear( )
406 clearSubtree( QModelIndex( ) );
412 FileTreeModel :: addFile( int index
,
413 const QString
& filename
,
418 QList
<QModelIndex
> & rowsAdded
,
419 bool torrentChanged
)
421 FileTreeItem
* i( rootItem
);
423 foreach( QString token
, filename
.split( "/" ) )
425 FileTreeItem
* child( i
->child( token
) );
428 QModelIndex
parentIndex( indexOf( i
, 0 ) );
429 const int n( i
->childCount( ) );
430 beginInsertRows( parentIndex
, n
, n
);
431 i
->appendChild(( child
= new FileTreeItem( -1, token
)));
433 rowsAdded
.append( indexOf( child
, 0 ) );
439 if( i
->update( index
, wanted
, priority
, size
, have
, torrentChanged
) )
440 dataChanged( indexOf( i
, 0 ), indexOf( i
, NUM_COLUMNS
-1 ) );
444 FileTreeModel :: parentsChanged( const QModelIndex
& index
, int column
)
446 QModelIndex walk
= index
;
449 walk
= parent( walk
, column
);
450 if( !walk
.isValid( ) )
452 dataChanged( walk
, walk
);
457 FileTreeModel :: subtreeChanged( const QModelIndex
& index
, int column
)
459 const int childCount
= rowCount( index
);
463 // tell everyone that this tier changed
464 dataChanged( index
.child(0,column
), index
.child(childCount
-1,column
) );
467 for( int i
=0; i
<childCount
; ++i
)
468 subtreeChanged( index
.child(i
,column
), column
);
472 FileTreeModel :: clicked( const QModelIndex
& index
)
474 const int column( index
.column( ) );
476 if( !index
.isValid( ) )
479 if( column
== COL_WANTED
)
481 FileTreeItem
* item( static_cast<FileTreeItem
*>(index
.internalPointer()));
484 item
->twiddleWanted( fileIds
, want
);
485 emit
wantedChanged( fileIds
, want
);
487 dataChanged( index
, index
);
488 parentsChanged( index
, column
);
489 subtreeChanged( index
, column
);
491 else if( column
== COL_PRIORITY
)
493 FileTreeItem
* item( static_cast<FileTreeItem
*>(index
.internalPointer()));
496 item
->twiddlePriority( fileIds
, priority
);
497 emit
priorityChanged( fileIds
, priority
);
499 dataChanged( index
, index
);
500 parentsChanged( index
, column
);
501 subtreeChanged( index
, column
);
510 FileTreeDelegate :: sizeHint( const QStyleOptionViewItem
& item
, const QModelIndex
& index
) const
514 switch( index
.column( ) )
517 const QFontMetrics
fm( item
.font
);
518 const QString text
= index
.data().toString();
519 const int iconSize
= QApplication::style()->pixelMetric( QStyle::PM_SmallIconSize
);
520 size
.rwidth() = HIG::PAD_SMALL
+ iconSize
;
521 size
.rheight() = std::max( iconSize
, fm
.height( ) );
527 size
= QSize( 20, 1 );
531 const QFontMetrics
fm( item
.font
);
532 const QString text
= index
.data().toString();
533 size
= fm
.size( 0, text
);
538 size
.rheight() += 8; // make the spacing a little nicer
543 FileTreeDelegate :: paint( QPainter
* painter
,
544 const QStyleOptionViewItem
& option
,
545 const QModelIndex
& index
) const
547 const int column( index
.column( ) );
549 if( ( column
!= COL_PROGRESS
) && ( column
!= COL_WANTED
) && ( column
!= COL_NAME
) )
551 QItemDelegate::paint(painter
, option
, index
);
555 QStyle
* style( QApplication :: style( ) );
556 if( option
.state
& QStyle::State_Selected
)
557 painter
->fillRect( option
.rect
, option
.palette
.highlight( ) );
559 if( option
.state
& QStyle::State_Selected
)
560 painter
->setBrush(option
.palette
.highlightedText());
562 if( column
== COL_NAME
)
564 // draw the file icon
565 static const int iconSize( style
->pixelMetric( QStyle :: PM_SmallIconSize
) );
566 const QRect
iconArea( option
.rect
.x(),
567 option
.rect
.y() + (option
.rect
.height()-iconSize
)/2,
568 iconSize
, iconSize
);
570 if( index
.model()->hasChildren( index
) )
571 icon
= style
->standardIcon( QStyle::StandardPixmap( QStyle::SP_DirOpenIcon
) );
574 QString name
= index
.data().toString();
575 icon
= Utils :: guessMimeIcon( name
.left( name
.lastIndexOf( " (" ) ) );
577 icon
.paint( painter
, iconArea
, Qt::AlignCenter
, QIcon::Normal
, QIcon::On
);
580 QStyleOptionViewItem
tmp( option
);
581 tmp
.rect
.setWidth( option
.rect
.width( ) - iconArea
.width( ) - HIG::PAD_SMALL
);
582 tmp
.rect
.moveRight( option
.rect
.right( ) );
583 QItemDelegate::paint( painter
, tmp
, index
);
585 else if( column
== COL_PROGRESS
)
587 QStyleOptionProgressBar p
;
588 p
.state
= option
.state
| QStyle::State_Small
;
589 p
.direction
= QApplication::layoutDirection();
590 p
.rect
= option
.rect
;
591 p
.rect
.setSize( QSize( option
.rect
.width()-2, option
.rect
.height()-8 ) );
592 p
.rect
.moveCenter( option
.rect
.center( ) );
593 p
.fontMetrics
= QApplication::fontMetrics();
596 p
.textAlignment
= Qt::AlignCenter
;
597 p
.textVisible
= true;
598 p
.progress
= (int)(100.0*index
.data().toDouble());
599 p
.text
= QString( ).sprintf( "%d%%", p
.progress
);
600 style
->drawControl( QStyle::CE_ProgressBar
, &p
, painter
);
602 else if( column
== COL_WANTED
)
604 QStyleOptionButton o
;
605 o
.state
= option
.state
;
606 o
.direction
= QApplication::layoutDirection();
607 o
.rect
.setSize( QSize( 20, option
.rect
.height( ) ) );
608 o
.rect
.moveCenter( option
.rect
.center( ) );
609 o
.fontMetrics
= QApplication::fontMetrics();
610 switch( index
.data().toInt() ) {
611 case Qt::Unchecked
: o
.state
|= QStyle::State_Off
; break;
612 case Qt::Checked
: o
.state
|= QStyle::State_On
; break;
613 default: o
.state
|= QStyle::State_NoChange
;break;
615 style
->drawControl( QStyle::CE_CheckBox
, &o
, painter
);
627 FileTreeView :: FileTreeView( QWidget
* parent
):
630 myProxy( new QSortFilterProxyModel( ) ),
633 setSortingEnabled( true );
634 setAlternatingRowColors( true );
635 setSelectionBehavior( QAbstractItemView::SelectRows
);
636 setSelectionMode( QAbstractItemView::ExtendedSelection
);
637 myProxy
->setSourceModel( &myModel
);
639 setItemDelegate( &myDelegate
);
640 setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff
);
641 sortByColumn( COL_NAME
, Qt::AscendingOrder
);
642 installEventFilter( this );
644 for( int i
=0; i
<NUM_COLUMNS
; ++i
)
645 header()->setResizeMode( i
, QHeaderView::Fixed
);
647 connect( this, SIGNAL(clicked(const QModelIndex
&)),
648 this, SLOT(onClicked(const QModelIndex
&)) );
650 connect( &myModel
, SIGNAL(priorityChanged(const QSet
<int>&, int)),
651 this, SIGNAL(priorityChanged(const QSet
<int>&, int)));
653 connect( &myModel
, SIGNAL(wantedChanged(const QSet
<int>&, bool)),
654 this, SIGNAL(wantedChanged(const QSet
<int>&, bool)));
657 FileTreeView :: ~FileTreeView( )
659 myProxy
->deleteLater();
663 FileTreeView :: onClicked( const QModelIndex
& proxyIndex
)
665 const QModelIndex modelIndex
= myProxy
->mapToSource( proxyIndex
);
666 myModel
.clicked( modelIndex
);
670 FileTreeView :: eventFilter( QObject
* o
, QEvent
* event
)
675 // this is kind of a hack to get the last three columns be the
676 // right size, and to have the filename column use whatever
677 // space is left over...
678 if( event
->type() == QEvent::Resize
)
680 QResizeEvent
* r
= dynamic_cast<QResizeEvent
*>(event
);
681 int left
= r
->size().width();
682 const QFontMetrics
fontMetrics( font( ) );
683 for( int column
=0; column
<NUM_COLUMNS
; ++column
) {
684 if( column
== COL_NAME
)
686 if( isColumnHidden( column
) )
688 const QString header
= myModel
.headerData( column
, Qt::Horizontal
).toString( ) + " ";
689 const int width
= fontMetrics
.size( 0, header
).width( );
690 setColumnWidth( column
, width
);
693 left
-= 20; // not sure why this is necessary. it works in different themes + font sizes though...
694 setColumnWidth( COL_NAME
, std::max(left
,0) );
698 // handle using the keyboard to toggle the
699 // wanted/unwanted state or the file priority
700 else if( event
->type() == QEvent::KeyPress
)
702 switch( dynamic_cast<QKeyEvent
*>(event
)->key() )
705 foreach( QModelIndex i
, selectionModel()->selectedRows(COL_WANTED
) )
711 foreach( QModelIndex i
, selectionModel()->selectedRows(COL_PRIORITY
) )
721 FileTreeView :: update( const FileList
& files
)
723 update( files
, true );
727 FileTreeView :: update( const FileList
& files
, bool torrentChanged
)
729 foreach( const TrFile file
, files
) {
730 QList
<QModelIndex
> added
;
731 myModel
.addFile( file
.index
, file
.filename
, file
.wanted
, file
.priority
, file
.size
, file
.have
, added
, torrentChanged
);
732 foreach( QModelIndex i
, added
)
733 expand( myProxy
->mapFromSource( i
) );
738 FileTreeView :: clear( )