source: trunk/qt/options.cc @ 14349

Last change on this file since 14349 was 14349, checked in by mikedld, 8 years ago

#5077: Remove torrent file from watch directory even if "show options dialog" is not set (patch from rb07 + some improvements)

Refactor Session::addTorrent (add new method) to eliminate duplicate
code in options.cc and ensure that FileAdded? object is being created
on torrent addition even with non-interactive workflow.
Move FileAdded? class from options.{h,cc} to session.{h,cc}.

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