1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 *
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
27 */
28
29 #include "propertieswidget.h"
30
31 #include <QClipboard>
32 #include <QDateTime>
33 #include <QDebug>
34 #include <QDir>
35 #include <QHeaderView>
36 #include <QListWidgetItem>
37 #include <QMenu>
38 #include <QSplitter>
39 #include <QShortcut>
40 #include <QStackedWidget>
41 #include <QThread>
42 #include <QUrl>
43
44 #include "base/bittorrent/downloadpriority.h"
45 #include "base/bittorrent/infohash.h"
46 #include "base/bittorrent/session.h"
47 #include "base/bittorrent/torrent.h"
48 #include "base/preferences.h"
49 #include "base/unicodestrings.h"
50 #include "base/utils/fs.h"
51 #include "base/utils/misc.h"
52 #include "base/utils/string.h"
53 #include "gui/autoexpandabledialog.h"
54 #include "gui/lineedit.h"
55 #include "gui/raisedmessagebox.h"
56 #include "gui/torrentcontentfiltermodel.h"
57 #include "gui/torrentcontentmodel.h"
58 #include "gui/uithememanager.h"
59 #include "gui/utils.h"
60 #include "downloadedpiecesbar.h"
61 #include "peerlistwidget.h"
62 #include "pieceavailabilitybar.h"
63 #include "proplistdelegate.h"
64 #include "proptabbar.h"
65 #include "speedwidget.h"
66 #include "trackerlistwidget.h"
67 #include "ui_propertieswidget.h"
68
69 #ifdef Q_OS_MACOS
70 #include "gui/macutilities.h"
71 #endif
72
PropertiesWidget(QWidget * parent)73 PropertiesWidget::PropertiesWidget(QWidget *parent)
74 : QWidget(parent)
75 , m_ui(new Ui::PropertiesWidget())
76 , m_torrent(nullptr)
77 , m_handleWidth(-1)
78 {
79 m_ui->setupUi(this);
80 setAutoFillBackground(true);
81
82 m_state = VISIBLE;
83
84 // Set Properties list model
85 m_propListModel = new TorrentContentFilterModel(this);
86 m_ui->filesList->setModel(m_propListModel);
87 m_propListDelegate = new PropListDelegate(this);
88 m_ui->filesList->setItemDelegate(m_propListDelegate);
89 m_ui->filesList->setSortingEnabled(true);
90
91 // Torrent content filtering
92 m_contentFilterLine = new LineEdit(this);
93 m_contentFilterLine->setPlaceholderText(tr("Filter files..."));
94 m_contentFilterLine->setFixedWidth(Utils::Gui::scaledSize(this, 300));
95 connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::filterText);
96 m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
97
98 // SIGNAL/SLOTS
99 connect(m_ui->selectAllButton, &QPushButton::clicked, m_propListModel, &TorrentContentFilterModel::selectAll);
100 connect(m_ui->selectNoneButton, &QPushButton::clicked, m_propListModel, &TorrentContentFilterModel::selectNone);
101 connect(m_propListModel, &TorrentContentFilterModel::filteredFilesChanged, this, &PropertiesWidget::filteredFilesChanged);
102 connect(m_ui->listWebSeeds, &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayWebSeedListMenu);
103 connect(m_propListDelegate, &PropListDelegate::filteredFilesChanged, this, &PropertiesWidget::filteredFilesChanged);
104 connect(m_ui->stackedProperties, &QStackedWidget::currentChanged, this, &PropertiesWidget::loadDynamicData);
105 connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentSavePathChanged, this, &PropertiesWidget::updateSavePath);
106 connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentMetadataReceived, this, &PropertiesWidget::updateTorrentInfos);
107 connect(m_ui->filesList, &QAbstractItemView::clicked
108 , m_ui->filesList, qOverload<const QModelIndex &>(&QAbstractItemView::edit));
109 connect(m_ui->filesList, &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayFilesListMenu);
110 connect(m_ui->filesList, &QAbstractItemView::doubleClicked, this, &PropertiesWidget::openItem);
111 connect(m_ui->filesList->header(), &QHeaderView::sectionMoved, this, &PropertiesWidget::saveSettings);
112 connect(m_ui->filesList->header(), &QHeaderView::sectionResized, this, &PropertiesWidget::saveSettings);
113 connect(m_ui->filesList->header(), &QHeaderView::sortIndicatorChanged, this, &PropertiesWidget::saveSettings);
114
115 // set bar height relative to screen dpi
116 const int barHeight = Utils::Gui::scaledSize(this, 18);
117
118 // Downloaded pieces progress bar
119 m_ui->tempProgressBarArea->setVisible(false);
120 m_downloadedPieces = new DownloadedPiecesBar(this);
121 m_ui->groupBarLayout->addWidget(m_downloadedPieces, 0, 1);
122 m_downloadedPieces->setFixedHeight(barHeight);
123 m_downloadedPieces->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
124
125 // Pieces availability bar
126 m_ui->tempAvailabilityBarArea->setVisible(false);
127 m_piecesAvailability = new PieceAvailabilityBar(this);
128 m_ui->groupBarLayout->addWidget(m_piecesAvailability, 1, 1);
129 m_piecesAvailability->setFixedHeight(barHeight);
130 m_piecesAvailability->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
131
132 // Tracker list
133 m_trackerList = new TrackerListWidget(this);
134 m_ui->trackerUpButton->setIcon(UIThemeManager::instance()->getIcon("go-up"));
135 m_ui->trackerUpButton->setIconSize(Utils::Gui::smallIconSize());
136 m_ui->trackerDownButton->setIcon(UIThemeManager::instance()->getIcon("go-down"));
137 m_ui->trackerDownButton->setIconSize(Utils::Gui::smallIconSize());
138 connect(m_ui->trackerUpButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::moveSelectionUp);
139 connect(m_ui->trackerDownButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::moveSelectionDown);
140 m_ui->hBoxLayoutTrackers->insertWidget(0, m_trackerList);
141 // Peers list
142 m_peerList = new PeerListWidget(this);
143 m_ui->vBoxLayoutPeerPage->addWidget(m_peerList);
144 // Tab bar
145 m_tabBar = new PropTabBar(nullptr);
146 m_tabBar->setContentsMargins(0, 5, 0, 5);
147 m_ui->verticalLayout->addLayout(m_tabBar);
148 connect(m_tabBar, &PropTabBar::tabChanged, m_ui->stackedProperties, &QStackedWidget::setCurrentIndex);
149 connect(m_tabBar, &PropTabBar::tabChanged, this, &PropertiesWidget::saveSettings);
150 connect(m_tabBar, &PropTabBar::visibilityToggled, this, &PropertiesWidget::setVisibility);
151 connect(m_tabBar, &PropTabBar::visibilityToggled, this, &PropertiesWidget::saveSettings);
152
153 const auto *editWebSeedsHotkey = new QShortcut(Qt::Key_F2, m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
154 connect(editWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::editWebSeed);
155 const auto *deleteWebSeedsHotkey = new QShortcut(QKeySequence::Delete, m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
156 connect(deleteWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::deleteSelectedUrlSeeds);
157 connect(m_ui->listWebSeeds, &QListWidget::doubleClicked, this, &PropertiesWidget::editWebSeed);
158
159 const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
160 connect(renameFileHotkey, &QShortcut::activated, this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); });
161 const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
162 connect(openFileHotkeyReturn, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile);
163 const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
164 connect(openFileHotkeyEnter, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile);
165
166 configure();
167 connect(Preferences::instance(), &Preferences::changed, this, &PropertiesWidget::configure);
168 }
169
~PropertiesWidget()170 PropertiesWidget::~PropertiesWidget()
171 {
172 delete m_tabBar;
173 delete m_ui;
174 }
175
showPiecesAvailability(bool show)176 void PropertiesWidget::showPiecesAvailability(bool show)
177 {
178 m_ui->labelPiecesAvailability->setVisible(show);
179 m_piecesAvailability->setVisible(show);
180 m_ui->labelAverageAvailabilityVal->setVisible(show);
181 if (show || !m_downloadedPieces->isVisible())
182 m_ui->lineBelowBars->setVisible(show);
183 }
184
showPiecesDownloaded(bool show)185 void PropertiesWidget::showPiecesDownloaded(bool show)
186 {
187 m_ui->labelDownloadedPieces->setVisible(show);
188 m_downloadedPieces->setVisible(show);
189 m_ui->labelProgressVal->setVisible(show);
190 if (show || !m_piecesAvailability->isVisible())
191 m_ui->lineBelowBars->setVisible(show);
192 }
193
setVisibility(const bool visible)194 void PropertiesWidget::setVisibility(const bool visible)
195 {
196 if (!visible && (m_state == VISIBLE))
197 {
198 const int tabBarHeight = m_tabBar->geometry().height(); // take height before hiding
199 auto *hSplitter = static_cast<QSplitter *>(parentWidget());
200 m_ui->stackedProperties->setVisible(false);
201 m_slideSizes = hSplitter->sizes();
202 hSplitter->handle(1)->setVisible(false);
203 hSplitter->handle(1)->setDisabled(true);
204 m_handleWidth = hSplitter->handleWidth();
205 hSplitter->setHandleWidth(0);
206 const QList<int> sizes {(hSplitter->geometry().height() - tabBarHeight), tabBarHeight};
207 hSplitter->setSizes(sizes);
208 setMaximumSize(maximumSize().width(), tabBarHeight);
209 m_state = REDUCED;
210 return;
211 }
212
213 if (visible && (m_state == REDUCED))
214 {
215 m_ui->stackedProperties->setVisible(true);
216 auto *hSplitter = static_cast<QSplitter *>(parentWidget());
217 if (m_handleWidth != -1)
218 hSplitter->setHandleWidth(m_handleWidth);
219 hSplitter->handle(1)->setDisabled(false);
220 hSplitter->handle(1)->setVisible(true);
221 hSplitter->setSizes(m_slideSizes);
222 m_state = VISIBLE;
223 setMaximumSize(maximumSize().width(), QWIDGETSIZE_MAX);
224 // Force refresh
225 loadDynamicData();
226 }
227 }
228
clear()229 void PropertiesWidget::clear()
230 {
231 qDebug("Clearing torrent properties");
232 m_ui->labelSavePathVal->clear();
233 m_ui->labelCreatedOnVal->clear();
234 m_ui->labelTotalPiecesVal->clear();
235 m_ui->labelHashVal->clear();
236 m_ui->labelCommentVal->clear();
237 m_ui->labelProgressVal->clear();
238 m_ui->labelAverageAvailabilityVal->clear();
239 m_ui->labelWastedVal->clear();
240 m_ui->labelUpTotalVal->clear();
241 m_ui->labelDlTotalVal->clear();
242 m_ui->labelUpLimitVal->clear();
243 m_ui->labelDlLimitVal->clear();
244 m_ui->labelElapsedVal->clear();
245 m_ui->labelConnectionsVal->clear();
246 m_ui->labelReannounceInVal->clear();
247 m_ui->labelShareRatioVal->clear();
248 m_ui->listWebSeeds->clear();
249 m_ui->labelETAVal->clear();
250 m_ui->labelSeedsVal->clear();
251 m_ui->labelPeersVal->clear();
252 m_ui->labelDlSpeedVal->clear();
253 m_ui->labelUpSpeedVal->clear();
254 m_ui->labelTotalSizeVal->clear();
255 m_ui->labelCompletedOnVal->clear();
256 m_ui->labelLastSeenCompleteVal->clear();
257 m_ui->labelCreatedByVal->clear();
258 m_ui->labelAddedOnVal->clear();
259 m_trackerList->clear();
260 m_downloadedPieces->clear();
261 m_piecesAvailability->clear();
262 m_peerList->clear();
263 m_contentFilterLine->clear();
264 m_propListModel->model()->clear();
265 }
266
getCurrentTorrent() const267 BitTorrent::Torrent *PropertiesWidget::getCurrentTorrent() const
268 {
269 return m_torrent;
270 }
271
getTrackerList() const272 TrackerListWidget *PropertiesWidget::getTrackerList() const
273 {
274 return m_trackerList;
275 }
276
getPeerList() const277 PeerListWidget *PropertiesWidget::getPeerList() const
278 {
279 return m_peerList;
280 }
281
getFilesList() const282 QTreeView *PropertiesWidget::getFilesList() const
283 {
284 return m_ui->filesList;
285 }
286
updateSavePath(BitTorrent::Torrent * const torrent)287 void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent)
288 {
289 if (torrent == m_torrent)
290 m_ui->labelSavePathVal->setText(Utils::Fs::toNativePath(m_torrent->savePath()));
291 }
292
loadTrackers(BitTorrent::Torrent * const torrent)293 void PropertiesWidget::loadTrackers(BitTorrent::Torrent *const torrent)
294 {
295 if (torrent == m_torrent)
296 m_trackerList->loadTrackers();
297 }
298
updateTorrentInfos(BitTorrent::Torrent * const torrent)299 void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent)
300 {
301 if (torrent == m_torrent)
302 loadTorrentInfos(m_torrent);
303 }
304
loadTorrentInfos(BitTorrent::Torrent * const torrent)305 void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
306 {
307 clear();
308 m_torrent = torrent;
309 m_downloadedPieces->setTorrent(m_torrent);
310 m_piecesAvailability->setTorrent(m_torrent);
311 if (!m_torrent) return;
312
313 // Save path
314 updateSavePath(m_torrent);
315 // Info hash (Truncated info hash (torrent ID) with libtorrent2)
316 // TODO: Update label for this property to express its meaning more clearly (or change it to display real info hash(es))
317 m_ui->labelHashVal->setText(m_torrent->id().toString());
318 m_propListModel->model()->clear();
319 if (m_torrent->hasMetadata())
320 {
321 // Creation date
322 m_ui->labelCreatedOnVal->setText(QLocale().toString(m_torrent->creationDate(), QLocale::ShortFormat));
323
324 m_ui->labelTotalSizeVal->setText(Utils::Misc::friendlyUnit(m_torrent->totalSize()));
325
326 // Comment
327 m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped()));
328
329 // URL seeds
330 loadUrlSeeds();
331
332 m_ui->labelCreatedByVal->setText(m_torrent->creator());
333
334 // List files in torrent
335 m_propListModel->model()->setupModelData(m_torrent->info());
336
337 // Expand single-item folders recursively
338 QModelIndex currentIndex;
339 while (m_propListModel->rowCount(currentIndex) == 1)
340 {
341 currentIndex = m_propListModel->index(0, 0, currentIndex);
342 m_ui->filesList->setExpanded(currentIndex, true);
343 }
344
345 // Load file priorities
346 m_propListModel->model()->updateFilesPriorities(m_torrent->filePriorities());
347 }
348 // Load dynamic data
349 loadDynamicData();
350 }
351
readSettings()352 void PropertiesWidget::readSettings()
353 {
354 const Preferences *const pref = Preferences::instance();
355 // Restore splitter sizes
356 QStringList sizesStr = pref->getPropSplitterSizes().split(',');
357 if (sizesStr.size() == 2)
358 {
359 m_slideSizes << sizesStr.first().toInt();
360 m_slideSizes << sizesStr.last().toInt();
361 auto *hSplitter = static_cast<QSplitter *>(parentWidget());
362 hSplitter->setSizes(m_slideSizes);
363 }
364 const int currentTab = pref->getPropCurTab();
365 const bool visible = pref->getPropVisible();
366 m_ui->filesList->header()->restoreState(pref->getPropFileListState());
367 m_tabBar->setCurrentIndex(currentTab);
368 if (!visible)
369 setVisibility(false);
370 }
371
saveSettings()372 void PropertiesWidget::saveSettings()
373 {
374 Preferences *const pref = Preferences::instance();
375 pref->setPropVisible(m_state == VISIBLE);
376 // Splitter sizes
377 auto *hSplitter = static_cast<QSplitter *>(parentWidget());
378 QList<int> sizes;
379 if (m_state == VISIBLE)
380 sizes = hSplitter->sizes();
381 else
382 sizes = m_slideSizes;
383 qDebug("Sizes: %d", sizes.size());
384 if (sizes.size() == 2)
385 pref->setPropSplitterSizes(QString::number(sizes.first()) + ',' + QString::number(sizes.last()));
386 pref->setPropFileListState(m_ui->filesList->header()->saveState());
387 // Remember current tab
388 pref->setPropCurTab(m_tabBar->currentIndex());
389 }
390
reloadPreferences()391 void PropertiesWidget::reloadPreferences()
392 {
393 // Take program preferences into consideration
394 m_peerList->updatePeerHostNameResolutionState();
395 m_peerList->updatePeerCountryResolutionState();
396 }
397
loadDynamicData()398 void PropertiesWidget::loadDynamicData()
399 {
400 // Refresh only if the torrent handle is valid and visible
401 if (!m_torrent || (m_state != VISIBLE)) return;
402
403 // Transfer infos
404 switch (m_ui->stackedProperties->currentIndex())
405 {
406 case PropTabBar::MainTab:
407 {
408 m_ui->labelWastedVal->setText(Utils::Misc::friendlyUnit(m_torrent->wastedSize()));
409
410 m_ui->labelUpTotalVal->setText(tr("%1 (%2 this session)").arg(Utils::Misc::friendlyUnit(m_torrent->totalUpload())
411 , Utils::Misc::friendlyUnit(m_torrent->totalPayloadUpload())));
412
413 m_ui->labelDlTotalVal->setText(tr("%1 (%2 this session)").arg(Utils::Misc::friendlyUnit(m_torrent->totalDownload())
414 , Utils::Misc::friendlyUnit(m_torrent->totalPayloadDownload())));
415
416 m_ui->labelUpLimitVal->setText(m_torrent->uploadLimit() <= 0 ? QString::fromUtf8(C_INFINITY) : Utils::Misc::friendlyUnit(m_torrent->uploadLimit(), true));
417
418 m_ui->labelDlLimitVal->setText(m_torrent->downloadLimit() <= 0 ? QString::fromUtf8(C_INFINITY) : Utils::Misc::friendlyUnit(m_torrent->downloadLimit(), true));
419
420 QString elapsedString;
421 if (m_torrent->isSeed())
422 elapsedString = tr("%1 (seeded for %2)", "e.g. 4m39s (seeded for 3m10s)")
423 .arg(Utils::Misc::userFriendlyDuration(m_torrent->activeTime())
424 , Utils::Misc::userFriendlyDuration(m_torrent->seedingTime()));
425 else
426 elapsedString = Utils::Misc::userFriendlyDuration(m_torrent->activeTime());
427 m_ui->labelElapsedVal->setText(elapsedString);
428
429 m_ui->labelConnectionsVal->setText(tr("%1 (%2 max)", "%1 and %2 are numbers, e.g. 3 (10 max)")
430 .arg(m_torrent->connectionsCount())
431 .arg(m_torrent->connectionsLimit() < 0 ? QString::fromUtf8(C_INFINITY) : QString::number(m_torrent->connectionsLimit())));
432
433 m_ui->labelETAVal->setText(Utils::Misc::userFriendlyDuration(m_torrent->eta(), MAX_ETA));
434
435 // Update next announce time
436 m_ui->labelReannounceInVal->setText(Utils::Misc::userFriendlyDuration(m_torrent->nextAnnounce()));
437
438 // Update ratio info
439 const qreal ratio = m_torrent->realRatio();
440 m_ui->labelShareRatioVal->setText(ratio > BitTorrent::Torrent::MAX_RATIO ? QString::fromUtf8(C_INFINITY) : Utils::String::fromDouble(ratio, 2));
441
442 m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
443 .arg(QString::number(m_torrent->seedsCount())
444 , QString::number(m_torrent->totalSeedsCount())));
445
446 m_ui->labelPeersVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
447 .arg(QString::number(m_torrent->leechsCount())
448 , QString::number(m_torrent->totalLeechersCount())));
449
450 const qlonglong dlDuration = m_torrent->activeTime() - m_torrent->finishedTime();
451 const QString dlAvg = Utils::Misc::friendlyUnit((m_torrent->totalDownload() / ((dlDuration == 0) ? -1 : dlDuration)), true);
452 m_ui->labelDlSpeedVal->setText(tr("%1 (%2 avg.)", "%1 and %2 are speed rates, e.g. 200KiB/s (100KiB/s avg.)")
453 .arg(Utils::Misc::friendlyUnit(m_torrent->downloadPayloadRate(), true), dlAvg));
454
455 const qlonglong ulDuration = m_torrent->activeTime();
456 const QString ulAvg = Utils::Misc::friendlyUnit((m_torrent->totalUpload() / ((ulDuration == 0) ? -1 : ulDuration)), true);
457 m_ui->labelUpSpeedVal->setText(tr("%1 (%2 avg.)", "%1 and %2 are speed rates, e.g. 200KiB/s (100KiB/s avg.)")
458 .arg(Utils::Misc::friendlyUnit(m_torrent->uploadPayloadRate(), true), ulAvg));
459
460 m_ui->labelLastSeenCompleteVal->setText(m_torrent->lastSeenComplete().isValid() ? QLocale().toString(m_torrent->lastSeenComplete(), QLocale::ShortFormat) : tr("Never"));
461
462 m_ui->labelCompletedOnVal->setText(m_torrent->completedTime().isValid() ? QLocale().toString(m_torrent->completedTime(), QLocale::ShortFormat) : QString {});
463
464 m_ui->labelAddedOnVal->setText(QLocale().toString(m_torrent->addedTime(), QLocale::ShortFormat));
465
466 if (m_torrent->hasMetadata())
467 {
468 m_ui->labelTotalPiecesVal->setText(tr("%1 x %2 (have %3)", "(torrent pieces) eg 152 x 4MB (have 25)").arg(m_torrent->piecesCount()).arg(Utils::Misc::friendlyUnit(m_torrent->pieceLength())).arg(m_torrent->piecesHave()));
469
470 if (!m_torrent->isSeed() && !m_torrent->isPaused() && !m_torrent->isQueued() && !m_torrent->isChecking())
471 {
472 // Pieces availability
473 showPiecesAvailability(true);
474 m_piecesAvailability->setAvailability(m_torrent->pieceAvailability());
475 m_ui->labelAverageAvailabilityVal->setText(Utils::String::fromDouble(m_torrent->distributedCopies(), 3));
476 }
477 else
478 {
479 showPiecesAvailability(false);
480 }
481
482 // Progress
483 qreal progress = m_torrent->progress() * 100.;
484 m_ui->labelProgressVal->setText(Utils::String::fromDouble(progress, 1) + '%');
485 m_downloadedPieces->setProgress(m_torrent->pieces(), m_torrent->downloadingPieces());
486 }
487 else
488 {
489 showPiecesAvailability(false);
490 }
491 }
492 break;
493 case PropTabBar::TrackersTab:
494 // Trackers
495 m_trackerList->loadTrackers();
496 break;
497 case PropTabBar::PeersTab:
498 // Load peers
499 m_peerList->loadPeers(m_torrent);
500 break;
501 case PropTabBar::FilesTab:
502 // Files progress
503 if (m_torrent->hasMetadata())
504 {
505 qDebug("Updating priorities in files tab");
506 m_ui->filesList->setUpdatesEnabled(false);
507 m_propListModel->model()->updateFilesProgress(m_torrent->filesProgress());
508 m_propListModel->model()->updateFilesAvailability(m_torrent->availableFileFractions());
509 // XXX: We don't update file priorities regularly for performance
510 // reasons. This means that priorities will not be updated if
511 // set from the Web UI.
512 // PropListModel->model()->updateFilesPriorities(h.file_priorities());
513 m_ui->filesList->setUpdatesEnabled(true);
514 }
515 break;
516 default:;
517 }
518 }
519
loadUrlSeeds()520 void PropertiesWidget::loadUrlSeeds()
521 {
522 if (!m_torrent)
523 return;
524
525 m_ui->listWebSeeds->clear();
526 qDebug("Loading URL seeds");
527 const QVector<QUrl> hcSeeds = m_torrent->urlSeeds();
528 // Add url seeds
529 for (const QUrl &hcSeed : hcSeeds)
530 {
531 qDebug("Loading URL seed: %s", qUtf8Printable(hcSeed.toString()));
532 new QListWidgetItem(hcSeed.toString(), m_ui->listWebSeeds);
533 }
534 }
535
getFullPath(const QModelIndex & index) const536 QString PropertiesWidget::getFullPath(const QModelIndex &index) const
537 {
538 if (m_propListModel->itemType(index) == TorrentContentModelItem::FileType)
539 {
540 const int fileIdx = m_propListModel->getFileIndex(index);
541 const QString filename {m_torrent->filePath(fileIdx)};
542 const QDir saveDir {m_torrent->savePath(true)};
543 const QString fullPath {Utils::Fs::expandPath(saveDir.absoluteFilePath(filename))};
544 return fullPath;
545 }
546
547 // folder type
548 const QModelIndex nameIndex {index.sibling(index.row(), TorrentContentModelItem::COL_NAME)};
549 QString folderPath {nameIndex.data().toString()};
550 for (QModelIndex modelIdx = m_propListModel->parent(nameIndex); modelIdx.isValid(); modelIdx = modelIdx.parent())
551 folderPath.prepend(modelIdx.data().toString() + '/');
552
553 const QDir saveDir {m_torrent->savePath(true)};
554 const QString fullPath {Utils::Fs::expandPath(saveDir.absoluteFilePath(folderPath))};
555 return fullPath;
556 }
557
openItem(const QModelIndex & index) const558 void PropertiesWidget::openItem(const QModelIndex &index) const
559 {
560 if (!index.isValid())
561 return;
562
563 m_torrent->flushCache(); // Flush data
564 Utils::Gui::openPath(getFullPath(index));
565 }
566
openParentFolder(const QModelIndex & index) const567 void PropertiesWidget::openParentFolder(const QModelIndex &index) const
568 {
569 const QString path = getFullPath(index);
570 m_torrent->flushCache(); // Flush data
571 #ifdef Q_OS_MACOS
572 MacUtils::openFiles({path});
573 #else
574 Utils::Gui::openFolderSelect(path);
575 #endif
576 }
577
displayFilesListMenu(const QPoint &)578 void PropertiesWidget::displayFilesListMenu(const QPoint &)
579 {
580 if (!m_torrent) return;
581
582 const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0);
583 if (selectedRows.empty()) return;
584
585 QMenu *menu = new QMenu(this);
586 menu->setAttribute(Qt::WA_DeleteOnClose);
587
588 if (selectedRows.size() == 1)
589 {
590 const QModelIndex index = selectedRows[0];
591
592 menu->addAction(UIThemeManager::instance()->getIcon("folder-documents"), tr("Open")
593 , this, [this, index]() { openItem(index); });
594 menu->addAction(UIThemeManager::instance()->getIcon("inode-directory"), tr("Open Containing Folder")
595 , this, [this, index]() { openParentFolder(index); });
596 menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename...")
597 , this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); });
598 menu->addSeparator();
599 }
600
601 if (!m_torrent->isSeed())
602 {
603 const auto applyPriorities = [this](const BitTorrent::DownloadPriority prio)
604 {
605 const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0);
606 for (const QModelIndex &index : selectedRows)
607 {
608 m_propListModel->setData(index.sibling(index.row(), PRIORITY)
609 , static_cast<int>(prio));
610 }
611
612 // Save changes
613 this->applyPriorities();
614 };
615
616 QMenu *subMenu = menu->addMenu(tr("Priority"));
617
618 subMenu->addAction(tr("Do not download"), subMenu, [applyPriorities]()
619 {
620 applyPriorities(BitTorrent::DownloadPriority::Ignored);
621 });
622 subMenu->addAction(tr("Normal"), subMenu, [applyPriorities]()
623 {
624 applyPriorities(BitTorrent::DownloadPriority::Normal);
625 });
626 subMenu->addAction(tr("High"), subMenu, [applyPriorities]()
627 {
628 applyPriorities(BitTorrent::DownloadPriority::High);
629 });
630 subMenu->addAction(tr("Maximum"), subMenu, [applyPriorities]()
631 {
632 applyPriorities(BitTorrent::DownloadPriority::Maximum);
633 });
634 subMenu->addSeparator();
635 subMenu->addAction(tr("By shown file order"), subMenu, [this]()
636 {
637 // Equally distribute the selected items into groups and for each group assign
638 // a download priority that will apply to each item. The number of groups depends on how
639 // many "download priority" are available to be assigned
640
641 const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0);
642
643 const int priorityGroups = 3;
644 const int priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1);
645
646 for (int i = 0; i < selectedRows.length(); ++i)
647 {
648 auto priority = BitTorrent::DownloadPriority::Ignored;
649 switch (i / priorityGroupSize)
650 {
651 case 0:
652 priority = BitTorrent::DownloadPriority::Maximum;
653 break;
654 case 1:
655 priority = BitTorrent::DownloadPriority::High;
656 break;
657 default:
658 case 2:
659 priority = BitTorrent::DownloadPriority::Normal;
660 break;
661 }
662
663 const QModelIndex &index = selectedRows[i];
664 m_propListModel->setData(index.sibling(index.row(), PRIORITY)
665 , static_cast<int>(priority));
666
667 // Save changes
668 this->applyPriorities();
669 }
670 });
671 }
672
673 // The selected torrent might have disappeared during exec()
674 // so we just close menu when an appropriate model is reset
675 connect(m_ui->filesList->model(), &QAbstractItemModel::modelAboutToBeReset
676 , menu, [menu]()
677 {
678 menu->setActiveAction(nullptr);
679 menu->close();
680 });
681
682 menu->popup(QCursor::pos());
683 }
684
displayWebSeedListMenu(const QPoint &)685 void PropertiesWidget::displayWebSeedListMenu(const QPoint &)
686 {
687 if (!m_torrent) return;
688
689 const QModelIndexList rows = m_ui->listWebSeeds->selectionModel()->selectedRows();
690
691 QMenu *menu = new QMenu(this);
692 menu->setAttribute(Qt::WA_DeleteOnClose);
693
694 menu->addAction(UIThemeManager::instance()->getIcon("list-add"), tr("New Web seed"), this, &PropertiesWidget::askWebSeed);
695
696 if (!rows.isEmpty())
697 {
698 menu->addAction(UIThemeManager::instance()->getIcon("list-remove"), tr("Remove Web seed")
699 , this, &PropertiesWidget::deleteSelectedUrlSeeds);
700 menu->addSeparator();
701 menu->addAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Copy Web seed URL")
702 , this, &PropertiesWidget::copySelectedWebSeedsToClipboard);
703 menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Edit Web seed URL")
704 , this, &PropertiesWidget::editWebSeed);
705 }
706
707 menu->popup(QCursor::pos());
708 }
709
openSelectedFile()710 void PropertiesWidget::openSelectedFile()
711 {
712 const QModelIndexList selectedIndexes = m_ui->filesList->selectionModel()->selectedRows(0);
713 if (selectedIndexes.size() != 1)
714 return;
715 openItem(selectedIndexes.first());
716 }
717
configure()718 void PropertiesWidget::configure()
719 {
720 // Speed widget
721 if (Preferences::instance()->isSpeedWidgetEnabled())
722 {
723 if (!m_speedWidget || !qobject_cast<SpeedWidget *>(m_speedWidget))
724 {
725 m_ui->speedLayout->removeWidget(m_speedWidget);
726 delete m_speedWidget;
727 m_speedWidget = new SpeedWidget {this};
728 m_ui->speedLayout->addWidget(m_speedWidget);
729 }
730 }
731 else
732 {
733 if (!m_speedWidget || !qobject_cast<QLabel *>(m_speedWidget))
734 {
735 m_ui->speedLayout->removeWidget(m_speedWidget);
736 delete m_speedWidget;
737 auto *label = new QLabel(tr("<center><b>Speed graphs are disabled</b><p>You may change this setting in Advanced Options </center>"), this);
738 label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
739 m_speedWidget = label;
740 m_ui->speedLayout->addWidget(m_speedWidget);
741 }
742 }
743 }
744
askWebSeed()745 void PropertiesWidget::askWebSeed()
746 {
747 bool ok = false;
748 // Ask user for a new url seed
749 const QString urlSeed = AutoExpandableDialog::getText(this, tr("New URL seed", "New HTTP source"),
750 tr("New URL seed:"), QLineEdit::Normal,
751 QLatin1String("http://www."), &ok);
752 if (!ok) return;
753 qDebug("Adding %s web seed", qUtf8Printable(urlSeed));
754 if (!m_ui->listWebSeeds->findItems(urlSeed, Qt::MatchFixedString).empty())
755 {
756 QMessageBox::warning(this, "qBittorrent",
757 tr("This URL seed is already in the list."),
758 QMessageBox::Ok);
759 return;
760 }
761 if (m_torrent)
762 m_torrent->addUrlSeeds({urlSeed});
763 // Refresh the seeds list
764 loadUrlSeeds();
765 }
766
deleteSelectedUrlSeeds()767 void PropertiesWidget::deleteSelectedUrlSeeds()
768 {
769 const QList<QListWidgetItem *> selectedItems = m_ui->listWebSeeds->selectedItems();
770 if (selectedItems.isEmpty()) return;
771
772 QVector<QUrl> urlSeeds;
773 urlSeeds.reserve(selectedItems.size());
774
775 for (const QListWidgetItem *item : selectedItems)
776 urlSeeds << item->text();
777
778 m_torrent->removeUrlSeeds(urlSeeds);
779 // Refresh list
780 loadUrlSeeds();
781 }
782
copySelectedWebSeedsToClipboard() const783 void PropertiesWidget::copySelectedWebSeedsToClipboard() const
784 {
785 const QList<QListWidgetItem *> selectedItems = m_ui->listWebSeeds->selectedItems();
786 if (selectedItems.isEmpty()) return;
787
788 QStringList urlsToCopy;
789 for (const QListWidgetItem *item : selectedItems)
790 urlsToCopy << item->text();
791
792 QApplication::clipboard()->setText(urlsToCopy.join('\n'));
793 }
794
editWebSeed()795 void PropertiesWidget::editWebSeed()
796 {
797 const QList<QListWidgetItem *> selectedItems = m_ui->listWebSeeds->selectedItems();
798 if (selectedItems.size() != 1) return;
799
800 const QListWidgetItem *selectedItem = selectedItems.last();
801 const QString oldSeed = selectedItem->text();
802 bool result;
803 const QString newSeed = AutoExpandableDialog::getText(this, tr("Web seed editing"),
804 tr("Web seed URL:"), QLineEdit::Normal,
805 oldSeed, &result);
806 if (!result) return;
807
808 if (!m_ui->listWebSeeds->findItems(newSeed, Qt::MatchFixedString).empty())
809 {
810 QMessageBox::warning(this, QLatin1String("qBittorrent"),
811 tr("This URL seed is already in the list."),
812 QMessageBox::Ok);
813 return;
814 }
815
816 m_torrent->removeUrlSeeds({oldSeed});
817 m_torrent->addUrlSeeds({newSeed});
818 loadUrlSeeds();
819 }
820
applyPriorities()821 void PropertiesWidget::applyPriorities()
822 {
823 m_torrent->prioritizeFiles(m_propListModel->model()->getFilePriorities());
824 }
825
filteredFilesChanged()826 void PropertiesWidget::filteredFilesChanged()
827 {
828 if (m_torrent)
829 applyPriorities();
830 }
831
filterText(const QString & filter)832 void PropertiesWidget::filterText(const QString &filter)
833 {
834 m_propListModel->setFilterRegExp(QRegExp(filter, Qt::CaseInsensitive, QRegExp::WildcardUnix));
835 if (filter.isEmpty())
836 {
837 m_ui->filesList->collapseAll();
838 m_ui->filesList->expand(m_propListModel->index(0, 0));
839 }
840 else
841 {
842 m_ui->filesList->expandAll();
843 }
844 }
845