source: trunk/qt/options.cc @ 13447

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

(trunk, qt) #4036 'Qt Client doesnt delete torrent files' -- use a patch by rb07 to work around a Firefox idiosyncrasy that prevented the added .torrent file from being removed.

  • Property svn:keywords set to Date Rev Author Id
File size: 17.2 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: options.cc 13447 2012-08-19 00:12:43Z jordan $
11 */
12
13#include <cstdio>
14#include <iostream>
15
16#include <QApplication>
17#include <QCheckBox>
18#include <QComboBox>
19#include <QDialogButtonBox>
20#include <QEvent>
21#include <QFileDialog>
22#include <QFileIconProvider>
23#include <QFileInfo>
24#include <QGridLayout>
25#include <QLabel>
26#include <QMessageBox>
27#include <QPushButton>
28#include <QResizeEvent>
29#include <QSet>
30#include <QVBoxLayout>
31#include <QWidget>
32#include <QLineEdit>
33
34#include <libtransmission/transmission.h>
35#include <libtransmission/bencode.h>
36#include <libtransmission/utils.h> /* mime64 */
37
38#include "add-data.h"
39#include "file-tree.h"
40#include "hig.h"
41#include "options.h"
42#include "prefs.h"
43#include "session.h"
44#include "torrent.h"
45#include "utils.h"
46
47/***
48****
49***/
50
51void
52FileAdded :: executed( int64_t tag, const QString& result, struct tr_benc * arguments )
53{
54    Q_UNUSED( arguments );
55
56    if( tag != myTag )
57        return;
58
59    if( ( result == "success" ) && !myDelFile.isEmpty( ) ) {
60        QFile file( myDelFile );
61        file.setPermissions( QFile::ReadOwner | QFile::WriteOwner );
62        file.remove();
63    }
64
65    if( result != "success" ) {
66        QString text = result;
67        for( int i=0, n=text.size(); i<n; ++i )
68            if( !i || text[i-1].isSpace() )
69                text[i] = text[i].toUpper();
70        QMessageBox::warning( QApplication::activeWindow(),
71                              tr( "Error Adding Torrent" ),
72                              QString("<p><b>%1</b></p><p>%2</p>").arg(text).arg(myName) );
73    }
74
75    deleteLater();
76}
77
78/***
79****
80***/
81
82Options :: Options( Session& session, const Prefs& prefs, const AddData& addme, QWidget * parent ):
83    QDialog( parent, Qt::Dialog ),
84    mySession( session ),
85    myAdd( addme ),
86    myHaveInfo( false ),
87    myDestinationButton( 0 ),
88    myVerifyButton( 0 ),
89    myVerifyFile( 0 ),
90    myVerifyHash( QCryptographicHash::Sha1 )
91
92{
93    setWindowTitle( tr( "Open Torrent" ) );
94    QFontMetrics fontMetrics( font( ) );
95    QGridLayout * layout = new QGridLayout( this );
96    int row = 0;
97
98    const int iconSize( style( )->pixelMetric( QStyle :: PM_SmallIconSize ) );
99    QIcon fileIcon = style( )->standardIcon( QStyle::SP_FileIcon );
100    const QPixmap filePixmap = fileIcon.pixmap( iconSize );
101
102    QPushButton * p;
103    int width = fontMetrics.size( 0, QString::fromAscii( "This is a pretty long torrent filename indeed.torrent" ) ).width( );
104    QLabel * l = new QLabel( tr( "&Torrent file:" ) );
105    layout->addWidget( l, row, 0, Qt::AlignLeft );
106    p = myFileButton =  new QPushButton;
107    p->setIcon( filePixmap );
108    p->setMinimumWidth( width );
109    p->setStyleSheet( QString::fromAscii( "text-align: left; padding-left: 5; padding-right: 5" ) );
110    p->installEventFilter( this );
111
112    layout->addWidget( p, row, 1 );
113    l->setBuddy( p );
114    connect( p, SIGNAL(clicked(bool)), this, SLOT(onFilenameClicked()));
115
116    const QFileIconProvider iconProvider;
117    const QIcon folderIcon = iconProvider.icon( QFileIconProvider::Folder );
118    const QPixmap folderPixmap = folderIcon.pixmap( iconSize );
119
120    l = new QLabel( tr( "&Destination folder:" ) );
121    layout->addWidget( l, ++row, 0, Qt::AlignLeft );
122
123    if( session.isLocal( ) )
124    {
125        myDestination.setPath( prefs.getString( Prefs :: DOWNLOAD_DIR ) );
126        p = myDestinationButton = new QPushButton;
127        p->setIcon( folderPixmap );
128        p->setStyleSheet( "text-align: left; padding-left: 5; padding-right: 5" );
129        p->installEventFilter( this );
130        layout->addWidget( p, row, 1 );
131        l->setBuddy( p );
132        connect( p, SIGNAL(clicked(bool)), this, SLOT(onDestinationClicked()));
133    }
134    else
135    {
136        QLineEdit * e = myDestinationEdit = new QLineEdit;
137        e->setText( prefs.getString( Prefs :: DOWNLOAD_DIR ) );
138        layout->addWidget( e, row, 1 );
139        l->setBuddy( e );
140    }
141
142    myTree = new FileTreeView;
143    layout->addWidget( myTree, ++row, 0, 1, 2 );
144    if( !session.isLocal( ) )
145        myTree->hideColumn( 1 ); // hide the % done, since we've no way of knowing
146
147    QComboBox * m = new QComboBox;
148    m->addItem( tr( "High" ),   TR_PRI_HIGH );
149    m->addItem( tr( "Normal" ), TR_PRI_NORMAL );
150    m->addItem( tr( "Low" ),    TR_PRI_LOW );
151    m->setCurrentIndex( 1 ); // Normal
152    myPriorityCombo = m;
153    l = new QLabel( tr( "Torrent &priority:" ) );
154    l->setBuddy( m );
155    layout->addWidget( l, ++row, 0, Qt::AlignLeft );
156    layout->addWidget( m, row, 1 );
157
158    if( session.isLocal( ) )
159    {
160        p = myVerifyButton = new QPushButton( tr( "&Verify Local Data" ) );
161        layout->addWidget( p, ++row, 0, Qt::AlignLeft );
162    }
163
164    QCheckBox * c;
165    c = myStartCheck = new QCheckBox( tr( "&Start when added" ) );
166    c->setChecked( prefs.getBool( Prefs :: START ) );
167    layout->addWidget( c, ++row, 0, 1, 2, Qt::AlignLeft );
168
169    c = myTrashCheck = new QCheckBox( tr( "Mo&ve .torrent file to the trash" ) );
170    c->setChecked( prefs.getBool( Prefs :: TRASH_ORIGINAL ) );
171    layout->addWidget( c, ++row, 0, 1, 2, Qt::AlignLeft );
172
173    QDialogButtonBox * b = new QDialogButtonBox( QDialogButtonBox::Open|QDialogButtonBox::Cancel, Qt::Horizontal, this );
174    connect( b, SIGNAL(rejected()), this, SLOT(deleteLater()) );
175    connect( b, SIGNAL(accepted()), this, SLOT(onAccepted()) );
176    layout->addWidget( b, ++row, 0, 1, 2 );
177
178    layout->setRowStretch( 2, 2 );
179    layout->setColumnStretch( 1, 2 );
180    layout->setSpacing( HIG :: PAD );
181
182    connect( myTree, SIGNAL(priorityChanged(const QSet<int>&,int)), this, SLOT(onPriorityChanged(const QSet<int>&,int)));
183    connect( myTree, SIGNAL(wantedChanged(const QSet<int>&,bool)), this, SLOT(onWantedChanged(const QSet<int>&,bool)));
184    if( session.isLocal( ) )
185        connect( myVerifyButton, SIGNAL(clicked(bool)), this, SLOT(onVerify()));
186
187    connect( &myVerifyTimer, SIGNAL(timeout()), this, SLOT(onTimeout()));
188
189    reload( );
190}
191
192Options :: ~Options( )
193{
194    clearInfo( );
195}
196
197/***
198****
199***/
200
201void
202Options :: refreshButton( QPushButton * p, const QString& text, int width )
203{
204    if( width <= 0 ) width = p->width( );
205    width -= 15;
206    QFontMetrics fontMetrics( font( ) );
207    QString str = fontMetrics.elidedText( text, Qt::ElideRight, width );
208    p->setText( str );
209}
210
211void
212Options :: refreshFileButton( int width )
213{
214    QString text;
215
216    switch( myAdd.type )
217    {
218        case AddData::FILENAME: text = QFileInfo(myAdd.filename).baseName(); break;
219        case AddData::URL:      text = myAdd.url.toString(); break;
220        case AddData::MAGNET:   text = myAdd.magnet; break;
221        default:                break;
222    }
223
224    refreshButton( myFileButton, text, width );
225}
226
227void
228Options :: refreshDestinationButton( int width )
229{
230    if( myDestinationButton != 0 )
231        refreshButton( myDestinationButton, myDestination.absolutePath(), width );
232}
233
234
235bool
236Options :: eventFilter( QObject * o, QEvent * event )
237{
238    if( o==myFileButton && event->type() == QEvent::Resize )
239    {
240        refreshFileButton( dynamic_cast<QResizeEvent*>(event)->size().width() );
241    }
242
243    if( o==myDestinationButton && event->type() == QEvent::Resize )
244    {
245        refreshDestinationButton( dynamic_cast<QResizeEvent*>(event)->size().width() );
246    }
247
248    return false;
249}
250
251/***
252****
253***/
254
255void
256Options :: clearInfo( )
257{
258    if( myHaveInfo )
259        tr_metainfoFree( &myInfo );
260    myHaveInfo = false;
261    myFiles.clear( );
262}
263
264void
265Options :: reload( )
266{
267    clearInfo( );
268    clearVerify( );
269
270    tr_ctor * ctor = tr_ctorNew( 0 );
271
272    switch( myAdd.type ) {
273        case AddData::MAGNET:   tr_ctorSetMetainfoFromMagnetLink( ctor, myAdd.magnet.toUtf8().constData() ); break;
274        case AddData::FILENAME: tr_ctorSetMetainfoFromFile( ctor, myAdd.filename.toUtf8().constData() ); break;
275        case AddData::METAINFO: tr_ctorSetMetainfo( ctor, (const uint8_t*)myAdd.metainfo.constData(), myAdd.metainfo.size() ); break;
276        default: break;
277    }
278
279    const int err = tr_torrentParse( ctor, &myInfo );
280    myHaveInfo = !err;
281    tr_ctorFree( ctor );
282
283    myTree->clear( );
284    myFiles.clear( );
285    myPriorities.clear( );
286    myWanted.clear( );
287
288    if( myHaveInfo )
289    {
290        myPriorities.insert( 0, myInfo.fileCount, TR_PRI_NORMAL );
291        myWanted.insert( 0, myInfo.fileCount, true );
292
293        for( tr_file_index_t i=0; i<myInfo.fileCount; ++i ) {
294            TrFile file;
295            file.index = i;
296            file.priority = myPriorities[i];
297            file.wanted = myWanted[i];
298            file.size = myInfo.files[i].length;
299            file.have = 0;
300            file.filename = QString::fromUtf8( myInfo.files[i].name );
301            myFiles.append( file );
302        }
303    }
304
305    myTree->update( myFiles );
306}
307
308void
309Options :: onPriorityChanged( const QSet<int>& fileIndices, int priority )
310{
311    foreach( int i, fileIndices )
312        myPriorities[i] = priority;
313}
314
315void
316Options :: onWantedChanged( const QSet<int>& fileIndices, bool isWanted )
317{
318    foreach( int i, fileIndices )
319        myWanted[i] = isWanted;
320}
321
322void
323Options :: onAccepted( )
324{
325    // rpc spec section 3.4 "adding a torrent"
326
327    const int64_t tag = mySession.getUniqueTag( );
328    tr_benc top;
329    tr_bencInitDict( &top, 3 );
330    tr_bencDictAddStr( &top, "method", "torrent-add" );
331    tr_bencDictAddInt( &top, "tag", tag );
332    tr_benc * args( tr_bencDictAddDict( &top, "arguments", 10 ) );
333    QString downloadDir;
334
335    // "download-dir"
336    if( myDestinationButton )
337        downloadDir = myDestination.absolutePath();
338    else
339        downloadDir = myDestinationEdit->text();
340    tr_bencDictAddStr( args, "download-dir", downloadDir.toUtf8().constData() );
341
342    // "metainfo"
343    switch( myAdd.type )
344    {
345        case AddData::MAGNET:
346            tr_bencDictAddStr( args, "filename", myAdd.magnet.toUtf8().constData() );
347            break;
348
349        case AddData::URL:
350            tr_bencDictAddStr( args, "filename", myAdd.url.toString().toUtf8().constData() );
351            break;
352
353        case AddData::FILENAME:
354        case AddData::METAINFO: {
355            const QByteArray b64 = myAdd.toBase64( );
356            tr_bencDictAddRaw( args, "metainfo", b64.constData(), b64.size() );
357            break;
358        }
359
360        default:
361            std::cerr << "unhandled AddData.type: " << myAdd.type << std::endl;
362    }
363
364    // paused
365    tr_bencDictAddBool( args, "paused", !myStartCheck->isChecked( ) );
366
367    // priority
368    const int index = myPriorityCombo->currentIndex( );
369    const int priority = myPriorityCombo->itemData(index).toInt( );
370    tr_bencDictAddInt( args, "bandwidthPriority", priority );
371
372    // files-unwanted
373    int count = myWanted.count( false );
374    if( count > 0 ) {
375        tr_benc * l = tr_bencDictAddList( args, "files-unwanted", count );
376        for( int i=0, n=myWanted.size(); i<n; ++i )
377            if( myWanted.at(i) == false )
378                tr_bencListAddInt( l, i );
379    }
380
381    // priority-low
382    count = myPriorities.count( TR_PRI_LOW );
383    if( count > 0 ) {
384        tr_benc * l = tr_bencDictAddList( args, "priority-low", count );
385        for( int i=0, n=myPriorities.size(); i<n; ++i )
386            if( myPriorities.at(i) == TR_PRI_LOW )
387                tr_bencListAddInt( l, i );
388    }
389
390    // priority-high
391    count = myPriorities.count( TR_PRI_HIGH );
392    if( count > 0 ) {
393        tr_benc * l = tr_bencDictAddList( args, "priority-high", count );
394        for( int i=0, n=myPriorities.size(); i<n; ++i )
395            if( myPriorities.at(i) == TR_PRI_HIGH )
396                tr_bencListAddInt( l, i );
397    }
398
399    // maybe delete the source .torrent
400    FileAdded * fileAdded = new FileAdded( tag, myAdd.readableName() );
401    if( myTrashCheck->isChecked( ) && ( myAdd.type==AddData::FILENAME ) )
402        fileAdded->setFileToDelete( myAdd.filename );
403    connect( &mySession, SIGNAL(executed(int64_t,const QString&, struct tr_benc*)),
404             fileAdded, SLOT(executed(int64_t,const QString&, struct tr_benc*)));
405
406//std::cerr << tr_bencToStr(&top,TR_FMT_JSON,NULL) << std::endl;
407    mySession.exec( &top );
408
409    tr_bencFree( &top );
410    deleteLater( );
411}
412
413void
414Options :: onFilenameClicked( )
415{
416    if( myAdd.type == AddData::FILENAME )
417    {
418        QFileDialog * d = new QFileDialog( this,
419                                           tr( "Open Torrent" ),
420                                           QFileInfo(myAdd.filename).absolutePath(),
421                                           tr( "Torrent Files (*.torrent);;All Files (*.*)" ) );
422        d->setFileMode( QFileDialog::ExistingFile );
423        connect( d, SIGNAL(filesSelected(const QStringList&)), this, SLOT(onFilesSelected(const QStringList&)) );
424        d->show( );
425    }
426}
427
428void
429Options :: onFilesSelected( const QStringList& files )
430{
431    if( files.size() == 1 )
432    {
433        myAdd.set( files.at(0) );
434        refreshFileButton( );
435        reload( );
436    }
437}
438
439void
440Options :: onDestinationClicked( )
441{
442    QFileDialog * d = new QFileDialog( this,
443                                       tr( "Select Destination" ),
444                                       myDestination.absolutePath( ) );
445    d->setFileMode( QFileDialog::Directory );
446    connect( d, SIGNAL(filesSelected(const QStringList&)), this, SLOT(onDestinationsSelected(const QStringList&)) );
447    d->show( );
448}
449
450void
451Options :: onDestinationsSelected( const QStringList& destinations )
452{
453    if( destinations.size() == 1 )
454    {
455        const QString& destination( destinations.first( ) );
456        myDestination.setPath( destination );
457        refreshDestinationButton( );
458    }
459}
460
461/***
462****
463****  VERIFY
464****
465***/
466
467void
468Options :: clearVerify( )
469{
470    myVerifyHash.reset( );
471    myVerifyFile.close( );
472    myVerifyFilePos = 0;
473    myVerifyFlags.clear( );
474    myVerifyFileIndex = 0;
475    myVerifyPieceIndex = 0;
476    myVerifyPiecePos = 0;
477    myVerifyTimer.stop( );
478
479    for( int i=0, n=myFiles.size(); i<n; ++i )
480        myFiles[i].have = 0;
481    myTree->update( myFiles );
482}
483
484void
485Options :: onVerify( )
486{
487    //std::cerr << "starting to verify..." << std::endl;
488    clearVerify( );
489    myVerifyFlags.insert( 0, myInfo.pieceCount, false );
490    myVerifyTimer.setSingleShot( false );
491    myVerifyTimer.start( 0 );
492}
493
494namespace
495{
496    uint64_t getPieceSize( const tr_info * info, tr_piece_index_t pieceIndex )
497    {
498        if( pieceIndex != info->pieceCount - 1 )
499            return info->pieceSize;
500        return info->totalSize % info->pieceSize;
501    }
502}
503
504void
505Options :: onTimeout( )
506{
507    const tr_file * file = &myInfo.files[myVerifyFileIndex];
508
509    if( !myVerifyFilePos && !myVerifyFile.isOpen( ) )
510    {
511        const QFileInfo fileInfo( myDestination, QString::fromUtf8( file->name ) );
512        myVerifyFile.setFileName( fileInfo.absoluteFilePath( ) );
513        //std::cerr << "opening file" << qPrintable(fileInfo.absoluteFilePath()) << std::endl;
514        myVerifyFile.open( QIODevice::ReadOnly );
515    }
516
517    int64_t leftInPiece = getPieceSize( &myInfo, myVerifyPieceIndex ) - myVerifyPiecePos;
518    int64_t leftInFile = file->length - myVerifyFilePos;
519    int64_t bytesThisPass = std::min( leftInFile, leftInPiece );
520    bytesThisPass = std::min( bytesThisPass, (int64_t)sizeof( myVerifyBuf ) );
521
522    if( myVerifyFile.isOpen() && myVerifyFile.seek( myVerifyFilePos ) ) {
523        int64_t numRead = myVerifyFile.read( myVerifyBuf, bytesThisPass );
524        if( numRead == bytesThisPass )
525            myVerifyHash.addData( myVerifyBuf, numRead );
526    }
527
528    leftInPiece -= bytesThisPass;
529    leftInFile -= bytesThisPass;
530    myVerifyPiecePos += bytesThisPass;
531    myVerifyFilePos += bytesThisPass;
532
533    myVerifyBins[myVerifyFileIndex] += bytesThisPass;
534
535    if( leftInPiece == 0 )
536    {
537        const QByteArray result( myVerifyHash.result( ) );
538        const bool matches = !memcmp( result.constData(),
539                                      myInfo.pieces[myVerifyPieceIndex].hash,
540                                      SHA_DIGEST_LENGTH );
541        myVerifyFlags[myVerifyPieceIndex] = matches;
542        myVerifyPiecePos = 0;
543        ++myVerifyPieceIndex;
544        myVerifyHash.reset( );
545
546        FileList changedFiles;
547        if( matches ) {
548            mybins_t::const_iterator i;
549            for( i=myVerifyBins.begin(); i!=myVerifyBins.end(); ++i ) {
550                TrFile& f( myFiles[i.key( )] );
551                f.have += i.value( );
552                changedFiles.append( f );
553            }
554        }
555        myTree->update( changedFiles );
556        myVerifyBins.clear( );
557    }
558
559    if( leftInFile == 0 )
560    {
561        //std::cerr << "closing file" << std::endl;
562        myVerifyFile.close( );
563        ++myVerifyFileIndex;
564        myVerifyFilePos = 0;
565    }
566
567    bool done = myVerifyPieceIndex >= myInfo.pieceCount;
568    if( done )
569    {
570        uint64_t have = 0;
571        foreach( const TrFile& f, myFiles )
572            have += f.have;
573
574        if( !have ) // everything failed
575        {
576            // did the user accidentally specify the child directory instead of the parent?
577            const QStringList tokens = QString(file->name).split('/');
578            if( !tokens.empty() && myDestination.dirName()==tokens.at(0) )
579            {
580                // move up one directory and try again
581                myDestination.cdUp( );
582                refreshDestinationButton( -1 );
583                onVerify( );
584                done = false;
585            }
586        }
587    }
588
589    if( done )
590        myVerifyTimer.stop( );
591}
Note: See TracBrowser for help on using the repository browser.