source: trunk/qt/app.cc @ 13724

Last change on this file since 13724 was 13724, checked in by jordan, 8 years ago

(trunk, qt) #5060 'start minimized to tray option' -- added.

  • Property svn:keywords set to Date Rev Author Id
File size: 17.8 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: app.cc 13724 2012-12-30 22:51:55Z jordan $
11 */
12
13#include <cassert>
14#include <ctime>
15#include <iostream>
16
17#include <QDBusConnection>
18#include <QDBusConnectionInterface>
19#include <QDBusError>
20#include <QDBusMessage>
21#include <QDialogButtonBox>
22#include <QIcon>
23#include <QLabel>
24#include <QLibraryInfo>
25#include <QRect>
26
27#include <libtransmission/transmission.h>
28#include <libtransmission/tr-getopt.h>
29#include <libtransmission/utils.h>
30#include <libtransmission/version.h>
31
32#include "add-data.h"
33#include "app.h"
34#include "dbus-adaptor.h"
35#include "formatter.h"
36#include "mainwin.h"
37#include "options.h"
38#include "prefs.h"
39#include "session.h"
40#include "session-dialog.h"
41#include "torrent-model.h"
42#include "utils.h"
43#include "watchdir.h"
44
45namespace
46{
47    const QString DBUS_SERVICE     = QString::fromAscii( "com.transmissionbt.Transmission"  );
48    const QString DBUS_OBJECT_PATH = QString::fromAscii( "/com/transmissionbt/Transmission" );
49    const QString DBUS_INTERFACE   = QString::fromAscii( "com.transmissionbt.Transmission"  );
50
51    const char * MY_READABLE_NAME( "transmission-qt" );
52
53    const tr_option opts[] =
54    {
55        { 'g', "config-dir", "Where to look for configuration files", "g", 1, "<path>" },
56        { 'm', "minimized",  "Start minimized in system tray", "m", 0, NULL },
57        { 'p', "port",  "Port to use when connecting to an existing session", "p", 1, "<port>" },
58        { 'r', "remote",  "Connect to an existing session at the specified hostname", "r", 1, "<host>" },
59        { 'u', "username", "Username to use when connecting to an existing session", "u", 1, "<username>" },
60        { 'v', "version", "Show version number and exit", "v", 0, NULL },
61        { 'w', "password", "Password to use when connecting to an existing session", "w", 1, "<password>" },
62        { 0, NULL, NULL, NULL, 0, NULL }
63    };
64
65    const char*
66    getUsage( void )
67    {
68        return "Usage:\n"
69               "  transmission [OPTIONS...] [torrent files]";
70    }
71
72    void
73    showUsage( void )
74    {
75        tr_getopt_usage( MY_READABLE_NAME, getUsage( ), opts );
76        exit( 0 );
77    }
78
79    enum
80    {
81        STATS_REFRESH_INTERVAL_MSEC = 3000,
82        SESSION_REFRESH_INTERVAL_MSEC = 3000,
83        MODEL_REFRESH_INTERVAL_MSEC = 3000
84    };
85}
86
87MyApp :: MyApp( int& argc, char ** argv ):
88    QApplication( argc, argv ),
89    myLastFullUpdateTime( 0 )
90{
91    const QString MY_CONFIG_NAME = QString::fromAscii( "transmission" );
92
93    setApplicationName( MY_CONFIG_NAME );
94
95    // install the qt translator
96    qtTranslator.load( "qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
97    installTranslator( &qtTranslator );
98
99    // install the transmission translator
100    appTranslator.load( QString(MY_CONFIG_NAME) + "_" + QLocale::system().name(), QCoreApplication::applicationDirPath() + "/translations" );
101    installTranslator( &appTranslator );
102
103    Formatter::initUnits( );
104
105    // set the default icon
106    QIcon icon;
107    QList<int> sizes;
108    sizes << 16 << 22 << 24 << 32 << 48;
109    foreach( int size, sizes )
110        icon.addPixmap( QPixmap( QString::fromAscii(":/icons/transmission-%1.png" ).arg(size) ) );
111    setWindowIcon( icon );
112
113    // parse the command-line arguments
114    int c;
115    bool minimized = false;
116    const char * optarg;
117    const char * host = 0;
118    const char * port = 0;
119    const char * username = 0;
120    const char * password = 0;
121    const char * configDir = 0;
122    QStringList filenames;
123    while( ( c = tr_getopt( getUsage( ), argc, (const char**)argv, opts, &optarg ) ) ) {
124        switch( c ) {
125            case 'g': configDir = optarg; break;
126            case 'p': port = optarg; break;
127            case 'r': host = optarg; break;
128            case 'u': username = optarg; break;
129            case 'w': password = optarg; break;
130            case 'm': minimized = true; break;
131            case 'v': std::cerr << MY_READABLE_NAME << ' ' << LONG_VERSION_STRING << std::endl; ::exit( 0 ); break;
132            case TR_OPT_ERR: Utils::toStderr( QObject::tr( "Invalid option" ) ); showUsage( ); break;
133            default:         filenames.append( optarg ); break;
134        }
135    }
136
137    // set the fallback config dir
138    if( configDir == 0 )
139        configDir = tr_getDefaultConfigDir( "transmission" );
140
141    // ensure our config directory exists
142    QDir dir( configDir );
143    if( !dir.exists() )
144        dir.mkpath( configDir );
145
146    // is this the first time we've run transmission?
147    const bool firstTime = !QFile(QDir(configDir).absoluteFilePath("settings.json")).exists();
148
149    // initialize the prefs
150    myPrefs = new Prefs ( configDir );
151    if( host != 0 )
152        myPrefs->set( Prefs::SESSION_REMOTE_HOST, host );
153    if( port != 0 )
154        myPrefs->set( Prefs::SESSION_REMOTE_PORT, port );
155    if( username != 0 )
156        myPrefs->set( Prefs::SESSION_REMOTE_USERNAME, username );
157    if( password != 0 )
158        myPrefs->set( Prefs::SESSION_REMOTE_PASSWORD, password );
159    if( ( host != 0 ) || ( port != 0 ) || ( username != 0 ) || ( password != 0 ) )
160        myPrefs->set( Prefs::SESSION_IS_REMOTE, true );
161    if ( myPrefs->getBool( Prefs::START_MINIMIZED) )
162        minimized = true;
163
164    // start as minimized only if the system tray present
165    if ( !myPrefs->getBool( Prefs::SHOW_TRAY_ICON ) )
166      minimized = false;
167
168    mySession = new Session( configDir, *myPrefs );
169    myModel = new TorrentModel( *myPrefs );
170    myWindow = new TrMainWindow( *mySession, *myPrefs, *myModel, minimized );
171    myWatchDir = new WatchDir( *myModel );
172
173    // when the session gets torrent info, update the model
174    connect( mySession, SIGNAL(torrentsUpdated(tr_variant*,bool)), myModel, SLOT(updateTorrents(tr_variant*,bool)) );
175    connect( mySession, SIGNAL(torrentsUpdated(tr_variant*,bool)), myWindow, SLOT(refreshActionSensitivity()) );
176    connect( mySession, SIGNAL(torrentsRemoved(tr_variant*)), myModel, SLOT(removeTorrents(tr_variant*)) );
177    // when the session source gets changed, request a full refresh
178    connect( mySession, SIGNAL(sourceChanged()), this, SLOT(onSessionSourceChanged()) );
179    // when the model sees a torrent for the first time, ask the session for full info on it
180    connect( myModel, SIGNAL(torrentsAdded(QSet<int>)), mySession, SLOT(initTorrents(QSet<int>)) );
181    connect( myModel, SIGNAL(torrentsAdded(QSet<int>)), this, SLOT(onTorrentsAdded(QSet<int>)) );
182
183    mySession->initTorrents( );
184    mySession->refreshSessionStats( );
185
186    // when torrents are added to the watch directory, tell the session
187    connect( myWatchDir, SIGNAL(torrentFileAdded(QString)), this, SLOT(addTorrent(QString)) );
188
189    // init from preferences
190    QList<int> initKeys;
191    initKeys << Prefs::DIR_WATCH;
192    foreach( int key, initKeys )
193        refreshPref( key );
194    connect( myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(const int)) );
195
196    QTimer * timer = &myModelTimer;
197    connect( timer, SIGNAL(timeout()), this, SLOT(refreshTorrents()) );
198    timer->setSingleShot( false );
199    timer->setInterval( MODEL_REFRESH_INTERVAL_MSEC );
200    timer->start( );
201
202    timer = &myStatsTimer;
203    connect( timer, SIGNAL(timeout()), mySession, SLOT(refreshSessionStats()) );
204    timer->setSingleShot( false );
205    timer->setInterval( STATS_REFRESH_INTERVAL_MSEC );
206    timer->start( );
207
208    timer = &mySessionTimer;
209    connect( timer, SIGNAL(timeout()), mySession, SLOT(refreshSessionInfo()) );
210    timer->setSingleShot( false );
211    timer->setInterval( SESSION_REFRESH_INTERVAL_MSEC );
212    timer->start( );
213
214    maybeUpdateBlocklist( );
215
216    if( !firstTime )
217        mySession->restart( );
218    else {
219        QDialog * d = new SessionDialog( *mySession, *myPrefs, myWindow );
220        d->show( );
221    }
222
223    if( !myPrefs->getBool( Prefs::USER_HAS_GIVEN_INFORMED_CONSENT ))
224    {
225        QDialog * dialog = new QDialog( myWindow );
226        dialog->setModal( true );
227        QVBoxLayout * v = new QVBoxLayout( dialog );
228        QLabel * l = new QLabel( tr( "Transmission is a file-sharing program.  When you run a torrent, its data will be made available to others by means of upload.  You and you alone are fully responsible for exercising proper judgement and abiding by your local laws." ) );
229        l->setWordWrap( true );
230        v->addWidget( l );
231        QDialogButtonBox * box = new QDialogButtonBox;
232        box->addButton( new QPushButton( tr( "&Cancel" ) ), QDialogButtonBox::RejectRole );
233        QPushButton * agree = new QPushButton( tr( "I &Agree" ) );
234        agree->setDefault( true );
235        box->addButton( agree, QDialogButtonBox::AcceptRole );
236        box->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed );
237        box->setOrientation( Qt::Horizontal );
238        v->addWidget( box );
239        connect( box, SIGNAL(rejected()), this, SLOT(quit()) );
240        connect( box, SIGNAL(accepted()), dialog, SLOT(deleteLater()) );
241        connect( box, SIGNAL(accepted()), this, SLOT(consentGiven()) );
242        dialog->show();
243    }
244
245    for( QStringList::const_iterator it=filenames.begin(), end=filenames.end(); it!=end; ++it )
246        addTorrent( *it );
247
248    // register as the dbus handler for Transmission
249    new TrDBusAdaptor( this );
250    QDBusConnection bus = QDBusConnection::sessionBus();
251    if( !bus.registerService( DBUS_SERVICE ) )
252        std::cerr << "couldn't register " << qPrintable(DBUS_SERVICE) << std::endl;
253    if( !bus.registerObject( DBUS_OBJECT_PATH, this ) )
254        std::cerr << "couldn't register " << qPrintable(DBUS_OBJECT_PATH) << std::endl;
255}
256
257/* these functions are for popping up desktop notifications */
258
259void
260MyApp :: onTorrentsAdded( QSet<int> torrents )
261{
262    if( !myPrefs->getBool( Prefs::SHOW_DESKTOP_NOTIFICATION ) )
263        return;
264
265    foreach( int id, torrents )
266    {
267        Torrent * tor = myModel->getTorrentFromId( id );
268
269        if( tor->name().isEmpty( ) ) // wait until the torrent's INFO fields are loaded
270            connect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onNewTorrentChanged(int)) );
271        else {
272            onNewTorrentChanged( id );
273            if( !tor->isSeed( ) )
274                connect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) );
275        }
276    }
277}
278
279void
280MyApp :: onTorrentCompleted( int id )
281{
282    Torrent * tor = myModel->getTorrentFromId( id );
283
284    if( tor && !tor->name().isEmpty() )
285    {
286        notify( tr( "Torrent Completed" ), tor->name( ) );
287
288        disconnect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) );
289    }
290}
291
292void
293MyApp :: onNewTorrentChanged( int id )
294{
295    Torrent * tor = myModel->getTorrentFromId( id );
296
297    if( tor && !tor->name().isEmpty() )
298    {
299        const int age_secs = tor->dateAdded().secsTo(QDateTime::currentDateTime());
300        if( age_secs < 30 )
301            notify( tr( "Torrent Added" ), tor->name( ) );
302
303        disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onNewTorrentChanged(int)) );
304
305        if( !tor->isSeed( ) )
306            connect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) );
307    }
308}
309
310/***
311****
312***/
313
314void
315MyApp :: consentGiven( )
316{
317    myPrefs->set<bool>( Prefs::USER_HAS_GIVEN_INFORMED_CONSENT, true );
318}
319
320MyApp :: ~MyApp( )
321{
322    const QRect mainwinRect( myWindow->geometry( ) );
323    delete myWatchDir;
324    delete myWindow;
325    delete myModel;
326    delete mySession;
327
328    myPrefs->set( Prefs :: MAIN_WINDOW_HEIGHT, std::max( 100, mainwinRect.height( ) ) );
329    myPrefs->set( Prefs :: MAIN_WINDOW_WIDTH, std::max( 100, mainwinRect.width( ) ) );
330    myPrefs->set( Prefs :: MAIN_WINDOW_X, mainwinRect.x( ) );
331    myPrefs->set( Prefs :: MAIN_WINDOW_Y, mainwinRect.y( ) );
332    delete myPrefs;
333}
334
335/***
336****
337***/
338
339void
340MyApp :: refreshPref( int key )
341{
342    switch( key )
343    {
344        case Prefs :: BLOCKLIST_UPDATES_ENABLED:
345            maybeUpdateBlocklist( );
346            break;
347
348        case Prefs :: DIR_WATCH:
349        case Prefs :: DIR_WATCH_ENABLED: {
350            const QString path( myPrefs->getString( Prefs::DIR_WATCH ) );
351            const bool isEnabled( myPrefs->getBool( Prefs::DIR_WATCH_ENABLED ) );
352            myWatchDir->setPath( path, isEnabled );
353            break;
354        }
355
356        default:
357            break;
358    }
359}
360
361void
362MyApp :: maybeUpdateBlocklist( )
363{
364    if( !myPrefs->getBool( Prefs :: BLOCKLIST_UPDATES_ENABLED ) )
365        return;
366
367     const QDateTime lastUpdatedAt = myPrefs->getDateTime( Prefs :: BLOCKLIST_DATE );
368     const QDateTime nextUpdateAt = lastUpdatedAt.addDays( 7 );
369     const QDateTime now = QDateTime::currentDateTime( );
370     if( now < nextUpdateAt )
371     {
372         mySession->updateBlocklist( );
373         myPrefs->set( Prefs :: BLOCKLIST_DATE, now );
374     }
375}
376
377void
378MyApp :: onSessionSourceChanged( )
379{
380    mySession->initTorrents( );
381    mySession->refreshSessionStats( );
382    mySession->refreshSessionInfo( );
383}
384
385void
386MyApp :: refreshTorrents( )
387{
388    // usually we just poll the torrents that have shown recent activity,
389    // but we also periodically ask for updates on the others to ensure
390    // nothing's falling through the cracks.
391    const time_t now = time( NULL );
392    if( myLastFullUpdateTime + 60 >= now )
393        mySession->refreshActiveTorrents( );
394    else {
395        myLastFullUpdateTime = now;
396        mySession->refreshAllTorrents( );
397    }
398}
399
400/***
401****
402***/
403
404void
405MyApp :: addTorrent( const QString& key )
406{
407    const AddData addme( key );
408
409    if( addme.type != addme.NONE )
410        addTorrent( addme );
411}
412
413void
414MyApp :: addTorrent( const AddData& addme )
415{
416    if( !myPrefs->getBool( Prefs :: OPTIONS_PROMPT ) )
417    {
418        mySession->addTorrent( addme );
419    }
420    else if( addme.type == addme.URL )
421    {
422        myWindow->openURL( addme.url.toString( ) );
423    }
424    else if( addme.type == addme.MAGNET )
425    {
426        myWindow->openURL( addme.magnet );
427    }
428    else
429    {
430        Options * o = new Options( *mySession, *myPrefs, addme, myWindow );
431        o->show( );
432    }
433
434    raise( );
435}
436
437/***
438****
439***/
440
441void
442MyApp :: raise( )
443{
444    QApplication :: alert ( myWindow );
445}
446
447bool
448MyApp :: notify( const QString& title, const QString& body ) const
449{
450    const QString dbusServiceName   = QString::fromAscii( "org.freedesktop.Notifications" );
451    const QString dbusInterfaceName = QString::fromAscii( "org.freedesktop.Notifications" );
452    const QString dbusPath          = QString::fromAscii( "/org/freedesktop/Notifications" );
453
454    QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QString::fromAscii("Notify"));
455    QList<QVariant> args;
456    args.append( QString::fromAscii( "Transmission" ) ); // app_name
457    args.append( 0U );                                   // replaces_id
458    args.append( QString::fromAscii( "transmission" ) ); // icon
459    args.append( title );                                // summary
460    args.append( body );                                 // body
461    args.append( QStringList( ) );                       // actions - unused for plain passive popups
462    args.append( QVariantMap( ) );                       // hints - unused atm
463    args.append( int32_t(-1) );                          // use the default timeout period
464    m.setArguments( args );
465    QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m);
466    //std::cerr << qPrintable(replyMsg.errorName()) << std::endl;
467    //std::cerr << qPrintable(replyMsg.errorMessage()) << std::endl;
468    return (replyMsg.type() == QDBusMessage::ReplyMessage) && !replyMsg.arguments().isEmpty();
469}
470
471/***
472****
473***/
474
475int
476main( int argc, char * argv[] )
477{
478std::cerr << "sizeof double " << sizeof(double) << std::endl;
479std::cerr << "sizeof Speed " << sizeof(Speed) << std::endl;
480std::cerr << "sizeof int " << sizeof(int) << std::endl;
481std::cerr << "sizeof bool " << sizeof(bool) << std::endl;
482std::cerr << "sizeof uint64_t " << sizeof(uint64_t) << std::endl;
483std::cerr << "sizeof QString is " << sizeof(QString) << std::endl;
484std::cerr << "sizeof TrFile is " << sizeof(struct TrFile) << std::endl;
485std::cerr << "sizeof Peer is " << sizeof(Peer) << std::endl;
486std::cerr << "sizeof TrackerStat is " << sizeof(TrackerStat) << std::endl;
487
488    // find .torrents, URLs, magnet links, etc in the command-line args
489    int c;
490    QStringList addme;
491    const char * optarg;
492    char ** argvv = argv;
493    while( ( c = tr_getopt( getUsage( ), argc, (const char **)argvv, opts, &optarg ) ) )
494        if( c == TR_OPT_UNK )
495            addme.append( optarg );
496
497    // try to delegate the work to an existing copy of Transmission
498    // before starting ourselves...
499    bool delegated = false;
500    QDBusConnection bus = QDBusConnection::sessionBus();
501    for( int i=0, n=addme.size(); i<n; ++i )
502    {
503        QDBusMessage request = QDBusMessage::createMethodCall( DBUS_SERVICE,
504                                                               DBUS_OBJECT_PATH,
505                                                               DBUS_INTERFACE,
506                                                               QString::fromAscii("AddMetainfo") );
507        QList<QVariant> arguments;
508        AddData a( addme[i] );
509        switch( a.type ) {
510            case AddData::URL:      arguments.push_back( a.url.toString( ) ); break;
511            case AddData::MAGNET:   arguments.push_back( a.magnet ); break;
512            case AddData::FILENAME: arguments.push_back( a.toBase64().constData() ); break;
513            case AddData::METAINFO: arguments.push_back( a.toBase64().constData() ); break;
514            default:                break;
515        }
516        request.setArguments( arguments );
517
518        QDBusMessage response = bus.call( request );
519        //std::cerr << qPrintable(response.errorName()) << std::endl;
520        //std::cerr << qPrintable(response.errorMessage()) << std::endl;
521        arguments = response.arguments( );
522        delegated |= (arguments.size()==1) && arguments[0].toBool();
523    }
524
525    if( delegated )
526        return 0;
527
528    tr_optind = 1;
529    MyApp app( argc, argv );
530    return app.exec( );
531}
Note: See TracBrowser for help on using the repository browser.