source: trunk/qt/Application.cc @ 14621

Last change on this file since 14621 was 14621, checked in by mikedld, 7 years ago

Refactor DBus IPC to allow for further extensibility

  • Property svn:keywords set to Date Rev Author Id
File size: 16.7 KB
Line 
1/*
2 * This file Copyright (C) 2009-2015 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: Application.cc 14621 2015-12-16 17:57:05Z mikedld $
8 */
9
10#include <ctime>
11#include <iostream>
12
13#include <QDBusConnection>
14#include <QDBusMessage>
15#include <QDBusReply>
16#include <QIcon>
17#include <QLibraryInfo>
18#include <QMessageBox>
19#include <QProcess>
20#include <QRect>
21#include <QSystemTrayIcon>
22
23#include <libtransmission/transmission.h>
24#include <libtransmission/tr-getopt.h>
25#include <libtransmission/utils.h>
26#include <libtransmission/version.h>
27
28#include "AddData.h"
29#include "Application.h"
30#include "DBusInteropHelper.h"
31#include "Formatter.h"
32#include "MainWindow.h"
33#include "OptionsDialog.h"
34#include "Prefs.h"
35#include "Session.h"
36#include "TorrentModel.h"
37#include "WatchDir.h"
38
39namespace
40{
41  const QLatin1String MY_CONFIG_NAME ("transmission");
42  const QLatin1String MY_READABLE_NAME ("transmission-qt");
43
44  const tr_option opts[] =
45  {
46    { 'g', "config-dir", "Where to look for configuration files", "g", 1, "<path>" },
47    { 'm', "minimized",  "Start minimized in system tray", "m", 0, NULL },
48    { 'p', "port",  "Port to use when connecting to an existing session", "p", 1, "<port>" },
49    { 'r', "remote",  "Connect to an existing session at the specified hostname", "r", 1, "<host>" },
50    { 'u', "username", "Username to use when connecting to an existing session", "u", 1, "<username>" },
51    { 'v', "version", "Show version number and exit", "v", 0, NULL },
52    { 'w', "password", "Password to use when connecting to an existing session", "w", 1, "<password>" },
53    { 0, NULL, NULL, NULL, 0, NULL }
54  };
55
56  const char*
57  getUsage ()
58  {
59    return "Usage:\n"
60           "  transmission [OPTIONS...] [torrent files]";
61  }
62
63  enum
64  {
65    STATS_REFRESH_INTERVAL_MSEC   = 3000,
66    SESSION_REFRESH_INTERVAL_MSEC = 3000,
67    MODEL_REFRESH_INTERVAL_MSEC   = 3000
68  };
69
70  bool
71  loadTranslation (QTranslator& translator, const QString& name, const QString& localeName,
72                   const QStringList& searchDirectories)
73  {
74    const QString filename = name + QLatin1Char ('_') + localeName;
75    for (const QString& directory: searchDirectories)
76    {
77      if (translator.load (filename, directory))
78        return true;
79    }
80
81    return false;
82  }
83}
84
85Application::Application (int& argc, char ** argv):
86  QApplication (argc, argv),
87  myPrefs(nullptr),
88  mySession(nullptr),
89  myModel(nullptr),
90  myWindow(nullptr),
91  myWatchDir(nullptr),
92  myLastFullUpdateTime (0)
93{
94  setApplicationName (MY_CONFIG_NAME);
95  loadTranslations ();
96
97  Formatter::initUnits ();
98
99#if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0)
100  setAttribute (Qt::AA_UseHighDpiPixmaps);
101#endif
102
103#if defined (_WIN32) || defined (__APPLE__)
104  if (QIcon::themeName ().isEmpty ())
105    QIcon::setThemeName (QLatin1String ("Faenza"));
106#endif
107
108  // set the default icon
109  QIcon icon = QIcon::fromTheme (QLatin1String ("transmission"));
110  if (icon.isNull ())
111    {
112      QList<int> sizes;
113      sizes << 16 << 22 << 24 << 32 << 48 << 64 << 72 << 96 << 128 << 192 << 256;
114      for (const int size: sizes)
115        icon.addPixmap (QPixmap (QString::fromLatin1 (":/icons/transmission-%1.png").arg (size)));
116    }
117  setWindowIcon (icon);
118
119#ifdef __APPLE__
120  setAttribute (Qt::AA_DontShowIconsInMenus);
121#endif
122
123  // parse the command-line arguments
124  int c;
125  bool minimized = false;
126  const char * optarg;
127  QString host;
128  QString port;
129  QString username;
130  QString password;
131  QString configDir;
132  QStringList filenames;
133  while ((c = tr_getopt (getUsage(), argc, const_cast<const char**> (argv), opts, &optarg)))
134    {
135      switch (c)
136        {
137          case 'g': configDir = QString::fromUtf8 (optarg); break;
138          case 'p': port = QString::fromUtf8 (optarg); break;
139          case 'r': host = QString::fromUtf8 (optarg); break;
140          case 'u': username = QString::fromUtf8 (optarg); break;
141          case 'w': password = QString::fromUtf8 (optarg); break;
142          case 'm': minimized = true; break;
143          case 'v':
144            std::cerr << MY_READABLE_NAME.latin1 () << ' ' << LONG_VERSION_STRING << std::endl;
145            quitLater ();
146            return;
147          case TR_OPT_ERR:
148            std::cerr << qPrintable(QObject::tr ("Invalid option")) << std::endl;
149            tr_getopt_usage (MY_READABLE_NAME.latin1 (), getUsage (), opts);
150            quitLater ();
151            return;
152          default:
153            filenames.append (QString::fromUtf8 (optarg));
154            break;
155        }
156    }
157
158  // try to delegate the work to an existing copy of Transmission
159  // before starting ourselves...
160  DBusInteropHelper interopClient;
161  if (interopClient.isConnected ())
162    {
163      bool delegated = false;
164      for (const QString& filename: filenames)
165        {
166          QString metainfo;
167
168          AddData a (filename);
169          switch (a.type)
170            {
171              case AddData::URL:      metainfo = a.url.toString (); break;
172              case AddData::MAGNET:   metainfo = a.magnet; break;
173              case AddData::FILENAME: metainfo = QString::fromLatin1 (a.toBase64 ()); break;
174              case AddData::METAINFO: metainfo = QString::fromLatin1 (a.toBase64 ()); break;
175              default:                break;
176            }
177
178          if (metainfo.isEmpty ())
179            continue;
180
181          const QVariant result = interopClient.addMetainfo (metainfo);
182          if (result.isValid () && result.toBool ())
183            delegated = true;
184        }
185
186      if (delegated)
187        {
188          quitLater ();
189          return;
190        }
191    }
192
193  // set the fallback config dir
194  if (configDir.isNull ())
195    configDir = QString::fromUtf8 (tr_getDefaultConfigDir ("transmission"));
196
197  // ensure our config directory exists
198  QDir dir (configDir);
199  if (!dir.exists ())
200    dir.mkpath (configDir);
201
202  // is this the first time we've run transmission?
203  const bool firstTime = !dir.exists (QLatin1String ("settings.json"));
204
205  // initialize the prefs
206  myPrefs = new Prefs (configDir);
207  if (!host.isNull ())
208    myPrefs->set (Prefs::SESSION_REMOTE_HOST, host);
209  if (!port.isNull ())
210    myPrefs->set (Prefs::SESSION_REMOTE_PORT, port.toUInt ());
211  if (!username.isNull ())
212    myPrefs->set (Prefs::SESSION_REMOTE_USERNAME, username);
213  if (!password.isNull ())
214    myPrefs->set (Prefs::SESSION_REMOTE_PASSWORD, password);
215  if (!host.isNull () || !port.isNull () || !username.isNull () || !password.isNull ())
216    myPrefs->set (Prefs::SESSION_IS_REMOTE, true);
217  if (myPrefs->getBool (Prefs::START_MINIMIZED))
218    minimized = true;
219
220  // start as minimized only if the system tray present
221  if (!myPrefs->getBool (Prefs::SHOW_TRAY_ICON))
222    minimized = false;
223
224  mySession = new Session (configDir, *myPrefs);
225  myModel = new TorrentModel (*myPrefs);
226  myWindow = new MainWindow (*mySession, *myPrefs, *myModel, minimized);
227  myWatchDir = new WatchDir (*myModel);
228
229  // when the session gets torrent info, update the model
230  connect (mySession, SIGNAL (torrentsUpdated (tr_variant*,bool)), myModel, SLOT (updateTorrents (tr_variant*,bool)));
231  connect (mySession, SIGNAL (torrentsUpdated (tr_variant*,bool)), myWindow, SLOT (refreshActionSensitivity ()));
232  connect (mySession, SIGNAL (torrentsRemoved (tr_variant*)), myModel, SLOT (removeTorrents (tr_variant*)));
233  // when the session source gets changed, request a full refresh
234  connect (mySession, SIGNAL (sourceChanged ()), this, SLOT (onSessionSourceChanged ()));
235  // when the model sees a torrent for the first time, ask the session for full info on it
236  connect (myModel, SIGNAL (torrentsAdded (QSet<int>)), mySession, SLOT (initTorrents (QSet<int>)));
237  connect (myModel, SIGNAL (torrentsAdded (QSet<int>)), this, SLOT (onTorrentsAdded (QSet<int>)));
238
239  mySession->initTorrents ();
240  mySession->refreshSessionStats ();
241
242  // when torrents are added to the watch directory, tell the session
243  connect (myWatchDir, SIGNAL (torrentFileAdded (QString)), this, SLOT (addTorrent (QString)));
244
245  // init from preferences
246  QList<int> initKeys;
247  initKeys << Prefs::DIR_WATCH;
248  for (const int key: initKeys)
249    refreshPref (key);
250  connect (myPrefs, SIGNAL (changed (int)), this, SLOT (refreshPref (const int)));
251
252  QTimer * timer = &myModelTimer;
253  connect (timer, SIGNAL (timeout ()), this, SLOT (refreshTorrents ()));
254  timer->setSingleShot (false);
255  timer->setInterval (MODEL_REFRESH_INTERVAL_MSEC);
256  timer->start ();
257
258  timer = &myStatsTimer;
259  connect (timer, SIGNAL (timeout ()), mySession, SLOT (refreshSessionStats ()));
260  timer->setSingleShot (false);
261  timer->setInterval (STATS_REFRESH_INTERVAL_MSEC);
262  timer->start ();
263
264  timer = &mySessionTimer;
265  connect (timer, SIGNAL (timeout ()), mySession, SLOT (refreshSessionInfo ()));
266  timer->setSingleShot (false);
267  timer->setInterval (SESSION_REFRESH_INTERVAL_MSEC);
268  timer->start ();
269
270  maybeUpdateBlocklist ();
271
272  if (!firstTime)
273    mySession->restart ();
274  else
275    myWindow->openSession ();
276
277  if (!myPrefs->getBool (Prefs::USER_HAS_GIVEN_INFORMED_CONSENT))
278    {
279      QMessageBox * dialog = new QMessageBox (QMessageBox::Information, QString (),
280                                              tr ("<b>Transmission is a file sharing program.</b>"),
281                                              QMessageBox::Ok | QMessageBox::Cancel, myWindow);
282      dialog->setInformativeText (tr ("When you run a torrent, its data will be made available to others by means of upload. "
283                                      "Any content you share is your sole responsibility."));
284      dialog->button (QMessageBox::Ok)->setText (tr ("I &Agree"));
285      dialog->setDefaultButton (QMessageBox::Ok);
286      dialog->setModal (true);
287
288      connect (dialog, SIGNAL (finished (int)), this, SLOT (consentGiven (int)));
289
290      dialog->setAttribute (Qt::WA_DeleteOnClose);
291      dialog->show ();
292    }
293
294  for (const QString& filename: filenames)
295    addTorrent (filename);
296
297  DBusInteropHelper::registerObject (this);
298}
299
300void
301Application::loadTranslations ()
302{
303  const QStringList qtQmDirs = QStringList () <<
304    QLibraryInfo::location (QLibraryInfo::TranslationsPath) <<
305#ifdef TRANSLATIONS_DIR
306    QString::fromUtf8 (TRANSLATIONS_DIR) <<
307#endif
308    (applicationDirPath () + QLatin1String ("/translations"));
309
310  const QStringList appQmDirs = QStringList () <<
311#ifdef TRANSLATIONS_DIR
312    QString::fromUtf8 (TRANSLATIONS_DIR) <<
313#endif
314    (applicationDirPath () + QLatin1String ("/translations"));
315
316  QString localeName = QLocale ().name ();
317
318  if (!loadTranslation (myAppTranslator, MY_CONFIG_NAME, localeName, appQmDirs))
319    {
320      localeName = QLatin1String ("en");
321      loadTranslation (myAppTranslator, MY_CONFIG_NAME, localeName, appQmDirs);
322    }
323
324  if (loadTranslation (myQtTranslator, QLatin1String ("qt"), localeName, qtQmDirs))
325    installTranslator (&myQtTranslator);
326  installTranslator (&myAppTranslator);
327}
328
329void
330Application::quitLater ()
331{
332  QTimer::singleShot (0, this, SLOT (quit ()));
333}
334
335/* these functions are for popping up desktop notifications */
336
337void
338Application::onTorrentsAdded (const QSet<int>& torrents)
339{
340  if (!myPrefs->getBool (Prefs::SHOW_NOTIFICATION_ON_ADD))
341    return;
342
343  for (const 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
362Application::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          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
385Application::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
407Application::consentGiven (int result)
408{
409  if (result == QMessageBox::Ok)
410    myPrefs->set<bool> (Prefs::USER_HAS_GIVEN_INFORMED_CONSENT, true);
411  else
412    quit ();
413}
414
415Application::~Application ()
416{
417  if (myPrefs != nullptr && myWindow != nullptr)
418    {
419      const QRect mainwinRect (myWindow->geometry ());
420      myPrefs->set (Prefs::MAIN_WINDOW_HEIGHT, std::max (100, mainwinRect.height ()));
421      myPrefs->set (Prefs::MAIN_WINDOW_WIDTH, std::max (100, mainwinRect.width ()));
422      myPrefs->set (Prefs::MAIN_WINDOW_X, mainwinRect.x ());
423      myPrefs->set (Prefs::MAIN_WINDOW_Y, mainwinRect.y ());
424    }
425
426  delete myWatchDir;
427  delete myWindow;
428  delete myModel;
429  delete mySession;
430  delete myPrefs;
431}
432
433/***
434****
435***/
436
437void
438Application::refreshPref (int key)
439{
440  switch (key)
441    {
442      case Prefs::BLOCKLIST_UPDATES_ENABLED:
443        maybeUpdateBlocklist ();
444        break;
445
446      case Prefs::DIR_WATCH:
447      case Prefs::DIR_WATCH_ENABLED:
448        {
449          const QString path (myPrefs->getString (Prefs::DIR_WATCH));
450          const bool isEnabled (myPrefs->getBool (Prefs::DIR_WATCH_ENABLED));
451          myWatchDir->setPath (path, isEnabled);
452          break;
453        }
454
455      default:
456        break;
457    }
458}
459
460void
461Application::maybeUpdateBlocklist ()
462{
463  if (!myPrefs->getBool (Prefs::BLOCKLIST_UPDATES_ENABLED))
464    return;
465
466  const QDateTime lastUpdatedAt = myPrefs->getDateTime (Prefs::BLOCKLIST_DATE);
467  const QDateTime nextUpdateAt = lastUpdatedAt.addDays (7);
468  const QDateTime now = QDateTime::currentDateTime ();
469
470  if (now < nextUpdateAt)
471    {
472      mySession->updateBlocklist ();
473      myPrefs->set (Prefs::BLOCKLIST_DATE, now);
474    }
475}
476
477void
478Application::onSessionSourceChanged ()
479{
480  mySession->initTorrents ();
481  mySession->refreshSessionStats ();
482  mySession->refreshSessionInfo ();
483}
484
485void
486Application::refreshTorrents ()
487{
488  // usually we just poll the torrents that have shown recent activity,
489  // but we also periodically ask for updates on the others to ensure
490  // nothing's falling through the cracks.
491  const time_t now = time (NULL);
492  if (myLastFullUpdateTime + 60 >= now)
493    {
494      mySession->refreshActiveTorrents ();
495    }
496  else
497    {
498      myLastFullUpdateTime = now;
499      mySession->refreshAllTorrents ();
500    }
501}
502
503/***
504****
505***/
506
507void
508Application::addTorrent (const QString& key)
509{
510  const AddData addme (key);
511
512  if (addme.type != addme.NONE)
513    addTorrent (addme);
514}
515
516void
517Application::addTorrent (const AddData& addme)
518{
519  if (!myPrefs->getBool (Prefs::OPTIONS_PROMPT))
520    {
521      mySession->addTorrent (addme);
522    }
523  else
524    {
525      OptionsDialog * o = new OptionsDialog (*mySession, *myPrefs, addme, myWindow);
526      o->show ();
527    }
528
529  raise ();
530}
531
532/***
533****
534***/
535
536void
537Application::raise ()
538{
539  alert (myWindow);
540}
541
542bool
543Application::notifyApp (const QString& title, const QString& body) const
544{
545  const QLatin1String dbusServiceName ("org.freedesktop.Notifications");
546  const QLatin1String dbusInterfaceName ("org.freedesktop.Notifications");
547  const QLatin1String dbusPath ("/org/freedesktop/Notifications");
548
549  QDBusConnection bus = QDBusConnection::sessionBus ();
550  if (bus.isConnected ())
551    {
552      QDBusMessage m = QDBusMessage::createMethodCall (dbusServiceName, dbusPath, dbusInterfaceName, QLatin1String ("Notify"));
553      QVariantList args;
554      args.append (QLatin1String ("Transmission")); // app_name
555      args.append (0U);                             // replaces_id
556      args.append (QLatin1String ("transmission")); // icon
557      args.append (title);                          // summary
558      args.append (body);                           // body
559      args.append (QStringList ());                 // actions - unused for plain passive popups
560      args.append (QVariantMap ());                 // hints - unused atm
561      args.append (static_cast<int32_t> (-1));      // use the default timeout period
562      m.setArguments (args);
563      const QDBusReply<quint32> replyMsg = bus.call (m);
564      if (replyMsg.isValid () && replyMsg.value () > 0)
565        return true;
566    }
567
568  myWindow->trayIcon ().showMessage (title, body);
569  return true;
570}
571
572FaviconCache& Application::faviconCache ()
573{
574  return myFavicons;
575}
576
577/***
578****
579***/
580
581int
582tr_main (int    argc,
583         char * argv[])
584{
585  Application app (argc, argv);
586  return app.exec ();
587}
Note: See TracBrowser for help on using the repository browser.