source: trunk/qt/Application.cc @ 14580

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

Display notifications via tray icon if dbus is not available

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