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. |
59 | class TorrentView : public QTreeWidget |
60 | { |
61 | Q_OBJECT |
62 | public: |
63 | TorrentView(QWidget *parent = nullptr); |
64 | |
65 | #if QT_CONFIG(draganddrop) |
66 | signals: |
67 | void fileDropped(const QString &fileName); |
68 | |
69 | protected: |
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. |
76 | class TorrentViewDelegate : public QItemDelegate |
77 | { |
78 | Q_OBJECT |
79 | public: |
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 | |
112 | MainWindow::MainWindow(QWidget *parent) |
113 | : QMainWindow(parent), quitDialog(nullptr), saveChanges(false) |
114 | { |
115 | // Initialize some static strings |
116 | QStringList ; |
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 * = 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 * = 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 * = 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 | |
216 | QSize MainWindow::sizeHint() const |
217 | { |
218 | const QHeaderView * = 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 | |
231 | const TorrentClient *MainWindow::clientForRow(int row) const |
232 | { |
233 | // Return the client at the given row. |
234 | return jobs.at(i: row).client; |
235 | } |
236 | |
237 | int 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 | |
250 | void 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 | |
278 | bool 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 | |
305 | void 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 | |
327 | void 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 | |
340 | void 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 | |
358 | bool 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 | |
431 | void 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 | |
459 | void 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 | |
476 | void 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 | |
487 | void 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 | |
498 | void 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 | |
525 | void 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 | |
539 | void 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 | |
553 | void 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 | |
562 | void 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 | |
578 | void 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 | |
594 | static 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 | |
609 | void 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 | |
616 | void 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 | |
623 | void 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 | |
665 | void 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 | |
680 | void 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 | |
710 | TorrentView::TorrentView(QWidget *parent) |
711 | : QTreeWidget(parent) |
712 | { |
713 | #if QT_CONFIG(draganddrop) |
714 | setAcceptDrops(true); |
715 | #endif |
716 | } |
717 | |
718 | #if QT_CONFIG(draganddrop) |
719 | void 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 | |
728 | void 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 | |