1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the examples of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:BSD$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** BSD License Usage
18** Alternatively, you may use this file under the terms of the BSD license
19** as follows:
20**
21** "Redistribution and use in source and binary forms, with or without
22** modification, are permitted provided that the following conditions are
23** met:
24** * Redistributions of source code must retain the above copyright
25** notice, this list of conditions and the following disclaimer.
26** * Redistributions in binary form must reproduce the above copyright
27** notice, this list of conditions and the following disclaimer in
28** the documentation and/or other materials provided with the
29** distribution.
30** * Neither the name of The Qt Company Ltd nor the names of its
31** contributors may be used to endorse or promote products derived
32** from this software without specific prior written permission.
33**
34**
35** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46**
47** $QT_END_LICENSE$
48**
49****************************************************************************/
50
51#include <QtWidgets>
52
53#include "addtorrentdialog.h"
54#include "mainwindow.h"
55#include "ratecontroller.h"
56#include "torrentclient.h"
57
58// TorrentView extends QTreeWidget to allow drag and drop.
59class TorrentView : public QTreeWidget
60{
61 Q_OBJECT
62public:
63 TorrentView(QWidget *parent = nullptr);
64
65#if QT_CONFIG(draganddrop)
66signals:
67 void fileDropped(const QString &fileName);
68
69protected:
70 void dragMoveEvent(QDragMoveEvent *event) override;
71 void dropEvent(QDropEvent *event) override;
72#endif
73};
74
75// TorrentViewDelegate is used to draw the progress bars.
76class TorrentViewDelegate : public QItemDelegate
77{
78 Q_OBJECT
79public:
80 inline TorrentViewDelegate(MainWindow *mainWindow) : QItemDelegate(mainWindow) {}
81
82 void paint(QPainter *painter, const QStyleOptionViewItem &option,
83 const QModelIndex &index ) const override
84 {
85 if (index.column() != 2) {
86 QItemDelegate::paint(painter, option, index);
87 return;
88 }
89
90 // Set up a QStyleOptionProgressBar to precisely mimic the
91 // environment of a progress bar.
92 QStyleOptionProgressBar progressBarOption;
93 progressBarOption.state = QStyle::State_Enabled;
94 progressBarOption.direction = QApplication::layoutDirection();
95 progressBarOption.rect = option.rect;
96 progressBarOption.fontMetrics = QApplication::fontMetrics();
97 progressBarOption.minimum = 0;
98 progressBarOption.maximum = 100;
99 progressBarOption.textAlignment = Qt::AlignCenter;
100 progressBarOption.textVisible = true;
101
102 // Set the progress and text values of the style option.
103 int progress = qobject_cast<MainWindow *>(object: parent())->clientForRow(row: index.row())->progress();
104 progressBarOption.progress = progress < 0 ? 0 : progress;
105 progressBarOption.text = QString::asprintf(format: "%d%%", progressBarOption.progress);
106
107 // Draw the progress bar onto the view.
108 QApplication::style()->drawControl(element: QStyle::CE_ProgressBar, opt: &progressBarOption, p: painter);
109 }
110};
111
112MainWindow::MainWindow(QWidget *parent)
113 : QMainWindow(parent), quitDialog(nullptr), saveChanges(false)
114{
115 // Initialize some static strings
116 QStringList headers;
117 headers << tr(s: "Torrent") << tr(s: "Peers/Seeds") << tr(s: "Progress")
118 << tr(s: "Down rate") << tr(s: "Up rate") << tr(s: "Status");
119
120 // Main torrent list
121 torrentView = new TorrentView(this);
122 torrentView->setItemDelegate(new TorrentViewDelegate(this));
123 torrentView->setHeaderLabels(headers);
124 torrentView->setSelectionBehavior(QAbstractItemView::SelectRows);
125 torrentView->setAlternatingRowColors(true);
126 torrentView->setRootIsDecorated(false);
127 setCentralWidget(torrentView);
128
129 // Set header resize modes and initial section sizes
130 QFontMetrics fm = fontMetrics();
131 QHeaderView *header = torrentView->header();
132 header->resizeSection(logicalIndex: 0, size: fm.horizontalAdvance("typical-name-for-a-torrent.torrent"));
133 header->resizeSection(logicalIndex: 1, size: fm.horizontalAdvance(headers.at(i: 1) + " "));
134 header->resizeSection(logicalIndex: 2, size: fm.horizontalAdvance(headers.at(i: 2) + " "));
135 header->resizeSection(logicalIndex: 3, size: qMax(a: fm.horizontalAdvance(headers.at(i: 3) + " "), b: fm.horizontalAdvance(" 1234.0 KB/s ")));
136 header->resizeSection(logicalIndex: 4, size: qMax(a: fm.horizontalAdvance(headers.at(i: 4) + " "), b: fm.horizontalAdvance(" 1234.0 KB/s ")));
137 header->resizeSection(logicalIndex: 5, size: qMax(a: fm.horizontalAdvance(headers.at(i: 5) + " "), b: fm.horizontalAdvance(tr(s: "Downloading") + " ")));
138
139 // Create common actions
140 QAction *newTorrentAction = new QAction(QIcon(":/icons/bottom.png"), tr(s: "Add &new torrent"), this);
141 pauseTorrentAction = new QAction(QIcon(":/icons/player_pause.png"), tr(s: "&Pause torrent"), this);
142 removeTorrentAction = new QAction(QIcon(":/icons/player_stop.png"), tr(s: "&Remove torrent"), this);
143
144 // File menu
145 QMenu *fileMenu = menuBar()->addMenu(title: tr(s: "&File"));
146 fileMenu->addAction(action: newTorrentAction);
147 fileMenu->addAction(action: pauseTorrentAction);
148 fileMenu->addAction(action: removeTorrentAction);
149 fileMenu->addSeparator();
150 fileMenu->addAction(actionIcon: QIcon(":/icons/exit.png"), text: tr(s: "E&xit"), object: this, slot: &MainWindow::close);
151
152 // Help menu
153 QMenu *helpMenu = menuBar()->addMenu(title: tr(s: "&Help"));
154 helpMenu->addAction(text: tr(s: "&About"), object: this, slot: &MainWindow::about);
155 helpMenu->addAction(text: tr(s: "About &Qt"), qApp, slot: QApplication::aboutQt);
156
157 // Top toolbar
158 QToolBar *topBar = new QToolBar(tr(s: "Tools"));
159 addToolBar(area: Qt::TopToolBarArea, toolbar: topBar);
160 topBar->setMovable(false);
161 topBar->addAction(action: newTorrentAction);
162 topBar->addAction(action: removeTorrentAction);
163 topBar->addAction(action: pauseTorrentAction);
164 topBar->addSeparator();
165 downActionTool = topBar->addAction(icon: QIcon(tr(s: ":/icons/1downarrow.png")), text: tr(s: "Move down"));
166 upActionTool = topBar->addAction(icon: QIcon(tr(s: ":/icons/1uparrow.png")), text: tr(s: "Move up"));
167
168 // Bottom toolbar
169 QToolBar *bottomBar = new QToolBar(tr(s: "Rate control"));
170 addToolBar(area: Qt::BottomToolBarArea, toolbar: bottomBar);
171 bottomBar->setMovable(false);
172 downloadLimitSlider = new QSlider(Qt::Horizontal);
173 downloadLimitSlider->setRange(min: 0, max: 1000);
174 bottomBar->addWidget(widget: new QLabel(tr(s: "Max download:")));
175 bottomBar->addWidget(widget: downloadLimitSlider);
176 bottomBar->addWidget(widget: (downloadLimitLabel = new QLabel(tr(s: "0 KB/s"))));
177 downloadLimitLabel->setFixedSize(QSize(fm.horizontalAdvance(tr(s: "99999 KB/s")), fm.lineSpacing()));
178 bottomBar->addSeparator();
179 uploadLimitSlider = new QSlider(Qt::Horizontal);
180 uploadLimitSlider->setRange(min: 0, max: 1000);
181 bottomBar->addWidget(widget: new QLabel(tr(s: "Max upload:")));
182 bottomBar->addWidget(widget: uploadLimitSlider);
183 bottomBar->addWidget(widget: (uploadLimitLabel = new QLabel(tr(s: "0 KB/s"))));
184 uploadLimitLabel->setFixedSize(QSize(fm.horizontalAdvance(tr(s: "99999 KB/s")), fm.lineSpacing()));
185
186#ifdef Q_OS_MACOS
187 setUnifiedTitleAndToolBarOnMac(true);
188#endif
189
190 // Set up connections
191 connect(sender: torrentView, signal: &TorrentView::itemSelectionChanged,
192 receiver: this, slot: &MainWindow::setActionsEnabled);
193 connect(sender: torrentView, signal: &TorrentView::fileDropped,
194 receiver: this, slot: &MainWindow::acceptFileDrop);
195 connect(sender: uploadLimitSlider, signal: &QSlider::valueChanged,
196 receiver: this, slot: &MainWindow::setUploadLimit);
197 connect(sender: downloadLimitSlider, signal: &QSlider::valueChanged,
198 receiver: this, slot: &MainWindow::setDownloadLimit);
199 connect(sender: newTorrentAction, signal: &QAction::triggered,
200 receiver: this, slot: QOverload<>::of(ptr: &MainWindow::addTorrent));
201 connect(sender: pauseTorrentAction, signal: &QAction::triggered,
202 receiver: this, slot: &MainWindow::pauseTorrent);
203 connect(sender: removeTorrentAction, signal: &QAction::triggered,
204 receiver: this, slot: &MainWindow::removeTorrent);
205 connect(sender: upActionTool, signal: &QAction::triggered,
206 receiver: this, slot: &MainWindow::moveTorrentUp);
207 connect(sender: downActionTool, signal: &QAction::triggered,
208 receiver: this, slot: &MainWindow::moveTorrentDown);
209
210 // Load settings and start
211 setWindowTitle(tr(s: "Torrent Client"));
212 setActionsEnabled();
213 QMetaObject::invokeMethod(obj: this, member: "loadSettings", type: Qt::QueuedConnection);
214}
215
216QSize MainWindow::sizeHint() const
217{
218 const QHeaderView *header = torrentView->header();
219
220 // Add up the sizes of all header sections. The last section is
221 // stretched, so its size is relative to the size of the width;
222 // instead of counting it, we count the size of its largest value.
223 int width = fontMetrics().horizontalAdvance(tr(s: "Downloading") + " ");
224 for (int i = 0; i < header->count() - 1; ++i)
225 width += header->sectionSize(logicalIndex: i);
226
227 return QSize(width, QMainWindow::sizeHint().height())
228 .expandedTo(otherSize: QApplication::globalStrut());
229}
230
231const TorrentClient *MainWindow::clientForRow(int row) const
232{
233 // Return the client at the given row.
234 return jobs.at(i: row).client;
235}
236
237int MainWindow::rowOfClient(TorrentClient *client) const
238{
239 // Return the row that displays this client's status, or -1 if the
240 // client is not known.
241 int row = 0;
242 for (const Job &job : jobs) {
243 if (job.client == client)
244 return row;
245 ++row;
246 }
247 return -1;
248}
249
250void MainWindow::loadSettings()
251{
252 // Load base settings (last working directory, upload/download limits).
253 QSettings settings("QtProject", "Torrent");
254 lastDirectory = settings.value(key: "LastDirectory").toString();
255 if (lastDirectory.isEmpty())
256 lastDirectory = QDir::currentPath();
257 int up = settings.value(key: "UploadLimit").toInt();
258 int down = settings.value(key: "DownloadLimit").toInt();
259 uploadLimitSlider->setValue(up ? up : 170);
260 downloadLimitSlider->setValue(down ? down : 550);
261
262 // Resume all previous downloads.
263 int size = settings.beginReadArray(prefix: "Torrents");
264 for (int i = 0; i < size; ++i) {
265 settings.setArrayIndex(i);
266 QByteArray resumeState = settings.value(key: "resumeState").toByteArray();
267 QString fileName = settings.value(key: "sourceFileName").toString();
268 QString dest = settings.value(key: "destinationFolder").toString();
269
270 if (addTorrent(fileName, destinationFolder: dest, resumeState)) {
271 TorrentClient *client = jobs.last().client;
272 client->setDownloadedBytes(settings.value(key: "downloadedBytes").toLongLong());
273 client->setUploadedBytes(settings.value(key: "uploadedBytes").toLongLong());
274 }
275 }
276}
277
278bool MainWindow::addTorrent()
279{
280 // Show the file dialog, let the user select what torrent to start downloading.
281 QString fileName = QFileDialog::getOpenFileName(parent: this, caption: tr(s: "Choose a torrent file"),
282 dir: lastDirectory,
283 filter: tr(s: "Torrents (*.torrent);;"
284 " All files (*.*)"));
285 if (fileName.isEmpty())
286 return false;
287 lastDirectory = QFileInfo(fileName).absolutePath();
288
289 // Show the "Add Torrent" dialog.
290 AddTorrentDialog *addTorrentDialog = new AddTorrentDialog(this);
291 addTorrentDialog->setTorrent(fileName);
292 addTorrentDialog->deleteLater();
293 if (!addTorrentDialog->exec())
294 return false;
295
296 // Add the torrent to our list of downloads
297 addTorrent(fileName, destinationFolder: addTorrentDialog->destinationFolder());
298 if (!saveChanges) {
299 saveChanges = true;
300 QTimer::singleShot(interval: 1000, receiver: this, slot: &MainWindow::saveSettings);
301 }
302 return true;
303}
304
305void MainWindow::removeTorrent()
306{
307 // Find the row of the current item, and find the torrent client
308 // for that row.
309 int row = torrentView->indexOfTopLevelItem(item: torrentView->currentItem());
310 TorrentClient *client = jobs.at(i: row).client;
311
312 // Stop the client.
313 client->disconnect();
314 connect(sender: client, signal: &TorrentClient::stopped,
315 receiver: this, slot: &MainWindow::torrentStopped);
316 client->stop();
317
318 // Remove the row from the view.
319 delete torrentView->takeTopLevelItem(index: row);
320 jobs.removeAt(i: row);
321 setActionsEnabled();
322
323 saveChanges = true;
324 saveSettings();
325}
326
327void MainWindow::torrentStopped()
328{
329 // Schedule the client for deletion.
330 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
331 client->deleteLater();
332
333 // If the quit dialog is shown, update its progress.
334 if (quitDialog) {
335 if (++jobsStopped == jobsToStop)
336 quitDialog->close();
337 }
338}
339
340void MainWindow::torrentError(TorrentClient::Error)
341{
342 // Delete the client.
343 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
344 int row = rowOfClient(client);
345 QString fileName = jobs.at(i: row).torrentFileName;
346 jobs.removeAt(i: row);
347
348 // Display the warning.
349 QMessageBox::warning(parent: this, title: tr(s: "Error"),
350 text: tr(s: "An error occurred while downloading %0: %1")
351 .arg(a: fileName)
352 .arg(a: client->errorString()));
353
354 delete torrentView->takeTopLevelItem(index: row);
355 client->deleteLater();
356}
357
358bool MainWindow::addTorrent(const QString &fileName, const QString &destinationFolder,
359 const QByteArray &resumeState)
360{
361 // Check if the torrent is already being downloaded.
362 for (const Job &job : qAsConst(t&: jobs)) {
363 if (job.torrentFileName == fileName && job.destinationDirectory == destinationFolder) {
364 QMessageBox::warning(parent: this, title: tr(s: "Already downloading"),
365 text: tr(s: "The torrent file %1 is "
366 "already being downloaded.").arg(a: fileName));
367 return false;
368 }
369 }
370
371 // Create a new torrent client and attempt to parse the torrent data.
372 TorrentClient *client = new TorrentClient(this);
373 if (!client->setTorrent(fileName)) {
374 QMessageBox::warning(parent: this, title: tr(s: "Error"),
375 text: tr(s: "The torrent file %1 cannot not be opened/resumed.").arg(a: fileName));
376 delete client;
377 return false;
378 }
379 client->setDestinationFolder(destinationFolder);
380 client->setDumpedState(resumeState);
381
382 // Setup the client connections.
383 connect(sender: client, signal: &TorrentClient::stateChanged,
384 receiver: this, slot: &MainWindow::updateState);
385 connect(sender: client, signal: &TorrentClient::peerInfoUpdated,
386 receiver: this, slot: &MainWindow::updatePeerInfo);
387 connect(sender: client, signal: &TorrentClient::progressUpdated,
388 receiver: this, slot: &MainWindow::updateProgress);
389 connect(sender: client, signal: &TorrentClient::downloadRateUpdated,
390 receiver: this, slot: &MainWindow::updateDownloadRate);
391 connect(sender: client, signal: &TorrentClient::uploadRateUpdated,
392 receiver: this, slot: &MainWindow::updateUploadRate);
393 connect(sender: client, signal: &TorrentClient::stopped,
394 receiver: this, slot: &MainWindow::torrentStopped);
395 connect(sender: client, signal: QOverload<TorrentClient::Error>::of(ptr: &TorrentClient::error),
396 receiver: this, slot: &MainWindow::torrentError);
397
398 // Add the client to the list of downloading jobs.
399 Job job;
400 job.client = client;
401 job.torrentFileName = fileName;
402 job.destinationDirectory = destinationFolder;
403 jobs << job;
404
405 // Create and add a row in the torrent view for this download.
406 QTreeWidgetItem *item = new QTreeWidgetItem(torrentView);
407
408 QString baseFileName = QFileInfo(fileName).fileName();
409 if (baseFileName.toLower().endsWith(s: ".torrent"))
410 baseFileName.chop(n: 8);
411
412 item->setText(column: 0, atext: baseFileName);
413 item->setToolTip(column: 0, atoolTip: tr(s: "Torrent: %1<br>Destination: %2")
414 .arg(a: baseFileName).arg(a: destinationFolder));
415 item->setText(column: 1, atext: tr(s: "0/0"));
416 item->setText(column: 2, atext: "0");
417 item->setText(column: 3, atext: "0.0 KB/s");
418 item->setText(column: 4, atext: "0.0 KB/s");
419 item->setText(column: 5, atext: tr(s: "Idle"));
420 item->setFlags(item->flags() & ~Qt::ItemIsEditable);
421 item->setTextAlignment(column: 1, alignment: Qt::AlignHCenter);
422
423 if (!saveChanges) {
424 saveChanges = true;
425 QTimer::singleShot(interval: 5000, receiver: this, slot: &MainWindow::saveSettings);
426 }
427 client->start();
428 return true;
429}
430
431void MainWindow::saveSettings()
432{
433 if (!saveChanges)
434 return;
435 saveChanges = false;
436
437 // Prepare and reset the settings
438 QSettings settings("QtProject", "Torrent");
439 settings.clear();
440
441 settings.setValue(key: "LastDirectory", value: lastDirectory);
442 settings.setValue(key: "UploadLimit", value: uploadLimitSlider->value());
443 settings.setValue(key: "DownloadLimit", value: downloadLimitSlider->value());
444
445 // Store data on all known torrents
446 settings.beginWriteArray(prefix: "Torrents");
447 for (int i = 0; i < jobs.size(); ++i) {
448 settings.setArrayIndex(i);
449 settings.setValue(key: "sourceFileName", value: jobs.at(i).torrentFileName);
450 settings.setValue(key: "destinationFolder", value: jobs.at(i).destinationDirectory);
451 settings.setValue(key: "uploadedBytes", value: jobs.at(i).client->uploadedBytes());
452 settings.setValue(key: "downloadedBytes", value: jobs.at(i).client->downloadedBytes());
453 settings.setValue(key: "resumeState", value: jobs.at(i).client->dumpedState());
454 }
455 settings.endArray();
456 settings.sync();
457}
458
459void MainWindow::updateState(TorrentClient::State)
460{
461 // Update the state string whenever the client's state changes.
462 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
463 int row = rowOfClient(client);
464 QTreeWidgetItem *item = torrentView->topLevelItem(index: row);
465 if (item) {
466 item->setToolTip(column: 0, atoolTip: tr(s: "Torrent: %1<br>Destination: %2<br>State: %3")
467 .arg(a: jobs.at(i: row).torrentFileName)
468 .arg(a: jobs.at(i: row).destinationDirectory)
469 .arg(a: client->stateString()));
470
471 item->setText(column: 5, atext: client->stateString());
472 }
473 setActionsEnabled();
474}
475
476void MainWindow::updatePeerInfo()
477{
478 // Update the number of connected, visited, seed and leecher peers.
479 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
480 int row = rowOfClient(client);
481
482 QTreeWidgetItem *item = torrentView->topLevelItem(index: row);
483 item->setText(column: 1, atext: tr(s: "%1/%2").arg(a: client->connectedPeerCount())
484 .arg(a: client->seedCount()));
485}
486
487void MainWindow::updateProgress(int percent)
488{
489 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
490 int row = rowOfClient(client);
491
492 // Update the progressbar.
493 QTreeWidgetItem *item = torrentView->topLevelItem(index: row);
494 if (item)
495 item->setText(column: 2, atext: QString::number(percent));
496}
497
498void MainWindow::setActionsEnabled()
499{
500 // Find the view item and client for the current row, and update
501 // the states of the actions.
502 QTreeWidgetItem *item = nullptr;
503 if (!torrentView->selectedItems().isEmpty())
504 item = torrentView->selectedItems().first();
505 TorrentClient *client = item ? jobs.at(i: torrentView->indexOfTopLevelItem(item)).client : nullptr;
506 bool pauseEnabled = client && ((client->state() == TorrentClient::Paused)
507 || (client->state() > TorrentClient::Preparing));
508
509 removeTorrentAction->setEnabled(item != nullptr);
510 pauseTorrentAction->setEnabled(item && pauseEnabled);
511
512 if (client && client->state() == TorrentClient::Paused) {
513 pauseTorrentAction->setIcon(QIcon(":/icons/player_play.png"));
514 pauseTorrentAction->setText(tr(s: "Resume torrent"));
515 } else {
516 pauseTorrentAction->setIcon(QIcon(":/icons/player_pause.png"));
517 pauseTorrentAction->setText(tr(s: "Pause torrent"));
518 }
519
520 int row = torrentView->indexOfTopLevelItem(item);
521 upActionTool->setEnabled(item && row != 0);
522 downActionTool->setEnabled(item && row != jobs.size() - 1);
523}
524
525void MainWindow::updateDownloadRate(int bytesPerSecond)
526{
527 // Update the download rate.
528 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
529 int row = rowOfClient(client);
530 const QString num = QString::asprintf(format: "%.1f KB/s", bytesPerSecond / 1024.0);
531 torrentView->topLevelItem(index: row)->setText(column: 3, atext: num);
532
533 if (!saveChanges) {
534 saveChanges = true;
535 QTimer::singleShot(interval: 5000, receiver: this, slot: &MainWindow::saveSettings);
536 }
537}
538
539void MainWindow::updateUploadRate(int bytesPerSecond)
540{
541 // Update the upload rate.
542 TorrentClient *client = qobject_cast<TorrentClient *>(object: sender());
543 int row = rowOfClient(client);
544 const QString num = QString::asprintf(format: "%.1f KB/s", bytesPerSecond / 1024.0);
545 torrentView->topLevelItem(index: row)->setText(column: 4, atext: num);
546
547 if (!saveChanges) {
548 saveChanges = true;
549 QTimer::singleShot(interval: 5000, receiver: this, slot: &MainWindow::saveSettings);
550 }
551}
552
553void MainWindow::pauseTorrent()
554{
555 // Pause or unpause the current torrent.
556 int row = torrentView->indexOfTopLevelItem(item: torrentView->currentItem());
557 TorrentClient *client = jobs.at(i: row).client;
558 client->setPaused(client->state() != TorrentClient::Paused);
559 setActionsEnabled();
560}
561
562void MainWindow::moveTorrentUp()
563{
564 QTreeWidgetItem *item = torrentView->currentItem();
565 int row = torrentView->indexOfTopLevelItem(item);
566 if (row == 0)
567 return;
568
569 Job tmp = jobs.at(i: row - 1);
570 jobs[row - 1] = jobs[row];
571 jobs[row] = tmp;
572
573 QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(index: row - 1);
574 torrentView->insertTopLevelItem(index: row, item: itemAbove);
575 setActionsEnabled();
576}
577
578void MainWindow::moveTorrentDown()
579{
580 QTreeWidgetItem *item = torrentView->currentItem();
581 int row = torrentView->indexOfTopLevelItem(item);
582 if (row == jobs.size() - 1)
583 return;
584
585 Job tmp = jobs.at(i: row + 1);
586 jobs[row + 1] = jobs[row];
587 jobs[row] = tmp;
588
589 QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(index: row + 1);
590 torrentView->insertTopLevelItem(index: row, item: itemAbove);
591 setActionsEnabled();
592}
593
594static int rateFromValue(int value)
595{
596 int rate = 0;
597 if (value >= 0 && value < 250) {
598 rate = 1 + int(value * 0.124);
599 } else if (value < 500) {
600 rate = 32 + int((value - 250) * 0.384);
601 } else if (value < 750) {
602 rate = 128 + int((value - 500) * 1.536);
603 } else {
604 rate = 512 + int((value - 750) * 6.1445);
605 }
606 return rate;
607}
608
609void MainWindow::setUploadLimit(int value)
610{
611 int rate = rateFromValue(value);
612 uploadLimitLabel->setText(tr(s: "%1 KB/s").arg(a: QString::asprintf(format: "%4d", rate)));
613 RateController::instance()->setUploadLimit(rate * 1024);
614}
615
616void MainWindow::setDownloadLimit(int value)
617{
618 int rate = rateFromValue(value);
619 downloadLimitLabel->setText(tr(s: "%1 KB/s").arg(a: QString::asprintf(format: "%4d", rate)));
620 RateController::instance()->setDownloadLimit(rate * 1024);
621}
622
623void MainWindow::about()
624{
625 QLabel *icon = new QLabel;
626 icon->setPixmap(QPixmap(":/icons/peertopeer.png"));
627
628 QLabel *text = new QLabel;
629 text->setWordWrap(true);
630 text->setText("<p>The <b>Torrent Client</b> example demonstrates how to"
631 " write a complete peer-to-peer file sharing"
632 " application using Qt's network and thread classes.</p>"
633 "<p>This feature complete client implementation of"
634 " the BitTorrent protocol can efficiently"
635 " maintain several hundred network connections"
636 " simultaneously.</p>");
637
638 QPushButton *quitButton = new QPushButton("OK");
639
640 QHBoxLayout *topLayout = new QHBoxLayout;
641 topLayout->setContentsMargins(left: 10, top: 10, right: 10, bottom: 10);
642 topLayout->setSpacing(10);
643 topLayout->addWidget(icon);
644 topLayout->addWidget(text);
645
646 QHBoxLayout *bottomLayout = new QHBoxLayout;
647 bottomLayout->addStretch();
648 bottomLayout->addWidget(quitButton);
649 bottomLayout->addStretch();
650
651 QVBoxLayout *mainLayout = new QVBoxLayout;
652 mainLayout->addLayout(layout: topLayout);
653 mainLayout->addLayout(layout: bottomLayout);
654
655 QDialog about(this);
656 about.setModal(true);
657 about.setWindowTitle(tr(s: "About Torrent Client"));
658 about.setLayout(mainLayout);
659
660 connect(sender: quitButton, signal: &QPushButton::clicked, receiver: &about, slot: &QDialog::close);
661
662 about.exec();
663}
664
665void MainWindow::acceptFileDrop(const QString &fileName)
666{
667 // Create and show the "Add Torrent" dialog.
668 AddTorrentDialog *addTorrentDialog = new AddTorrentDialog;
669 lastDirectory = QFileInfo(fileName).absolutePath();
670 addTorrentDialog->setTorrent(fileName);
671 addTorrentDialog->deleteLater();
672 if (!addTorrentDialog->exec())
673 return;
674
675 // Add the torrent to our list of downloads.
676 addTorrent(fileName, destinationFolder: addTorrentDialog->destinationFolder());
677 saveSettings();
678}
679
680void MainWindow::closeEvent(QCloseEvent *)
681{
682 if (jobs.isEmpty())
683 return;
684
685 // Save upload / download numbers.
686 saveSettings();
687 saveChanges = false;
688
689 quitDialog = new QProgressDialog(tr(s: "Disconnecting from trackers"), tr(s: "Abort"), 0, jobsToStop, this);
690
691 // Stop all clients, remove the rows from the view and wait for
692 // them to signal that they have stopped.
693 jobsToStop = 0;
694 jobsStopped = 0;
695 for (const Job &job : qAsConst(t&: jobs)) {
696 ++jobsToStop;
697 TorrentClient *client = job.client;
698 client->disconnect();
699 connect(sender: client, signal: &TorrentClient::stopped, receiver: this, slot: &MainWindow::torrentStopped);
700 client->stop();
701 delete torrentView->takeTopLevelItem(index: 0);
702 }
703
704 if (jobsToStop > jobsStopped)
705 quitDialog->exec();
706 quitDialog->deleteLater();
707 quitDialog = nullptr;
708}
709
710TorrentView::TorrentView(QWidget *parent)
711 : QTreeWidget(parent)
712{
713#if QT_CONFIG(draganddrop)
714 setAcceptDrops(true);
715#endif
716}
717
718#if QT_CONFIG(draganddrop)
719void TorrentView::dragMoveEvent(QDragMoveEvent *event)
720{
721 // Accept file actions with a '.torrent' extension.
722 QUrl url(event->mimeData()->text());
723 if (url.isValid() && url.scheme() == "file"
724 && url.path().toLower().endsWith(s: ".torrent"))
725 event->acceptProposedAction();
726}
727
728void TorrentView::dropEvent(QDropEvent *event)
729{
730 // Accept drops if the file has a '.torrent' extension and it
731 // exists.
732 QString fileName = QUrl(event->mimeData()->text()).path();
733 if (QFile::exists(fileName) && fileName.toLower().endsWith(s: ".torrent"))
734 emit fileDropped(fileName);
735}
736#endif
737
738#include "mainwindow.moc"
739

source code of qtbase/examples/network/torrent/mainwindow.cpp