source: trunk/qt/Application.cc @ 14600

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

Fallback to English if no proper translation is available

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