source: trunk/qt/options.cc @ 14225

Last change on this file since 14225 was 14225, checked in by jordan, 9 years ago

Licensing changes:

  1. add the option the code to be used under GPLv2 or GPLv3; previously only GPLv2 was allowed
  1. add the "proxy option" as described in GPLv3 so we can add future licenses without having to bulk-edit everything again :)
  1. remove the awkward "exception for MIT code in Mac client" clause; it was unnecessary and confusing.
  • Property svn:keywords set to Date Rev Author Id
File size: 17.8 KB
Line 
1/*
2 * This file Copyright (C) 2009-2014 Mnemosyne LLC
3 *
4 * It may be used under the GNU Public License v2 or v3 licenses,
5 * or any future license endorsed by Mnemosyne LLC.
6 *
7 * $Id: options.cc 14225 2014-01-19 01:09:44Z jordan $
8 */
9
10#include <algorithm> // std::min()
11
12#include <QApplication>
13#include <QCheckBox>
14#include <QComboBox>
15#include <QDialogButtonBox>
16#include <QEvent>
17#include <QFileDialog>
18#include <QFileIconProvider>
19#include <QFileInfo>
20#include <QGridLayout>
21#include <QLabel>
22#include <QMessageBox>
23#include <QPushButton>
24#include <QResizeEvent>
25#include <QSet>
26#include <QVBoxLayout>
27#include <QWidget>
28#include <QLineEdit>
29
30#include <libtransmission/transmission.h>
31#include <libtransmission/utils.h> /* mime64 */
32#include <libtransmission/variant.h>
33
34#include "add-data.h"
35#include "file-tree.h"
36#include "freespace-label.h"
37#include "hig.h"
38#include "options.h"
39#include "prefs.h"
40#include "session.h"
41#include "torrent.h"
42#include "utils.h"
43
44/***
45****
46***/
47
48void
49FileAdded :: executed (int64_t tag, const QString& result, struct tr_variant * arguments)
50{
51  Q_UNUSED (arguments);
52
53  if (tag != myTag)
54    return;
55
56  if ((result == "success") && !myDelFile.isEmpty ())
57    {
58      QFile file (myDelFile);
59      file.setPermissions (QFile::ReadOwner | QFile::WriteOwner);
60      file.remove ();
61    }
62
63  if (result != "success")
64    {
65      QString text = result;
66
67      for (int i=0, n=text.size (); i<n; ++i)
68        if (!i || text[i-1].isSpace ())
69          text[i] = text[i].toUpper ();
70
71      QMessageBox::warning (QApplication::activeWindow (),
72                            tr ("Error Adding Torrent"),
73                            QString ("<p><b>%1</b></p><p>%2</p>").arg (text).arg (myName));
74    }
75
76  deleteLater ();
77}
78
79/***
80****
81***/
82
83Options :: Options (Session& session, const Prefs& prefs, const AddData& addme, QWidget * parent):
84  QDialog (parent, Qt::Dialog),
85  mySession (session),
86  myAdd (addme),
87  myHaveInfo (false),
88  mySourceButton (0),
89  mySourceEdit (0),
90  myDestinationButton (0),
91  myDestinationEdit (0),
92  myVerifyButton (0),
93  myVerifyFile (0),
94  myVerifyHash (QCryptographicHash::Sha1),
95  myEditTimer (this)
96{
97  QFontMetrics fontMetrics (font ());
98  QGridLayout * layout = new QGridLayout (this);
99  int row = 0;
100
101  QString title;
102  if (myAdd.type == AddData::FILENAME)
103    title = tr ("Open Torrent from File");
104  else
105    title = tr ("Open Torrent from URL or Magnet Link");
106  setWindowTitle (title);
107
108  myEditTimer.setInterval (2000);
109  myEditTimer.setSingleShot (true);
110  connect (&myEditTimer, SIGNAL (timeout ()), this, SLOT (onDestinationEditedIdle ()));
111
112  const int iconSize (style ()->pixelMetric (QStyle :: PM_SmallIconSize));
113  QIcon fileIcon = style ()->standardIcon (QStyle::SP_FileIcon);
114  const QPixmap filePixmap = fileIcon.pixmap (iconSize);
115
116  QLabel * l = new QLabel (tr ("&Source:"));
117  layout->addWidget (l, row, 0, Qt::AlignLeft);
118
119  QWidget * w;
120  QPushButton * p;
121
122  if (myAdd.type == AddData::FILENAME)
123    {
124      p = mySourceButton =  new QPushButton;
125      p->setIcon (filePixmap);
126      p->setStyleSheet (QString::fromUtf8 ("text-align: left; padding-left: 5; padding-right: 5"));
127      p->installEventFilter (this);
128      w = p;
129      connect (p, SIGNAL (clicked (bool)), this, SLOT (onFilenameClicked ()));
130    }
131  else
132    {
133      QLineEdit * e = mySourceEdit = new QLineEdit;
134      e->setText (myAdd.readableName());
135      e->setCursorPosition (0);
136      e->selectAll ();
137      w = e;
138      connect (e, SIGNAL(editingFinished()), this, SLOT(onSourceEditingFinished()));
139    }
140
141  const int width = fontMetrics.size (0, QString::fromUtf8 ("This is a pretty long torrent filename indeed.torrent")).width ();
142  w->setMinimumWidth (width);
143  layout->addWidget (w, row, 1);
144  l->setBuddy (w);
145
146  l = new QLabel (tr ("&Destination folder:"));
147  layout->addWidget (l, ++row, 0, Qt::AlignLeft);
148  const QString downloadDir (prefs.getString (Prefs::DOWNLOAD_DIR));
149  myFreespaceLabel = new FreespaceLabel (mySession, downloadDir, this);
150
151  if (session.isLocal ())
152    {
153      const QFileIconProvider iconProvider;
154      const QIcon folderIcon = iconProvider.icon (QFileIconProvider::Folder);
155      const QPixmap folderPixmap = folderIcon.pixmap (iconSize);
156
157      myLocalDestination.setPath (downloadDir);
158      p = myDestinationButton = new QPushButton;
159      p->setIcon (folderPixmap);
160      p->setStyleSheet ("text-align: left; padding-left: 5; padding-right: 5");
161      p->installEventFilter (this);
162      layout->addWidget (p, row, 1);
163      l->setBuddy (p);
164      connect (p, SIGNAL (clicked (bool)), this, SLOT (onDestinationClicked ()));
165    }
166  else
167    {
168      QLineEdit * e = myDestinationEdit = new QLineEdit;
169      e->setText (downloadDir);
170      layout->addWidget (e, row, 1);
171      l->setBuddy (e);
172      connect (e, SIGNAL (textEdited (const QString&)), this, SLOT (onDestinationEdited (const QString&)));
173    }
174
175  l = myFreespaceLabel;
176  layout->addWidget (l, ++row, 0, 1, 2, Qt::Alignment (Qt::AlignRight | Qt::AlignTop));
177  layout->setRowMinimumHeight (row, l->height () + HIG::PAD_SMALL);
178
179  myTree = new FileTreeView (0, false);
180  layout->addWidget (myTree, ++row, 0, 1, 2);
181  if (!session.isLocal ())
182    myTree->hideColumn (2); // hide the % done, since we've no way of knowing
183
184  QComboBox * m = new QComboBox;
185  m->addItem (tr ("High"),   TR_PRI_HIGH);
186  m->addItem (tr ("Normal"), TR_PRI_NORMAL);
187  m->addItem (tr ("Low"),    TR_PRI_LOW);
188  m->setCurrentIndex (1); // Normal
189  myPriorityCombo = m;
190  l = new QLabel (tr ("&Priority:"));
191  l->setBuddy (m);
192  layout->addWidget (l, ++row, 0, Qt::AlignLeft);
193  layout->addWidget (m, row, 1);
194
195  if (session.isLocal ())
196    {
197      p = myVerifyButton = new QPushButton (tr ("&Verify Local Data"));
198      layout->addWidget (p, ++row, 0, Qt::AlignLeft);
199    }
200
201  QCheckBox * c;
202  c = myStartCheck = new QCheckBox (tr ("S&tart when added"));
203  c->setChecked (prefs.getBool (Prefs :: START));
204  layout->addWidget (c, ++row, 0, 1, 2, Qt::AlignLeft);
205
206  c = myTrashCheck = new QCheckBox (tr ("Mo&ve .torrent file to the trash"));
207  c->setChecked (prefs.getBool (Prefs :: TRASH_ORIGINAL));
208  layout->addWidget (c, ++row, 0, 1, 2, Qt::AlignLeft);
209
210  QDialogButtonBox * b = new QDialogButtonBox (QDialogButtonBox::Open|QDialogButtonBox::Cancel, Qt::Horizontal, this);
211  connect (b, SIGNAL (rejected ()), this, SLOT (deleteLater ()));
212  connect (b, SIGNAL (accepted ()), this, SLOT (onAccepted ()));
213  layout->addWidget (b, ++row, 0, 1, 2);
214
215  layout->setRowStretch (3, 2);
216  layout->setColumnStretch (1, 2);
217  layout->setSpacing (HIG :: PAD);
218
219  connect (myTree, SIGNAL (priorityChanged (const QSet<int>&,int)), this, SLOT (onPriorityChanged (const QSet<int>&,int)));
220  connect (myTree, SIGNAL (wantedChanged (const QSet<int>&,bool)), this, SLOT (onWantedChanged (const QSet<int>&,bool)));
221  if (session.isLocal ())
222    connect (myVerifyButton, SIGNAL (clicked (bool)), this, SLOT (onVerify ()));
223
224  connect (&myVerifyTimer, SIGNAL (timeout ()), this, SLOT (onTimeout ()));
225
226  reload ();
227}
228
229Options :: ~Options ()
230{
231  clearInfo ();
232}
233
234/***
235****
236***/
237
238void
239Options :: refreshButton (QPushButton * p, const QString& text, int width)
240{
241  if (width <= 0)
242    width = p->width ();
243  width -= 15;
244  QFontMetrics fontMetrics (font ());
245  QString str = fontMetrics.elidedText (text, Qt::ElideRight, width);
246  p->setText (str);
247}
248
249void
250Options :: refreshSource (int width)
251{
252  QString text = myAdd.readableName ();
253
254  if (mySourceButton)
255    refreshButton (mySourceButton, text, width);
256
257  if (mySourceEdit)
258    mySourceEdit->setText (text);
259}
260
261void
262Options :: refreshDestinationButton (int width)
263{
264  if (myDestinationButton != 0)
265    refreshButton (myDestinationButton, myLocalDestination.absolutePath (), width);
266}
267
268
269bool
270Options :: eventFilter (QObject * o, QEvent * event)
271{
272  if (event->type() == QEvent::Resize)
273    {
274      if (o == mySourceButton)
275        refreshSource (dynamic_cast<QResizeEvent*> (event)->size ().width ());
276
277      else if (o == myDestinationButton)
278        refreshDestinationButton (dynamic_cast<QResizeEvent*> (event)->size ().width ());
279    }
280
281  return false;
282}
283
284/***
285****
286***/
287
288void
289Options :: clearInfo ()
290{
291  if (myHaveInfo)
292    tr_metainfoFree (&myInfo);
293
294  myHaveInfo = false;
295  myFiles.clear ();
296}
297
298void
299Options :: reload ()
300{
301  clearInfo ();
302  clearVerify ();
303
304  tr_ctor * ctor = tr_ctorNew (0);
305
306  switch (myAdd.type)
307    {
308      case AddData::MAGNET:
309        tr_ctorSetMetainfoFromMagnetLink (ctor, myAdd.magnet.toUtf8 ().constData ());
310        break;
311
312      case AddData::FILENAME:
313        tr_ctorSetMetainfoFromFile (ctor, myAdd.filename.toUtf8 ().constData ());
314        break;
315
316      case AddData::METAINFO:
317        tr_ctorSetMetainfo (ctor, (const quint8*)myAdd.metainfo.constData (), myAdd.metainfo.size ());
318        break;
319
320      default:
321        break;
322    }
323
324  const int err = tr_torrentParse (ctor, &myInfo);
325  myHaveInfo = !err;
326  tr_ctorFree (ctor);
327
328  myTree->clear ();
329  myTree->setVisible (myHaveInfo && (myInfo.fileCount>0));
330  myFiles.clear ();
331  myPriorities.clear ();
332  myWanted.clear ();
333
334  if (myVerifyButton)
335    myVerifyButton->setVisible (myHaveInfo && (myInfo.fileCount>0));
336
337  if (myHaveInfo)
338    {
339      myPriorities.insert (0, myInfo.fileCount, TR_PRI_NORMAL);
340      myWanted.insert (0, myInfo.fileCount, true);
341
342      for (tr_file_index_t i=0; i<myInfo.fileCount; ++i)
343        {
344          TrFile file;
345          file.index = i;
346          file.priority = myPriorities[i];
347          file.wanted = myWanted[i];
348          file.size = myInfo.files[i].length;
349          file.have = 0;
350          file.filename = QString::fromUtf8 (myInfo.files[i].name);
351          myFiles.append (file);
352        }
353    }
354
355  myTree->update (myFiles);
356}
357
358void
359Options :: onPriorityChanged (const QSet<int>& fileIndices, int priority)
360{
361  foreach (int i, fileIndices)
362    myPriorities[i] = priority;
363}
364
365void
366Options :: onWantedChanged (const QSet<int>& fileIndices, bool isWanted)
367{
368  foreach (int i, fileIndices)
369    myWanted[i] = isWanted;
370}
371
372void
373Options :: onAccepted ()
374{
375  // rpc spec section 3.4 "adding a torrent"
376
377  const int64_t tag = mySession.getUniqueTag ();
378  tr_variant top;
379  tr_variantInitDict (&top, 3);
380  tr_variantDictAddStr (&top, TR_KEY_method, "torrent-add");
381  tr_variantDictAddInt (&top, TR_KEY_tag, tag);
382  tr_variant * args (tr_variantDictAddDict (&top, TR_KEY_arguments, 10));
383  QString downloadDir;
384
385  // "download-dir"
386  if (myDestinationButton)
387    downloadDir = myLocalDestination.absolutePath ();
388  else
389    downloadDir = myDestinationEdit->text ();
390
391  tr_variantDictAddStr (args, TR_KEY_download_dir, downloadDir.toUtf8 ().constData ());
392
393  // "metainfo"
394  switch (myAdd.type)
395    {
396      case AddData::MAGNET:
397        tr_variantDictAddStr (args, TR_KEY_filename, myAdd.magnet.toUtf8 ().constData ());
398        break;
399
400      case AddData::URL:
401        tr_variantDictAddStr (args, TR_KEY_filename, myAdd.url.toString ().toUtf8 ().constData ());
402        break;
403
404      case AddData::FILENAME:
405      case AddData::METAINFO: {
406        const QByteArray b64 = myAdd.toBase64 ();
407        tr_variantDictAddRaw (args, TR_KEY_metainfo, b64.constData (), b64.size ());
408        break;
409      }
410
411      default:
412        qWarning ("unhandled AddData.type: %d", myAdd.type);
413    }
414
415  // paused
416  tr_variantDictAddBool (args, TR_KEY_paused, !myStartCheck->isChecked ());
417
418  // priority
419  const int index = myPriorityCombo->currentIndex ();
420  const int priority = myPriorityCombo->itemData (index).toInt ();
421  tr_variantDictAddInt (args, TR_KEY_bandwidthPriority, priority);
422
423  // files-unwanted
424  int count = myWanted.count (false);
425  if (count > 0)
426    {
427      tr_variant * l = tr_variantDictAddList (args, TR_KEY_files_unwanted, count);
428      for (int i=0, n=myWanted.size (); i<n; ++i)
429        if (myWanted.at (i) == false)
430          tr_variantListAddInt (l, i);
431    }
432
433  // priority-low
434  count = myPriorities.count (TR_PRI_LOW);
435  if (count > 0)
436    {
437      tr_variant * l = tr_variantDictAddList (args, TR_KEY_priority_low, count);
438      for (int i=0, n=myPriorities.size (); i<n; ++i)
439        if (myPriorities.at (i) == TR_PRI_LOW)
440          tr_variantListAddInt (l, i);
441    }
442
443  // priority-high
444  count = myPriorities.count (TR_PRI_HIGH);
445  if (count > 0)
446    {
447      tr_variant * l = tr_variantDictAddList (args, TR_KEY_priority_high, count);
448      for (int i=0, n=myPriorities.size (); i<n; ++i)
449        if (myPriorities.at (i) == TR_PRI_HIGH)
450          tr_variantListAddInt (l, i);
451    }
452
453  // maybe delete the source .torrent
454  FileAdded * fileAdded = new FileAdded (tag, myAdd.readableName ());
455  if (myTrashCheck->isChecked () && (myAdd.type==AddData::FILENAME))
456    fileAdded->setFileToDelete (myAdd.filename);
457  connect (&mySession, SIGNAL (executed (int64_t,const QString&, struct tr_variant*)),
458           fileAdded, SLOT (executed (int64_t,const QString&, struct tr_variant*)));
459
460  mySession.exec (&top);
461
462  tr_variantFree (&top);
463  deleteLater ();
464}
465
466void
467Options :: onFilenameClicked ()
468{
469  if (myAdd.type == AddData::FILENAME)
470    {
471      QFileDialog * d = new QFileDialog (this,
472                                         tr ("Open Torrent"),
473                                         QFileInfo (myAdd.filename).absolutePath (),
474                                         tr ("Torrent Files (*.torrent);;All Files (*.*)"));
475      d->setFileMode (QFileDialog::ExistingFile);
476      d->setAttribute (Qt::WA_DeleteOnClose);
477      connect (d, SIGNAL (filesSelected (const QStringList&)), this, SLOT (onFilesSelected (const QStringList&)));
478      d->show ();
479    }
480}
481
482void
483Options :: onFilesSelected (const QStringList& files)
484{
485  if (files.size () == 1)
486    {
487      myAdd.set (files.at (0));
488      refreshSource ();
489      reload ();
490    }
491}
492
493void
494Options :: onSourceEditingFinished ()
495{
496  myAdd.set (mySourceEdit->text());
497}
498
499void
500Options :: onDestinationClicked ()
501{
502  QFileDialog * d = new QFileDialog (this, tr ("Select Destination"), myLocalDestination.absolutePath ());
503  d->setFileMode (QFileDialog::Directory);
504  d->setAttribute (Qt::WA_DeleteOnClose);
505  connect (d, SIGNAL (filesSelected (const QStringList&)), this, SLOT (onDestinationsSelected (const QStringList&)));
506  d->show ();
507}
508
509void
510Options :: onDestinationsSelected (const QStringList& destinations)
511{
512  if (destinations.size () == 1)
513    {
514      QString destination = Utils::removeTrailingDirSeparator (destinations.first ());
515      myFreespaceLabel->setPath (destination);
516      myLocalDestination.setPath (destination);
517      refreshDestinationButton ();
518    }
519}
520
521void
522Options :: onDestinationEdited (const QString& text)
523{
524  Q_UNUSED (text);
525
526  myEditTimer.start ();
527}
528
529void
530Options :: onDestinationEditedIdle ()
531{
532  myFreespaceLabel->setPath (myDestinationEdit->text());
533}
534
535/***
536****
537****  VERIFY
538****
539***/
540
541void
542Options :: clearVerify ()
543{
544  myVerifyHash.reset ();
545  myVerifyFile.close ();
546  myVerifyFilePos = 0;
547  myVerifyFlags.clear ();
548  myVerifyFileIndex = 0;
549  myVerifyPieceIndex = 0;
550  myVerifyPiecePos = 0;
551  myVerifyTimer.stop ();
552
553  for (int i=0, n=myFiles.size (); i<n; ++i)
554    myFiles[i].have = 0;
555
556  myTree->update (myFiles);
557}
558
559void
560Options :: onVerify ()
561{
562  clearVerify ();
563  myVerifyFlags.insert (0, myInfo.pieceCount, false);
564  myVerifyTimer.setSingleShot (false);
565  myVerifyTimer.start (0);
566}
567
568namespace
569{
570  uint64_t getPieceSize (const tr_info * info, tr_piece_index_t pieceIndex)
571  {
572    if (pieceIndex != info->pieceCount - 1)
573      return info->pieceSize;
574    return info->totalSize % info->pieceSize;
575  }
576}
577
578void
579Options :: onTimeout ()
580{
581  if (myFiles.isEmpty())
582    {
583      myVerifyTimer.stop ();
584      return;
585    }
586
587  const tr_file * file = &myInfo.files[myVerifyFileIndex];
588
589  if (!myVerifyFilePos && !myVerifyFile.isOpen ())
590    {
591      const QFileInfo fileInfo (myLocalDestination, QString::fromUtf8 (file->name));
592      myVerifyFile.setFileName (fileInfo.absoluteFilePath ());
593      myVerifyFile.open (QIODevice::ReadOnly);
594    }
595
596  int64_t leftInPiece = getPieceSize (&myInfo, myVerifyPieceIndex) - myVerifyPiecePos;
597  int64_t leftInFile = file->length - myVerifyFilePos;
598  int64_t bytesThisPass = std::min (leftInFile, leftInPiece);
599  bytesThisPass = std::min (bytesThisPass, (int64_t)sizeof (myVerifyBuf));
600
601  if (myVerifyFile.isOpen () && myVerifyFile.seek (myVerifyFilePos))
602    {
603      int64_t numRead = myVerifyFile.read (myVerifyBuf, bytesThisPass);
604      if (numRead == bytesThisPass)
605        myVerifyHash.addData (myVerifyBuf, numRead);
606    }
607
608  leftInPiece -= bytesThisPass;
609  leftInFile -= bytesThisPass;
610  myVerifyPiecePos += bytesThisPass;
611  myVerifyFilePos += bytesThisPass;
612
613  myVerifyBins[myVerifyFileIndex] += bytesThisPass;
614
615  if (leftInPiece == 0)
616    {
617      const QByteArray result (myVerifyHash.result ());
618      const bool matches = !memcmp (result.constData (),
619                                    myInfo.pieces[myVerifyPieceIndex].hash,
620                                    SHA_DIGEST_LENGTH);
621      myVerifyFlags[myVerifyPieceIndex] = matches;
622      myVerifyPiecePos = 0;
623      ++myVerifyPieceIndex;
624      myVerifyHash.reset ();
625
626      FileList changedFiles;
627      if (matches)
628        {
629          mybins_t::const_iterator i;
630          for (i=myVerifyBins.begin (); i!=myVerifyBins.end (); ++i)
631            {
632              TrFile& f (myFiles[i.key ()]);
633              f.have += i.value ();
634              changedFiles.append (f);
635            }
636        }
637      myTree->update (changedFiles);
638      myVerifyBins.clear ();
639    }
640
641  if (leftInFile == 0)
642    {
643      myVerifyFile.close ();
644      ++myVerifyFileIndex;
645      myVerifyFilePos = 0;
646    }
647
648  bool done = myVerifyPieceIndex >= myInfo.pieceCount;
649  if (done)
650    {
651      uint64_t have = 0;
652      foreach (const TrFile& f, myFiles)
653        have += f.have;
654
655      if (!have) // everything failed
656        {
657          // did the user accidentally specify the child directory instead of the parent?
658          const QStringList tokens = QString (file->name).split ('/');
659          if (!tokens.empty () && myLocalDestination.dirName ()==tokens.at (0))
660            {
661              // move up one directory and try again
662              myLocalDestination.cdUp ();
663              refreshDestinationButton (-1);
664              onVerify ();
665              done = false;
666            }
667        }
668    }
669
670  if (done)
671    myVerifyTimer.stop ();
672}
Note: See TracBrowser for help on using the repository browser.