source: trunk/qt/options.cc @ 13381

Last change on this file since 13381 was 13381, checked in by jordan, 10 years ago

(trunk qt) #4235 "allow transmission-qt to specify download dir for remote sessions" -- patch added from taem

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