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