Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #include <QtWidgets>
++
++ #include "addtorrentdialog.h"
++ #include "mainwindow.h"
++ #include "ratecontroller.h"
++ #include "torrentclient.h"
++
++ // TorrentView extends QTreeWidget to allow drag and drop.
++ class TorrentView : public QTreeWidget
++ {
++ Q_OBJECT
++ public:
++ TorrentView(QWidget *parent = nullptr);
++
++ #if QT_CONFIG(draganddrop)
++ signals:
++ void fileDropped(const QString &fileName);
++
++ protected:
++ void dragMoveEvent(QDragMoveEvent *event) override;
++ void dropEvent(QDropEvent *event) override;
++ #endif
++ };
++
++ // TorrentViewDelegate is used to draw the progress bars.
++ class TorrentViewDelegate : public QStyledItemDelegate
++ {
++ Q_OBJECT
++ public:
++ inline TorrentViewDelegate(MainWindow *mainWindow) : QStyledItemDelegate(mainWindow) {}
++
++ void paint(QPainter *painter, const QStyleOptionViewItem &option,
++ const QModelIndex &index ) const override
++ {
++ if (index.column() != 2) {
++ QStyledItemDelegate::paint(painter, option, index);
++ return;
++ }
++
++ // Set up a QStyleOptionProgressBar to precisely mimic the
++ // environment of a progress bar.
++ QStyleOptionProgressBar progressBarOption;
++ progressBarOption.state |= QStyle::State_Enabled;
++ progressBarOption.direction = QApplication::layoutDirection();
++ progressBarOption.rect = option.rect;
++ progressBarOption.fontMetrics = QFontMetrics(QApplication::font());
++ progressBarOption.minimum = 0;
++ progressBarOption.maximum = 100;
++ progressBarOption.textAlignment = Qt::AlignCenter;
++ progressBarOption.textVisible = true;
++
++ // Set the progress and text values of the style option.
++ int progress = qobject_cast<MainWindow *>(parent())->clientForRow(index.row())->progress();
++ progressBarOption.progress = progress < 0 ? 0 : progress;
++ progressBarOption.text = QString::asprintf("%d%%", progressBarOption.progress);
++
++ // Draw the progress bar onto the view.
++ QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
++ }
++ };
++
++ MainWindow::MainWindow(QWidget *parent)
++ : QMainWindow(parent), quitDialog(nullptr), saveChanges(false)
++ {
++ // Initialize some static strings
++ QStringList headers;
++ headers << tr("Torrent") << tr("Peers/Seeds") << tr("Progress")
++ << tr("Down rate") << tr("Up rate") << tr("Status");
++
++ // Main torrent list
++ torrentView = new TorrentView(this);
++ torrentView->setItemDelegate(new TorrentViewDelegate(this));
++ torrentView->setHeaderLabels(headers);
++ torrentView->setSelectionBehavior(QAbstractItemView::SelectRows);
++ torrentView->setAlternatingRowColors(true);
++ torrentView->setRootIsDecorated(false);
++ setCentralWidget(torrentView);
++
++ // Set header resize modes and initial section sizes
++ const QFontMetrics fm = torrentView->fontMetrics();
++ QHeaderView *header = torrentView->header();
++ header->resizeSection(0, fm.horizontalAdvance("typical-name-for-a-torrent.torrent"));
++ header->resizeSection(1, fm.horizontalAdvance(headers.at(1) + " "));
++ header->resizeSection(2, fm.horizontalAdvance(headers.at(2) + " "));
++ header->resizeSection(3, qMax(fm.horizontalAdvance(headers.at(3) + " "), fm.horizontalAdvance(" 1234.0 KB/s ")));
++ header->resizeSection(4, qMax(fm.horizontalAdvance(headers.at(4) + " "), fm.horizontalAdvance(" 1234.0 KB/s ")));
++ header->resizeSection(5, qMax(fm.horizontalAdvance(headers.at(5) + " "), fm.horizontalAdvance(tr("Downloading") + " ")));
++
++ // Create common actions
++ QAction *newTorrentAction = new QAction(QIcon(":/icons/bottom.svg"), tr("Add &new torrent"), this);
++ pauseTorrentAction = new QAction(QIcon(":/icons/player_pause.svg"), tr("&Pause torrent"), this);
++ removeTorrentAction = new QAction(QIcon(":/icons/player_stop.svg"), tr("&Remove torrent"), this);
++
++ // File menu
++ QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
++ fileMenu->addAction(newTorrentAction);
++ fileMenu->addAction(pauseTorrentAction);
++ fileMenu->addAction(removeTorrentAction);
++ fileMenu->addSeparator();
++ fileMenu->addAction(QIcon(":/icons/exit.svg"), tr("E&xit"), this, &MainWindow::close);
++
++ // Help menu
++ QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
++ helpMenu->addAction(QIcon(":/icons/about.svg"), tr("&About"), this, &MainWindow::about);
++ helpMenu->addAction(QIcon(":/icons/about.svg"), tr("About &Qt"), qApp, QApplication::aboutQt);
++
++ // Top toolbar
++ QToolBar *topBar = new QToolBar(tr("Tools"));
++ addToolBar(Qt::TopToolBarArea, topBar);
++ topBar->setMovable(false);
++ topBar->addAction(newTorrentAction);
++ topBar->addAction(removeTorrentAction);
++ topBar->addAction(pauseTorrentAction);
++ topBar->addSeparator();
++ downActionTool = topBar->addAction(QIcon(tr(":/icons/1downarrow.svg")), tr("Move down"));
++ upActionTool = topBar->addAction(QIcon(tr(":/icons/1uparrow.svg")), tr("Move up"));
++
++ // Bottom toolbar
++ QToolBar *bottomBar = new QToolBar(tr("Rate control"));
++ addToolBar(Qt::BottomToolBarArea, bottomBar);
++ bottomBar->setMovable(false);
++ downloadLimitSlider = new QSlider(Qt::Horizontal);
++ downloadLimitSlider->setRange(0, 1000);
++ bottomBar->addWidget(new QLabel(tr("Max download:")));
++ bottomBar->addWidget(downloadLimitSlider);
++ bottomBar->addWidget((downloadLimitLabel = new QLabel(tr("0 KB/s"))));
++ downloadLimitLabel->setFixedSize(QSize(fm.horizontalAdvance(tr("99999 KB/s")), fm.lineSpacing()));
++ bottomBar->addSeparator();
++ uploadLimitSlider = new QSlider(Qt::Horizontal);
++ uploadLimitSlider->setRange(0, 1000);
++ bottomBar->addWidget(new QLabel(tr("Max upload:")));
++ bottomBar->addWidget(uploadLimitSlider);
++ bottomBar->addWidget((uploadLimitLabel = new QLabel(tr("0 KB/s"))));
++ uploadLimitLabel->setFixedSize(QSize(fm.horizontalAdvance(tr("99999 KB/s")), fm.lineSpacing()));
++
++ #ifdef Q_OS_MACOS
++ setUnifiedTitleAndToolBarOnMac(true);
++ #endif
++
++ // Set up connections
++ connect(torrentView, &TorrentView::itemSelectionChanged,
++ this, &MainWindow::setActionsEnabled);
++ connect(torrentView, &TorrentView::fileDropped,
++ this, &MainWindow::acceptFileDrop);
++ connect(uploadLimitSlider, &QSlider::valueChanged,
++ this, &MainWindow::setUploadLimit);
++ connect(downloadLimitSlider, &QSlider::valueChanged,
++ this, &MainWindow::setDownloadLimit);
++ connect(newTorrentAction, &QAction::triggered,
++ this, QOverload<>::of(&MainWindow::addTorrent));
++ connect(pauseTorrentAction, &QAction::triggered,
++ this, &MainWindow::pauseTorrent);
++ connect(removeTorrentAction, &QAction::triggered,
++ this, &MainWindow::removeTorrent);
++ connect(upActionTool, &QAction::triggered,
++ this, &MainWindow::moveTorrentUp);
++ connect(downActionTool, &QAction::triggered,
++ this, &MainWindow::moveTorrentDown);
++
++ // Load settings and start
++ setWindowTitle(tr("Torrent Client"));
++ setActionsEnabled();
++ QMetaObject::invokeMethod(this, "loadSettings", Qt::QueuedConnection);
++ }
++
++ QSize MainWindow::sizeHint() const
++ {
++ const QHeaderView *header = torrentView->header();
++
++ // Add up the sizes of all header sections. The last section is
++ // stretched, so its size is relative to the size of the width;
++ // instead of counting it, we count the size of its largest value.
++ int width = fontMetrics().horizontalAdvance(tr("Downloading") + " ");
++ for (int i = 0; i < header->count() - 1; ++i)
++ width += header->sectionSize(i);
++
++ return QSize(width, QMainWindow::sizeHint().height());
++ }
++
++ const TorrentClient *MainWindow::clientForRow(int row) const
++ {
++ // Return the client at the given row.
++ return jobs.at(row).client;
++ }
++
++ int MainWindow::rowOfClient(TorrentClient *client) const
++ {
++ // Return the row that displays this client's status, or -1 if the
++ // client is not known.
++ int row = 0;
++ for (const Job &job : jobs) {
++ if (job.client == client)
++ return row;
++ ++row;
++ }
++ return -1;
++ }
++
++ void MainWindow::loadSettings()
++ {
++ // Load base settings (last working directory, upload/download limits).
++ QSettings settings("QtProject", "Torrent");
++ lastDirectory = settings.value("LastDirectory").toString();
++ if (lastDirectory.isEmpty())
++ lastDirectory = QDir::currentPath();
++ int up = settings.value("UploadLimit").toInt();
++ int down = settings.value("DownloadLimit").toInt();
++ uploadLimitSlider->setValue(up ? up : 170);
++ downloadLimitSlider->setValue(down ? down : 550);
++
++ // Resume all previous downloads.
++ int size = settings.beginReadArray("Torrents");
++ for (int i = 0; i < size; ++i) {
++ settings.setArrayIndex(i);
++ QByteArray resumeState = settings.value("resumeState").toByteArray();
++ QString fileName = settings.value("sourceFileName").toString();
++ QString dest = settings.value("destinationFolder").toString();
++
++ if (addTorrent(fileName, dest, resumeState)) {
++ TorrentClient *client = jobs.last().client;
++ client->setDownloadedBytes(settings.value("downloadedBytes").toLongLong());
++ client->setUploadedBytes(settings.value("uploadedBytes").toLongLong());
++ }
++ }
++ }
++
++ bool MainWindow::addTorrent()
++ {
++ // Show the file dialog, let the user select what torrent to start downloading.
++ QString fileName = QFileDialog::getOpenFileName(this, tr("Choose a torrent file"),
++ lastDirectory,
++ tr("Torrents (*.torrent);;"
++ " All files (*.*)"));
++ if (fileName.isEmpty())
++ return false;
++ lastDirectory = QFileInfo(fileName).absolutePath();
++
++ // Show the "Add Torrent" dialog.
++ AddTorrentDialog *addTorrentDialog = new AddTorrentDialog(this);
++ addTorrentDialog->setTorrent(fileName);
++ addTorrentDialog->deleteLater();
++ if (!addTorrentDialog->exec())
++ return false;
++
++ // Add the torrent to our list of downloads
++ addTorrent(fileName, addTorrentDialog->destinationFolder());
++ if (!saveChanges) {
++ saveChanges = true;
++ QTimer::singleShot(1000, this, &MainWindow::saveSettings);
++ }
++ return true;
++ }
++
++ void MainWindow::removeTorrent()
++ {
++ // Find the row of the current item, and find the torrent client
++ // for that row.
++ int row = torrentView->indexOfTopLevelItem(torrentView->currentItem());
++ TorrentClient *client = jobs.at(row).client;
++
++ // Stop the client.
++ client->disconnect();
++ connect(client, &TorrentClient::stopped,
++ this, &MainWindow::torrentStopped);
++ client->stop();
++
++ // Remove the row from the view.
++ delete torrentView->takeTopLevelItem(row);
++ jobs.removeAt(row);
++ setActionsEnabled();
++
++ saveChanges = true;
++ saveSettings();
++ }
++
++ void MainWindow::torrentStopped()
++ {
++ // Schedule the client for deletion.
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ client->deleteLater();
++
++ // If the quit dialog is shown, update its progress.
++ if (quitDialog) {
++ if (++jobsStopped == jobsToStop)
++ quitDialog->close();
++ }
++ }
++
++ void MainWindow::torrentError(TorrentClient::Error)
++ {
++ // Delete the client.
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ int row = rowOfClient(client);
++ QString fileName = jobs.at(row).torrentFileName;
++ jobs.removeAt(row);
++
++ // Display the warning.
++ QMessageBox::warning(this, tr("Error"),
++ tr("An error occurred while downloading %0: %1")
++ .arg(fileName)
++ .arg(client->errorString()));
++
++ delete torrentView->takeTopLevelItem(row);
++ client->deleteLater();
++ }
++
++ bool MainWindow::addTorrent(const QString &fileName, const QString &destinationFolder,
++ const QByteArray &resumeState)
++ {
++ // Check if the torrent is already being downloaded.
++ for (const Job &job : std::as_const(jobs)) {
++ if (job.torrentFileName == fileName && job.destinationDirectory == destinationFolder) {
++ QMessageBox::warning(this, tr("Already downloading"),
++ tr("The torrent file %1 is "
++ "already being downloaded.").arg(fileName));
++ return false;
++ }
++ }
++
++ // Create a new torrent client and attempt to parse the torrent data.
++ TorrentClient *client = new TorrentClient(this);
++ if (!client->setTorrent(fileName)) {
++ QMessageBox::warning(this, tr("Error"),
++ tr("The torrent file %1 cannot not be opened/resumed.").arg(fileName));
++ delete client;
++ return false;
++ }
++ client->setDestinationFolder(destinationFolder);
++ client->setDumpedState(resumeState);
++
++ // Setup the client connections.
++ connect(client, &TorrentClient::stateChanged,
++ this, &MainWindow::updateState);
++ connect(client, &TorrentClient::peerInfoUpdated,
++ this, &MainWindow::updatePeerInfo);
++ connect(client, &TorrentClient::progressUpdated,
++ this, &MainWindow::updateProgress);
++ connect(client, &TorrentClient::downloadRateUpdated,
++ this, &MainWindow::updateDownloadRate);
++ connect(client, &TorrentClient::uploadRateUpdated,
++ this, &MainWindow::updateUploadRate);
++ connect(client, &TorrentClient::stopped,
++ this, &MainWindow::torrentStopped);
++ connect(client, QOverload<TorrentClient::Error>::of(&TorrentClient::error),
++ this, &MainWindow::torrentError);
++
++ // Add the client to the list of downloading jobs.
++ Job job;
++ job.client = client;
++ job.torrentFileName = fileName;
++ job.destinationDirectory = destinationFolder;
++ jobs << job;
++
++ // Create and add a row in the torrent view for this download.
++ QTreeWidgetItem *item = new QTreeWidgetItem(torrentView);
++
++ QString baseFileName = QFileInfo(fileName).fileName();
++ if (baseFileName.endsWith(u".torrent", Qt::CaseInsensitive))
++ baseFileName.chop(8);
++
++ item->setText(0, baseFileName);
++ item->setToolTip(0, tr("Torrent: %1<br>Destination: %2")
++ .arg(baseFileName).arg(destinationFolder));
++ item->setText(1, tr("0/0"));
++ item->setText(2, "0");
++ item->setText(3, "0.0 KB/s");
++ item->setText(4, "0.0 KB/s");
++ item->setText(5, tr("Idle"));
++ item->setFlags(item->flags() & ~Qt::ItemIsEditable);
++ item->setTextAlignment(1, Qt::AlignHCenter);
++
++ if (!saveChanges) {
++ saveChanges = true;
++ QTimer::singleShot(5000, this, &MainWindow::saveSettings);
++ }
++ client->start();
++ return true;
++ }
++
++ void MainWindow::saveSettings()
++ {
++ if (!saveChanges)
++ return;
++ saveChanges = false;
++
++ // Prepare and reset the settings
++ QSettings settings("QtProject", "Torrent");
++ settings.clear();
++
++ settings.setValue("LastDirectory", lastDirectory);
++ settings.setValue("UploadLimit", uploadLimitSlider->value());
++ settings.setValue("DownloadLimit", downloadLimitSlider->value());
++
++ // Store data on all known torrents
++ settings.beginWriteArray("Torrents");
++ for (int i = 0; i < jobs.size(); ++i) {
++ settings.setArrayIndex(i);
++ settings.setValue("sourceFileName", jobs.at(i).torrentFileName);
++ settings.setValue("destinationFolder", jobs.at(i).destinationDirectory);
++ settings.setValue("uploadedBytes", jobs.at(i).client->uploadedBytes());
++ settings.setValue("downloadedBytes", jobs.at(i).client->downloadedBytes());
++ settings.setValue("resumeState", jobs.at(i).client->dumpedState());
++ }
++ settings.endArray();
++ settings.sync();
++ }
++
++ void MainWindow::updateState(TorrentClient::State)
++ {
++ // Update the state string whenever the client's state changes.
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ int row = rowOfClient(client);
++ QTreeWidgetItem *item = torrentView->topLevelItem(row);
++ if (item) {
++ item->setToolTip(0, tr("Torrent: %1<br>Destination: %2<br>State: %3")
++ .arg(jobs.at(row).torrentFileName)
++ .arg(jobs.at(row).destinationDirectory)
++ .arg(client->stateString()));
++
++ item->setText(5, client->stateString());
++ }
++ setActionsEnabled();
++ }
++
++ void MainWindow::updatePeerInfo()
++ {
++ // Update the number of connected, visited, seed and leecher peers.
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ int row = rowOfClient(client);
++
++ QTreeWidgetItem *item = torrentView->topLevelItem(row);
++ item->setText(1, tr("%1/%2").arg(client->connectedPeerCount())
++ .arg(client->seedCount()));
++ }
++
++ void MainWindow::updateProgress(int percent)
++ {
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ int row = rowOfClient(client);
++
++ // Update the progressbar.
++ QTreeWidgetItem *item = torrentView->topLevelItem(row);
++ if (item)
++ item->setText(2, QString::number(percent));
++ }
++
++ void MainWindow::setActionsEnabled()
++ {
++ // Find the view item and client for the current row, and update
++ // the states of the actions.
++ QTreeWidgetItem *item = nullptr;
++ if (!torrentView->selectedItems().isEmpty())
++ item = torrentView->selectedItems().first();
++ TorrentClient *client = item ? jobs.at(torrentView->indexOfTopLevelItem(item)).client : nullptr;
++ bool pauseEnabled = client && ((client->state() == TorrentClient::Paused)
++ || (client->state() > TorrentClient::Preparing));
++
++ removeTorrentAction->setEnabled(item != nullptr);
++ pauseTorrentAction->setEnabled(item && pauseEnabled);
++
++ if (client && client->state() == TorrentClient::Paused) {
++ pauseTorrentAction->setIcon(QIcon(":/icons/player_play.svg"));
++ pauseTorrentAction->setText(tr("Resume torrent"));
++ } else {
++ pauseTorrentAction->setIcon(QIcon(":/icons/player_pause.svg"));
++ pauseTorrentAction->setText(tr("Pause torrent"));
++ }
++
++ int row = torrentView->indexOfTopLevelItem(item);
++ upActionTool->setEnabled(item && row != 0);
++ downActionTool->setEnabled(item && row != jobs.size() - 1);
++ }
++
++ void MainWindow::updateDownloadRate(int bytesPerSecond)
++ {
++ // Update the download rate.
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ int row = rowOfClient(client);
++ const QString num = QString::asprintf("%.1f KB/s", bytesPerSecond / 1024.0);
++ torrentView->topLevelItem(row)->setText(3, num);
++
++ if (!saveChanges) {
++ saveChanges = true;
++ QTimer::singleShot(5000, this, &MainWindow::saveSettings);
++ }
++ }
++
++ void MainWindow::updateUploadRate(int bytesPerSecond)
++ {
++ // Update the upload rate.
++ TorrentClient *client = qobject_cast<TorrentClient *>(sender());
++ int row = rowOfClient(client);
++ const QString num = QString::asprintf("%.1f KB/s", bytesPerSecond / 1024.0);
++ torrentView->topLevelItem(row)->setText(4, num);
++
++ if (!saveChanges) {
++ saveChanges = true;
++ QTimer::singleShot(5000, this, &MainWindow::saveSettings);
++ }
++ }
++
++ void MainWindow::pauseTorrent()
++ {
++ // Pause or unpause the current torrent.
++ int row = torrentView->indexOfTopLevelItem(torrentView->currentItem());
++ TorrentClient *client = jobs.at(row).client;
++ client->setPaused(client->state() != TorrentClient::Paused);
++ setActionsEnabled();
++ }
++
++ void MainWindow::moveTorrentUp()
++ {
++ QTreeWidgetItem *item = torrentView->currentItem();
++ int row = torrentView->indexOfTopLevelItem(item);
++ if (row == 0)
++ return;
++
++ Job tmp = jobs.at(row - 1);
++ jobs[row - 1] = jobs[row];
++ jobs[row] = tmp;
++
++ QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(row - 1);
++ torrentView->insertTopLevelItem(row, itemAbove);
++ setActionsEnabled();
++ }
++
++ void MainWindow::moveTorrentDown()
++ {
++ QTreeWidgetItem *item = torrentView->currentItem();
++ int row = torrentView->indexOfTopLevelItem(item);
++ if (row == jobs.size() - 1)
++ return;
++
++ Job tmp = jobs.at(row + 1);
++ jobs[row + 1] = jobs[row];
++ jobs[row] = tmp;
++
++ QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(row + 1);
++ torrentView->insertTopLevelItem(row, itemAbove);
++ setActionsEnabled();
++ }
++
++ static int rateFromValue(int value)
++ {
++ int rate = 0;
++ if (value >= 0 && value < 250) {
++ rate = 1 + int(value * 0.124);
++ } else if (value < 500) {
++ rate = 32 + int((value - 250) * 0.384);
++ } else if (value < 750) {
++ rate = 128 + int((value - 500) * 1.536);
++ } else {
++ rate = 512 + int((value - 750) * 6.1445);
++ }
++ return rate;
++ }
++
++ void MainWindow::setUploadLimit(int value)
++ {
++ int rate = rateFromValue(value);
++ uploadLimitLabel->setText(tr("%1 KB/s").arg(QString::asprintf("%4d", rate)));
++ RateController::instance()->setUploadLimit(rate * 1024);
++ }
++
++ void MainWindow::setDownloadLimit(int value)
++ {
++ int rate = rateFromValue(value);
++ downloadLimitLabel->setText(tr("%1 KB/s").arg(QString::asprintf("%4d", rate)));
++ RateController::instance()->setDownloadLimit(rate * 1024);
++ }
++
++ void MainWindow::about()
++ {
++ QLabel *icon = new QLabel;
++ QImage img(":/icons/peertopeer.svg");
++ QPixmap pm;
++ pm.convertFromImage(img);
++ icon->setPixmap(pm);
++
++ QLabel *text = new QLabel;
++ text->setWordWrap(true);
++ text->setText("<p>The <b>Torrent Client</b> example demonstrates how to"
++ " write a complete peer-to-peer file sharing"
++ " application using Qt's network and thread classes.</p>"
++ "<p>This feature complete client implementation of"
++ " the BitTorrent protocol can efficiently"
++ " maintain several hundred network connections"
++ " simultaneously.</p>");
++
++ QPushButton *quitButton = new QPushButton("OK");
++
++ QHBoxLayout *topLayout = new QHBoxLayout;
++ topLayout->setContentsMargins(10, 10, 10, 10);
++ topLayout->setSpacing(10);
++ topLayout->addWidget(icon);
++ topLayout->addWidget(text);
++
++ QHBoxLayout *bottomLayout = new QHBoxLayout;
++ bottomLayout->addStretch();
++ bottomLayout->addWidget(quitButton);
++ bottomLayout->addStretch();
++
++ QVBoxLayout *mainLayout = new QVBoxLayout;
++ mainLayout->addLayout(topLayout);
++ mainLayout->addLayout(bottomLayout);
++
++ QDialog about(this);
++ about.setModal(true);
++ about.setWindowTitle(tr("About Torrent Client"));
++ about.setLayout(mainLayout);
++
++ connect(quitButton, &QPushButton::clicked, &about, &QDialog::close);
++
++ about.exec();
++ }
++
++ void MainWindow::acceptFileDrop(const QString &fileName)
++ {
++ // Create and show the "Add Torrent" dialog.
++ AddTorrentDialog *addTorrentDialog = new AddTorrentDialog;
++ lastDirectory = QFileInfo(fileName).absolutePath();
++ addTorrentDialog->setTorrent(fileName);
++ addTorrentDialog->deleteLater();
++ if (!addTorrentDialog->exec())
++ return;
++
++ // Add the torrent to our list of downloads.
++ addTorrent(fileName, addTorrentDialog->destinationFolder());
++ saveSettings();
++ }
++
++ void MainWindow::closeEvent(QCloseEvent *)
++ {
++ if (jobs.isEmpty())
++ return;
++
++ // Save upload / download numbers.
++ saveSettings();
++ saveChanges = false;
++
++ quitDialog = new QProgressDialog(tr("Disconnecting from trackers"), tr("Abort"), 0, jobsToStop, this);
++
++ // Stop all clients, remove the rows from the view and wait for
++ // them to signal that they have stopped.
++ jobsToStop = 0;
++ jobsStopped = 0;
++ for (const Job &job : std::as_const(jobs)) {
++ ++jobsToStop;
++ TorrentClient *client = job.client;
++ client->disconnect();
++ connect(client, &TorrentClient::stopped, this, &MainWindow::torrentStopped);
++ client->stop();
++ delete torrentView->takeTopLevelItem(0);
++ }
++
++ if (jobsToStop > jobsStopped)
++ quitDialog->exec();
++ quitDialog->deleteLater();
++ quitDialog = nullptr;
++ }
++
++ TorrentView::TorrentView(QWidget *parent)
++ : QTreeWidget(parent)
++ {
++ #if QT_CONFIG(draganddrop)
++ setAcceptDrops(true);
++ #endif
++ }
++
++ #if QT_CONFIG(draganddrop)
++ void TorrentView::dragMoveEvent(QDragMoveEvent *event)
++ {
++ // Accept file actions with a '.torrent' extension.
++ QUrl url(event->mimeData()->text());
++ if (url.isValid() && url.scheme() == "file"
++ && url.path().toLower().endsWith(".torrent"))
++ event->acceptProposedAction();
++ }
++
++ void TorrentView::dropEvent(QDropEvent *event)
++ {
++ // Accept drops if the file has a '.torrent' extension and it
++ // exists.
++ QString fileName = QUrl(event->mimeData()->text()).path();
++ if (QFile::exists(fileName) && fileName.toLower().endsWith(".torrent"))
++ emit fileDropped(fileName);
++ }
++ #endif
++
++ #include "mainwindow.moc"