source: trunk/qt/options.cc @ 14002

Last change on this file since 14002 was 14002, checked in by jordan, 8 years ago

(qt) merge together the two 'add url/magnet link' dialogs.

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