source: trunk/qt/app.cc @ 14396

Last change on this file since 14396 was 14396, checked in by mikedld, 8 years ago

#4050: Use TRANSLATIONS_DIR macro as a hint for Qt client translation files location

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