source: trunk/qt/options.cc @ 14010

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

(qt) #5280 'show options dialog in more cases': fix verify button issue reported by lucke

  • Property svn:keywords set to Date Rev Author Id
File size: 17.9 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 14010 2013-02-10 22:44:25Z 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 (myVerifyButton)
336    myVerifyButton->setVisible (myHaveInfo && (myInfo.fileCount>0));
337
338  if (myHaveInfo)
339    {
340      myPriorities.insert (0, myInfo.fileCount, TR_PRI_NORMAL);
341      myWanted.insert (0, myInfo.fileCount, true);
342
343      for (tr_file_index_t i=0; i<myInfo.fileCount; ++i)
344        {
345          TrFile file;
346          file.index = i;
347          file.priority = myPriorities[i];
348          file.wanted = myWanted[i];
349          file.size = myInfo.files[i].length;
350          file.have = 0;
351          file.filename = QString::fromUtf8 (myInfo.files[i].name);
352          myFiles.append (file);
353        }
354    }
355
356  myTree->update (myFiles);
357}
358
359void
360Options :: onPriorityChanged (const QSet<int>& fileIndices, int priority)
361{
362  foreach (int i, fileIndices)
363    myPriorities[i] = priority;
364}
365
366void
367Options :: onWantedChanged (const QSet<int>& fileIndices, bool isWanted)
368{
369  foreach (int i, fileIndices)
370    myWanted[i] = isWanted;
371}
372
373void
374Options :: onAccepted ()
375{
376  // rpc spec section 3.4 "adding a torrent"
377
378  const int64_t tag = mySession.getUniqueTag ();
379  tr_variant top;
380  tr_variantInitDict (&top, 3);
381  tr_variantDictAddStr (&top, TR_KEY_method, "torrent-add");
382  tr_variantDictAddInt (&top, TR_KEY_tag, tag);
383  tr_variant * args (tr_variantDictAddDict (&top, TR_KEY_arguments, 10));
384  QString downloadDir;
385
386  // "download-dir"
387  if (myDestinationButton)
388    downloadDir = myLocalDestination.absolutePath ();
389  else
390    downloadDir = myDestinationEdit->text ();
391
392  tr_variantDictAddStr (args, TR_KEY_download_dir, downloadDir.toUtf8 ().constData ());
393
394  // "metainfo"
395  switch (myAdd.type)
396    {
397      case AddData::MAGNET:
398        tr_variantDictAddStr (args, TR_KEY_filename, myAdd.magnet.toUtf8 ().constData ());
399        break;
400
401      case AddData::URL:
402        tr_variantDictAddStr (args, TR_KEY_filename, myAdd.url.toString ().toUtf8 ().constData ());
403        break;
404
405      case AddData::FILENAME:
406      case AddData::METAINFO: {
407        const QByteArray b64 = myAdd.toBase64 ();
408        tr_variantDictAddRaw (args, TR_KEY_metainfo, b64.constData (), b64.size ());
409        break;
410      }
411
412      default:
413        qWarning ("unhandled AddData.type: %d", myAdd.type);
414    }
415
416  // paused
417  tr_variantDictAddBool (args, TR_KEY_paused, !myStartCheck->isChecked ());
418
419  // priority
420  const int index = myPriorityCombo->currentIndex ();
421  const int priority = myPriorityCombo->itemData (index).toInt ();
422  tr_variantDictAddInt (args, TR_KEY_bandwidthPriority, priority);
423
424  // files-unwanted
425  int count = myWanted.count (false);
426  if (count > 0)
427    {
428      tr_variant * l = tr_variantDictAddList (args, TR_KEY_files_unwanted, count);
429      for (int i=0, n=myWanted.size (); i<n; ++i)
430        if (myWanted.at (i) == false)
431          tr_variantListAddInt (l, i);
432    }
433
434  // priority-low
435  count = myPriorities.count (TR_PRI_LOW);
436  if (count > 0)
437    {
438      tr_variant * l = tr_variantDictAddList (args, TR_KEY_priority_low, count);
439      for (int i=0, n=myPriorities.size (); i<n; ++i)
440        if (myPriorities.at (i) == TR_PRI_LOW)
441          tr_variantListAddInt (l, i);
442    }
443
444  // priority-high
445  count = myPriorities.count (TR_PRI_HIGH);
446  if (count > 0)
447    {
448      tr_variant * l = tr_variantDictAddList (args, TR_KEY_priority_high, count);
449      for (int i=0, n=myPriorities.size (); i<n; ++i)
450        if (myPriorities.at (i) == TR_PRI_HIGH)
451          tr_variantListAddInt (l, i);
452    }
453
454  // maybe delete the source .torrent
455  FileAdded * fileAdded = new FileAdded (tag, myAdd.readableName ());
456  if (myTrashCheck->isChecked () && (myAdd.type==AddData::FILENAME))
457    fileAdded->setFileToDelete (myAdd.filename);
458  connect (&mySession, SIGNAL (executed (int64_t,const QString&, struct tr_variant*)),
459           fileAdded, SLOT (executed (int64_t,const QString&, struct tr_variant*)));
460
461  mySession.exec (&top);
462
463  tr_variantFree (&top);
464  deleteLater ();
465}
466
467void
468Options :: onFilenameClicked ()
469{
470  if (myAdd.type == AddData::FILENAME)
471    {
472      QFileDialog * d = new QFileDialog (this,
473                                         tr ("Open Torrent"),
474                                         QFileInfo (myAdd.filename).absolutePath (),
475                                         tr ("Torrent Files (*.torrent);;All Files (*.*)"));
476      d->setFileMode (QFileDialog::ExistingFile);
477      d->setAttribute (Qt::WA_DeleteOnClose);
478      connect (d, SIGNAL (filesSelected (const QStringList&)), this, SLOT (onFilesSelected (const QStringList&)));
479      d->show ();
480    }
481}
482
483void
484Options :: onFilesSelected (const QStringList& files)
485{
486  if (files.size () == 1)
487    {
488      myAdd.set (files.at (0));
489      refreshSource ();
490      reload ();
491    }
492}
493
494void
495Options :: onSourceEditingFinished ()
496{
497  myAdd.set (mySourceEdit->text());
498}
499
500void
501Options :: onDestinationClicked ()
502{
503  QFileDialog * d = new QFileDialog (this, tr ("Select Destination"), myLocalDestination.absolutePath ());
504  d->setFileMode (QFileDialog::Directory);
505  d->setAttribute (Qt::WA_DeleteOnClose);
506  connect (d, SIGNAL (filesSelected (const QStringList&)), this, SLOT (onDestinationsSelected (const QStringList&)));
507  d->show ();
508}
509
510void
511Options :: onDestinationsSelected (const QStringList& destinations)
512{
513  if (destinations.size () == 1)
514    {
515      const QString& destination (destinations.first ());
516      myFreespaceLabel->setPath (destination);
517      myLocalDestination.setPath (destination);
518      refreshDestinationButton ();
519    }
520}
521
522void
523Options :: onDestinationEdited (const QString& text)
524{
525  Q_UNUSED (text);
526
527  myEditTimer.start ();
528}
529
530void
531Options :: onDestinationEditedIdle ()
532{
533  myFreespaceLabel->setPath (myDestinationEdit->text());
534}
535
536/***
537****
538****  VERIFY
539****
540***/
541
542void
543Options :: clearVerify ()
544{
545  myVerifyHash.reset ();
546  myVerifyFile.close ();
547  myVerifyFilePos = 0;
548  myVerifyFlags.clear ();
549  myVerifyFileIndex = 0;
550  myVerifyPieceIndex = 0;
551  myVerifyPiecePos = 0;
552  myVerifyTimer.stop ();
553
554  for (int i=0, n=myFiles.size (); i<n; ++i)
555    myFiles[i].have = 0;
556
557  myTree->update (myFiles);
558}
559
560void
561Options :: onVerify ()
562{
563  clearVerify ();
564  myVerifyFlags.insert (0, myInfo.pieceCount, false);
565  myVerifyTimer.setSingleShot (false);
566  myVerifyTimer.start (0);
567}
568
569namespace
570{
571  uint64_t getPieceSize (const tr_info * info, tr_piece_index_t pieceIndex)
572  {
573    if (pieceIndex != info->pieceCount - 1)
574      return info->pieceSize;
575    return info->totalSize % info->pieceSize;
576  }
577}
578
579void
580Options :: onTimeout ()
581{
582  if (myFiles.isEmpty())
583    {
584      myVerifyTimer.stop ();
585      return;
586    }
587
588  const tr_file * file = &myInfo.files[myVerifyFileIndex];
589
590  if (!myVerifyFilePos && !myVerifyFile.isOpen ())
591    {
592      const QFileInfo fileInfo (myLocalDestination, QString::fromUtf8 (file->name));
593      myVerifyFile.setFileName (fileInfo.absoluteFilePath ());
594      myVerifyFile.open (QIODevice::ReadOnly);
595    }
596
597  int64_t leftInPiece = getPieceSize (&myInfo, myVerifyPieceIndex) - myVerifyPiecePos;
598  int64_t leftInFile = file->length - myVerifyFilePos;
599  int64_t bytesThisPass = std::min (leftInFile, leftInPiece);
600  bytesThisPass = std::min (bytesThisPass, (int64_t)sizeof (myVerifyBuf));
601
602  if (myVerifyFile.isOpen () && myVerifyFile.seek (myVerifyFilePos))
603    {
604      int64_t numRead = myVerifyFile.read (myVerifyBuf, bytesThisPass);
605      if (numRead == bytesThisPass)
606        myVerifyHash.addData (myVerifyBuf, numRead);
607    }
608
609  leftInPiece -= bytesThisPass;
610  leftInFile -= bytesThisPass;
611  myVerifyPiecePos += bytesThisPass;
612  myVerifyFilePos += bytesThisPass;
613
614  myVerifyBins[myVerifyFileIndex] += bytesThisPass;
615
616  if (leftInPiece == 0)
617    {
618      const QByteArray result (myVerifyHash.result ());
619      const bool matches = !memcmp (result.constData (),
620                                    myInfo.pieces[myVerifyPieceIndex].hash,
621                                    SHA_DIGEST_LENGTH);
622      myVerifyFlags[myVerifyPieceIndex] = matches;
623      myVerifyPiecePos = 0;
624      ++myVerifyPieceIndex;
625      myVerifyHash.reset ();
626
627      FileList changedFiles;
628      if (matches)
629        {
630          mybins_t::const_iterator i;
631          for (i=myVerifyBins.begin (); i!=myVerifyBins.end (); ++i)
632            {
633              TrFile& f (myFiles[i.key ()]);
634              f.have += i.value ();
635              changedFiles.append (f);
636            }
637        }
638      myTree->update (changedFiles);
639      myVerifyBins.clear ();
640    }
641
642  if (leftInFile == 0)
643    {
644      myVerifyFile.close ();
645      ++myVerifyFileIndex;
646      myVerifyFilePos = 0;
647    }
648
649  bool done = myVerifyPieceIndex >= myInfo.pieceCount;
650  if (done)
651    {
652      uint64_t have = 0;
653      foreach (const TrFile& f, myFiles)
654        have += f.have;
655
656      if (!have) // everything failed
657        {
658          // did the user accidentally specify the child directory instead of the parent?
659          const QStringList tokens = QString (file->name).split ('/');
660          if (!tokens.empty () && myLocalDestination.dirName ()==tokens.at (0))
661            {
662              // move up one directory and try again
663              myLocalDestination.cdUp ();
664              refreshDestinationButton (-1);
665              onVerify ();
666              done = false;
667            }
668        }
669    }
670
671  if (done)
672    myVerifyTimer.stop ();
673}
Note: See TracBrowser for help on using the repository browser.