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: torrent-delegate.cc 13553 2012-10-07 17:51:56Z jordan $
11 */
12
13#include <iostream>
14
15#include <QApplication>
16#include <QFont>
17#include <QFontMetrics>
18#include <QIcon>
19#include <QModelIndex>
20#include <QPainter>
21#include <QPixmap>
22#include <QPixmapCache>
23#include <QStyleOptionProgressBarV2>
24
25#include "formatter.h"
26#include "torrent.h"
27#include "torrent-delegate.h"
28#include "torrent-model.h"
29
30enum
31{
32   GUI_PAD = 6,
33   BAR_HEIGHT = 12
34};
35
36QColor TorrentDelegate :: greenBrush;
37QColor TorrentDelegate :: blueBrush;
38QColor TorrentDelegate :: greenBack;
39QColor TorrentDelegate :: blueBack;
40
41TorrentDelegate :: TorrentDelegate( QObject * parent ):
42    QStyledItemDelegate( parent ),
43    myProgressBarStyle( new QStyleOptionProgressBarV2 )
44{
45    myProgressBarStyle->minimum = 0;
46    myProgressBarStyle->maximum = 1000;
47
48    greenBrush = QColor("forestgreen");
49    greenBack = QColor("darkseagreen");
50
51    blueBrush = QColor("steelblue");
52    blueBack = QColor("lightgrey");
53}
54
55TorrentDelegate :: ~TorrentDelegate( )
56{
57    delete myProgressBarStyle;
58}
59
60/***
61****
62***/
63
64QSize
65TorrentDelegate :: margin( const QStyle& style ) const
66{
67    Q_UNUSED( style );
68
69    return QSize( 4, 4 );
70}
71
72QString
73TorrentDelegate :: progressString( const Torrent& tor ) const
74{
75    const bool isMagnet( !tor.hasMetadata( ) );
76    const bool isDone( tor.isDone( ) );
77    const bool isSeed( tor.isSeed( ) );
78    const uint64_t haveTotal( tor.haveTotal( ) );
79    QString str;
80    double seedRatio;
81    const bool hasSeedRatio( tor.getSeedRatio( seedRatio ) );
82
83    if( isMagnet ) // magnet link with no metadata
84    {
85        /* %1 is the percentage of torrent metadata downloaded */
86        str = tr( "Magnetized transfer - retrieving metadata (%1%)" )
87            .arg( Formatter::percentToString( tor.metadataPercentDone() * 100.0 ) );
88    }
89    else if( !isDone ) // downloading
90    {
91        /* %1 is how much we've got,
92           %2 is how much we'll have when done,
93           %3 is a percentage of the two */
94        str = tr( "%1 of %2 (%3%)" ).arg( Formatter::sizeToString( haveTotal ) )
95                                    .arg( Formatter::sizeToString( tor.sizeWhenDone( ) ) )
96                                    .arg( Formatter::percentToString( tor.percentDone( ) * 100.0 ) );
97    }
98    else if( !isSeed ) // partial seed
99    {
100        if( hasSeedRatio )
101        {
102            /* %1 is how much we've got,
103               %2 is the torrent's total size,
104               %3 is a percentage of the two,
105               %4 is how much we've uploaded,
106               %5 is our upload-to-download ratio
107               %6 is the ratio we want to reach before we stop uploading */
108            str = tr( "%1 of %2 (%3%), uploaded %4 (Ratio: %5 Goal: %6)" )
109                  .arg( Formatter::sizeToString( haveTotal ) )
110                  .arg( Formatter::sizeToString( tor.totalSize( ) ) )
111                  .arg( Formatter::percentToString( tor.percentComplete( ) * 100.0 ) )
112                  .arg( Formatter::sizeToString( tor.uploadedEver( ) ) )
113                  .arg( Formatter::ratioToString( tor.ratio( ) ) )
114                  .arg( Formatter::ratioToString( seedRatio ) );
115        }
116        else
117        {
118            /* %1 is how much we've got,
119               %2 is the torrent's total size,
120               %3 is a percentage of the two,
121               %4 is how much we've uploaded,
122               %5 is our upload-to-download ratio */
123            str = tr( "%1 of %2 (%3%), uploaded %4 (Ratio: %5)" )
124                  .arg( Formatter::sizeToString( haveTotal ) )
125                  .arg( Formatter::sizeToString( tor.totalSize( ) ) )
126                  .arg( Formatter::percentToString( tor.percentComplete( ) * 100.0 ) )
127                  .arg( Formatter::sizeToString( tor.uploadedEver( ) ) )
128                  .arg( Formatter::ratioToString( tor.ratio( ) ) );
129        }
130    }
131    else // seeding
132    {
133        if( hasSeedRatio )
134        {
135            /* %1 is the torrent's total size,
136               %2 is how much we've uploaded,
137               %3 is our upload-to-download ratio,
138               %4 is the ratio we want to reach before we stop uploading */
139            str = tr( "%1, uploaded %2 (Ratio: %3 Goal: %4)" )
140                  .arg( Formatter::sizeToString( haveTotal ) )
141                  .arg( Formatter::sizeToString( tor.uploadedEver( ) ) )
142                  .arg( Formatter::ratioToString( tor.ratio( ) ) )
143                  .arg( Formatter::ratioToString( seedRatio ) );
144        }
145        else /* seeding w/o a ratio */
146        {
147            /* %1 is the torrent's total size,
148               %2 is how much we've uploaded,
149               %3 is our upload-to-download ratio */
150            str = tr( "%1, uploaded %2 (Ratio: %3)" )
151                  .arg( Formatter::sizeToString( haveTotal ) )
152                  .arg( Formatter::sizeToString( tor.uploadedEver( ) ) )
153                  .arg( Formatter::ratioToString( tor.ratio( ) ) );
154        }
155    }
156
157    /* add time when downloading */
158    if( ( hasSeedRatio && tor.isSeeding( ) ) || tor.isDownloading( ) )
159    {
160        str += tr( " - " );
161        if( tor.hasETA( ) )
162            str += tr( "%1 left" ).arg( Formatter::timeToString( tor.getETA( ) ) );
163        else
164            str += tr( "Remaining time unknown" );
165    }
166
167    return str;
168}
169
170QString
171TorrentDelegate :: shortTransferString( const Torrent& tor ) const
172{
173    static const QChar upArrow( 0x2191 );
174    static const QChar downArrow( 0x2193 );
175    const bool haveMeta( tor.hasMetadata( ) );
176    const bool haveDown( haveMeta && ((tor.webseedsWeAreDownloadingFrom()>0) || (tor.peersWeAreDownloadingFrom( )>0)) );
177    const bool haveUp( haveMeta && tor.peersWeAreUploadingTo( ) > 0 );
178    QString downStr, upStr, str;
179
180    if( haveDown )
181        downStr = Formatter::speedToString( tor.downloadSpeed( ) );
182    if( haveUp )
183        upStr = Formatter::speedToString( tor.uploadSpeed( ) );
184
185    if( haveDown && haveUp )
186        str = tr( "%1 %2, %3 %4" ).arg(downArrow).arg(downStr).arg(upArrow).arg(upStr);
187    else if( haveDown )
188        str = tr( "%1 %2" ).arg(downArrow).arg(downStr);
189    else if( haveUp )
190        str = tr( "%1 %2" ).arg(upArrow).arg(upStr);
191    else if( tor.isStalled( ) )
192        str = tr( "Stalled" );
193    else if( tor.hasMetadata( ) )
194        str = tr( "Idle" );
195
196    return str;
197}
198
199QString
200TorrentDelegate :: shortStatusString( const Torrent& tor ) const
201{
202    QString str;
203
204    switch( tor.getActivity( ) )
205    {
206        case TR_STATUS_CHECK:
207            str = tr( "Verifying local data (%1% tested)" ).arg( Formatter::percentToString( tor.getVerifyProgress()*100.0 ) );
208            break;
209
210        case TR_STATUS_DOWNLOAD:
211        case TR_STATUS_SEED:
212            if( !tor.isDownloading( ) )
213                str = tr( "Ratio: %1, " ).arg( Formatter::ratioToString( tor.ratio( ) ) );
214            str += shortTransferString( tor );
215            break;
216
217        default:
218            str = tor.activityString( );
219            break;
220    }
221
222    return str;
223}
224
225QString
226TorrentDelegate :: statusString( const Torrent& tor ) const
227{
228    QString str;
229
230    if( tor.hasError( ) )
231    {
232        str = tor.getError( );
233    }
234    else switch( tor.getActivity( ) )
235    {
236        case TR_STATUS_STOPPED:
237        case TR_STATUS_CHECK_WAIT:
238        case TR_STATUS_CHECK:
239        case TR_STATUS_DOWNLOAD_WAIT:
240        case TR_STATUS_SEED_WAIT:
241            str = shortStatusString( tor );
242            break;
243
244        case TR_STATUS_DOWNLOAD:
245            if( !tor.hasMetadata() ) {
246                str = tr( "Downloading metadata from %n peer(s) (%1% done)", 0, tor.peersWeAreDownloadingFrom( ) )
247                        .arg( Formatter::percentToString( 100.0 * tor.metadataPercentDone( ) ) );
248            } else {
249                /* it would be nicer for translation if this was all one string, but I don't see how to do multiple %n's in tr() */
250                str = tr( "Downloading from %1 of %n connected peer(s)", 0, tor.connectedPeersAndWebseeds( ) )
251                        .arg( tor.peersWeAreDownloadingFrom( ) );
252                if (tor.webseedsWeAreDownloadingFrom())
253                    str += tr(" and %n web seed(s)", "", tor.webseedsWeAreDownloadingFrom());
254            }
255            break;
256
257        case TR_STATUS_SEED:
258            str = tr( "Seeding to %1 of %n connected peer(s)", 0, tor.connectedPeers( ) )
259                  .arg( tor.peersWeAreUploadingTo( ) );
260            break;
261
262        default:
263            str = tr( "Error" );
264            break;
265    }
266
267    if( tor.isReadyToTransfer( ) ) {
268        QString s = shortTransferString( tor );
269        if( !s.isEmpty( ) )
270            str += tr( " - " ) + s;
271    }
272
273    return str;
274}
275
276/***
277****
278***/
279
280namespace
281{
282    int MAX3( int a, int b, int c )
283    {
284        const int ab( a > b ? a : b );
285        return ab > c ? ab : c;
286    }
287}
288
289QSize
290TorrentDelegate :: sizeHint( const QStyleOptionViewItem& option, const Torrent& tor ) const
291{
292    const QStyle* style( QApplication::style( ) );
293    static const int iconSize( style->pixelMetric( QStyle::PM_MessageBoxIconSize ) );
294
295    QFont nameFont( option.font );
296    nameFont.setWeight( QFont::Bold );
297    const QFontMetrics nameFM( nameFont );
298    const QString nameStr( tor.name( ) );
299    const int nameWidth = nameFM.width( nameStr );
300    QFont statusFont( option.font );
301    statusFont.setPointSize( int( option.font.pointSize( ) * 0.9 ) );
302    const QFontMetrics statusFM( statusFont );
303    const QString statusStr( statusString( tor ) );
304    const int statusWidth = statusFM.width( statusStr );
305    QFont progressFont( statusFont );
306    const QFontMetrics progressFM( progressFont );
307    const QString progressStr( progressString( tor ) );
308    const int progressWidth = progressFM.width( progressStr );
309    const QSize m( margin( *style ) );
310    return QSize( m.width()*2 + iconSize + GUI_PAD + MAX3( nameWidth, statusWidth, progressWidth ),
311                  //m.height()*3 + nameFM.lineSpacing() + statusFM.lineSpacing()*2 + progressFM.lineSpacing() );
312                  m.height()*3 + nameFM.lineSpacing() + statusFM.lineSpacing() + BAR_HEIGHT + progressFM.lineSpacing() );
313}
314
315QSize
316TorrentDelegate :: sizeHint( const QStyleOptionViewItem  & option,
317                             const QModelIndex           & index ) const
318{
319    const Torrent * tor( index.data( TorrentModel::TorrentRole ).value<const Torrent*>() );
320    return sizeHint( option, *tor );
321}
322
323void
324TorrentDelegate :: paint( QPainter                    * painter,
325                          const QStyleOptionViewItem  & option,
326                          const QModelIndex           & index) const
327{
328    const Torrent * tor( index.data( TorrentModel::TorrentRole ).value<const Torrent*>() );
329    painter->save( );
330    painter->setClipRect( option.rect );
331    drawTorrent( painter, option, *tor );
332    painter->restore( );
333}
334
335void
336TorrentDelegate :: setProgressBarPercentDone( const QStyleOptionViewItem& option, const Torrent& tor ) const
337{
338    double seedRatioLimit;
339    if (tor.isSeeding() && tor.getSeedRatio(seedRatioLimit))
340    {
341        const double seedRateRatio = tor.ratio() / seedRatioLimit;
342        const int scaledProgress = seedRateRatio * (myProgressBarStyle->maximum - myProgressBarStyle->minimum);
343        myProgressBarStyle->progress = myProgressBarStyle->minimum + scaledProgress;
344    }
345    else
346    {
347        const bool isMagnet( !tor.hasMetadata( ) );
348        myProgressBarStyle->direction = option.direction;
349        myProgressBarStyle->progress = int(myProgressBarStyle->minimum + (((isMagnet ? tor.metadataPercentDone() : tor.percentDone()) * (myProgressBarStyle->maximum - myProgressBarStyle->minimum))));
350    }
351}
352
353void
354TorrentDelegate :: drawTorrent( QPainter * painter, const QStyleOptionViewItem& option, const Torrent& tor ) const
355{
356    const QStyle * style( QApplication::style( ) );
357    static const int iconSize( style->pixelMetric( QStyle::PM_LargeIconSize ) );
358    QFont nameFont( option.font );
359    nameFont.setWeight( QFont::Bold );
360    const QFontMetrics nameFM( nameFont );
361    const QString nameStr( tor.name( ) );
362    const QSize nameSize( nameFM.size( 0, nameStr ) );
363    QFont statusFont( option.font );
364    statusFont.setPointSize( int( option.font.pointSize( ) * 0.9 ) );
365    const QFontMetrics statusFM( statusFont );
366    const QString statusStr( progressString( tor ) );
367    QFont progressFont( statusFont );
368    const QFontMetrics progressFM( progressFont );
369    const QString progressStr( statusString( tor ) );
370    const bool isPaused( tor.isPaused( ) );
371
372    painter->save( );
373
374    if (option.state & QStyle::State_Selected) {
375        QPalette::ColorGroup cg = option.state & QStyle::State_Enabled
376                                  ? QPalette::Normal : QPalette::Disabled;
377        if (cg == QPalette::Normal && !(option.state & QStyle::State_Active))
378            cg = QPalette::Inactive;
379
380        painter->fillRect(option.rect, option.palette.brush(cg, QPalette::Highlight));
381    }
382
383    QIcon::Mode im;
384    if( isPaused || !(option.state & QStyle::State_Enabled ) ) im = QIcon::Disabled;
385    else if( option.state & QStyle::State_Selected ) im = QIcon::Selected;
386    else im = QIcon::Normal;
387
388    QIcon::State qs;
389    if( isPaused ) qs = QIcon::Off;
390    else qs = QIcon::On;
391
392    QPalette::ColorGroup cg = QPalette::Normal;
393    if( isPaused || !(option.state & QStyle::State_Enabled ) ) cg = QPalette::Disabled;
394    if( cg == QPalette::Normal && !(option.state & QStyle::State_Active ) ) cg = QPalette::Inactive;
395
396    QPalette::ColorRole cr;
397    if( option.state & QStyle::State_Selected ) cr = QPalette::HighlightedText;
398    else cr = QPalette::Text;
399
400    QStyle::State progressBarState( option.state );
401    if( isPaused ) progressBarState = QStyle::State_None;
402    progressBarState |= QStyle::State_Small;
403
404    // layout
405    const QSize m( margin( *style ) );
406    QRect fillArea( option.rect );
407    fillArea.adjust( m.width(), m.height(), -m.width(), -m.height() );
408    QRect iconArea( fillArea.x( ), fillArea.y( ) + ( fillArea.height( ) - iconSize ) / 2, iconSize, iconSize );
409    QRect nameArea( iconArea.x( ) + iconArea.width( ) + GUI_PAD, fillArea.y( ),
410                    fillArea.width( ) - GUI_PAD - iconArea.width( ), nameSize.height( ) );
411    QRect statusArea( nameArea );
412    statusArea.moveTop( nameArea.y( ) + nameFM.lineSpacing( ) );
413    statusArea.setHeight( nameSize.height( ) );
414    QRect barArea( statusArea );
415    barArea.setHeight( BAR_HEIGHT );
416    barArea.moveTop( statusArea.y( ) + statusFM.lineSpacing( ) );
417    QRect progArea( statusArea );
418    progArea.moveTop( barArea.y( ) + barArea.height( ) );
419
420    // render
421    if( tor.hasError( ) )
422        painter->setPen( QColor( "red" ) );
423    else
424        painter->setPen( option.palette.color( cg, cr ) );
425    tor.getMimeTypeIcon().paint( painter, iconArea, Qt::AlignCenter, im, qs );
426    painter->setFont( nameFont );
427    painter->drawText( nameArea, 0, nameFM.elidedText( nameStr, Qt::ElideRight, nameArea.width( ) ) );
428    painter->setFont( statusFont );
429    painter->drawText( statusArea, 0, statusFM.elidedText( statusStr, Qt::ElideRight, statusArea.width( ) ) );
430    painter->setFont( progressFont );
431    painter->drawText( progArea, 0, progressFM.elidedText( progressStr, Qt::ElideRight, progArea.width( ) ) );
432    myProgressBarStyle->rect = barArea;
433    if ( tor.isDownloading() ) {
434        myProgressBarStyle->palette.setBrush( QPalette::Highlight, blueBrush );
435        myProgressBarStyle->palette.setColor( QPalette::Base, blueBack );
436        myProgressBarStyle->palette.setColor( QPalette::Background, blueBack );
437    }
438    else if ( tor.isSeeding() ) {
439        myProgressBarStyle->palette.setBrush( QPalette::Highlight, greenBrush );
440        myProgressBarStyle->palette.setColor( QPalette::Base, greenBack );
441        myProgressBarStyle->palette.setColor( QPalette::Background, greenBack );
442    }
443    myProgressBarStyle->state = progressBarState;
444    setProgressBarPercentDone( option, tor );
445
446    style->drawControl( QStyle::CE_ProgressBar, myProgressBarStyle, painter );
447
448    painter->restore( );
449}
450