source: trunk/qt/Application.cc @ 14603

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

Improve Qt client appearance on hidpi screens a bit

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