2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "searchjobwidget.h"
32 #include <QApplication>
34 #include <QDesktopServices>
35 #include <QHeaderView>
38 #include <QSortFilterProxyModel>
39 #include <QStandardItemModel>
43 #include "base/bittorrent/session.h"
44 #include "base/preferences.h"
45 #include "base/search/searchdownloadhandler.h"
46 #include "base/search/searchhandler.h"
47 #include "base/search/searchpluginmanager.h"
48 #include "base/settingvalue.h"
49 #include "base/utils/misc.h"
50 #include "addnewtorrentdialog.h"
51 #include "guiiconprovider.h"
53 #include "searchlistdelegate.h"
54 #include "searchsortmodel.h"
55 #include "ui_searchjobwidget.h"
58 SearchJobWidget::SearchJobWidget(SearchHandler
*searchHandler
, QWidget
*parent
)
60 , m_ui(new Ui::SearchJobWidget
)
61 , m_searchHandler(searchHandler
)
65 // This hack fixes reordering of first column with Qt5.
66 // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
68 unused
.setVerticalHeader(m_ui
->resultsBrowser
->header());
69 m_ui
->resultsBrowser
->header()->setParent(m_ui
->resultsBrowser
);
70 unused
.setVerticalHeader(new QHeaderView(Qt::Horizontal
));
73 m_ui
->resultsBrowser
->setSelectionMode(QAbstractItemView::ExtendedSelection
);
74 header()->setStretchLastSection(false);
76 // Set Search results list model
77 m_searchListModel
= new QStandardItemModel(0, SearchSortModel::NB_SEARCH_COLUMNS
, this);
78 m_searchListModel
->setHeaderData(SearchSortModel::NAME
, Qt::Horizontal
, tr("Name", "i.e: file name"));
79 m_searchListModel
->setHeaderData(SearchSortModel::SIZE
, Qt::Horizontal
, tr("Size", "i.e: file size"));
80 m_searchListModel
->setHeaderData(SearchSortModel::SEEDS
, Qt::Horizontal
, tr("Seeders", "i.e: Number of full sources"));
81 m_searchListModel
->setHeaderData(SearchSortModel::LEECHES
, Qt::Horizontal
, tr("Leechers", "i.e: Number of partial sources"));
82 m_searchListModel
->setHeaderData(SearchSortModel::ENGINE_URL
, Qt::Horizontal
, tr("Search engine"));
83 // Set columns text alignment
84 m_searchListModel
->setHeaderData(SearchSortModel::SIZE
, Qt::Horizontal
, QVariant(Qt::AlignRight
| Qt::AlignVCenter
), Qt::TextAlignmentRole
);
85 m_searchListModel
->setHeaderData(SearchSortModel::SEEDS
, Qt::Horizontal
, QVariant(Qt::AlignRight
| Qt::AlignVCenter
), Qt::TextAlignmentRole
);
86 m_searchListModel
->setHeaderData(SearchSortModel::LEECHES
, Qt::Horizontal
, QVariant(Qt::AlignRight
| Qt::AlignVCenter
), Qt::TextAlignmentRole
);
88 m_proxyModel
= new SearchSortModel(this);
89 m_proxyModel
->setDynamicSortFilter(true);
90 m_proxyModel
->setSourceModel(m_searchListModel
);
91 m_proxyModel
->setNameFilter(searchHandler
->pattern());
92 m_ui
->resultsBrowser
->setModel(m_proxyModel
);
94 m_searchDelegate
= new SearchListDelegate(this);
95 m_ui
->resultsBrowser
->setItemDelegate(m_searchDelegate
);
97 m_ui
->resultsBrowser
->hideColumn(SearchSortModel::DL_LINK
); // Hide url column
98 m_ui
->resultsBrowser
->hideColumn(SearchSortModel::DESC_LINK
);
100 m_ui
->resultsBrowser
->setRootIsDecorated(false);
101 m_ui
->resultsBrowser
->setAllColumnsShowFocus(true);
102 m_ui
->resultsBrowser
->setSortingEnabled(true);
104 // Ensure that at least one column is visible at all times
105 bool atLeastOne
= false;
106 for (unsigned int i
= 0; i
< SearchSortModel::DL_LINK
; ++i
) {
107 if (!m_ui
->resultsBrowser
->isColumnHidden(i
)) {
113 m_ui
->resultsBrowser
->setColumnHidden(SearchSortModel::NAME
, false);
114 // To also mitigate the above issue, we have to resize each column when
115 // its size is 0, because explicitly 'showing' the column isn't enough
116 // in the above scenario.
117 for (unsigned int i
= 0; i
< SearchSortModel::DL_LINK
; ++i
)
118 if ((m_ui
->resultsBrowser
->columnWidth(i
) <= 0) && !m_ui
->resultsBrowser
->isColumnHidden(i
))
119 m_ui
->resultsBrowser
->resizeColumnToContents(i
);
121 header()->setContextMenuPolicy(Qt::CustomContextMenu
);
122 connect(header(), &QWidget::customContextMenuRequested
, this, &SearchJobWidget::displayToggleColumnsMenu
);
123 connect(header(), &QHeaderView::sectionResized
, this, &SearchJobWidget::saveSettings
);
124 connect(header(), &QHeaderView::sectionMoved
, this, &SearchJobWidget::saveSettings
);
125 connect(header(), &QHeaderView::sortIndicatorChanged
, this, &SearchJobWidget::saveSettings
);
127 fillFilterComboBoxes();
131 m_lineEditSearchResultsFilter
= new LineEdit(this);
132 m_lineEditSearchResultsFilter
->setFixedWidth(Utils::Gui::scaledSize(this, 170));
133 m_lineEditSearchResultsFilter
->setPlaceholderText(tr("Filter search results..."));
134 m_ui
->horizontalLayout
->insertWidget(0, m_lineEditSearchResultsFilter
);
136 connect(m_lineEditSearchResultsFilter
, &LineEdit::textChanged
, this, &SearchJobWidget::filterSearchResults
);
137 connect(m_ui
->filterMode
, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged
)
138 , this, &SearchJobWidget::updateFilter
);
139 connect(m_ui
->minSeeds
, &QAbstractSpinBox::editingFinished
, this, &SearchJobWidget::updateFilter
);
140 connect(m_ui
->minSeeds
, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged
)
141 , this, &SearchJobWidget::updateFilter
);
142 connect(m_ui
->maxSeeds
, &QAbstractSpinBox::editingFinished
, this, &SearchJobWidget::updateFilter
);
143 connect(m_ui
->maxSeeds
, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged
)
144 , this, &SearchJobWidget::updateFilter
);
145 connect(m_ui
->minSize
, &QAbstractSpinBox::editingFinished
, this, &SearchJobWidget::updateFilter
);
146 connect(m_ui
->minSize
, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged
)
147 , this, &SearchJobWidget::updateFilter
);
148 connect(m_ui
->maxSize
, &QAbstractSpinBox::editingFinished
, this, &SearchJobWidget::updateFilter
);
149 connect(m_ui
->maxSize
, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged
)
150 , this, &SearchJobWidget::updateFilter
);
151 connect(m_ui
->minSizeUnit
, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged
)
152 , this, &SearchJobWidget::updateFilter
);
153 connect(m_ui
->maxSizeUnit
, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged
)
154 , this, &SearchJobWidget::updateFilter
);
156 connect(m_ui
->resultsBrowser
, &QAbstractItemView::doubleClicked
, this, &SearchJobWidget::onItemDoubleClicked
);
158 connect(searchHandler
, &SearchHandler::newSearchResults
, this, &SearchJobWidget::appendSearchResults
);
159 connect(searchHandler
, &SearchHandler::searchFinished
, this, &SearchJobWidget::searchFinished
);
160 connect(searchHandler
, &SearchHandler::searchFailed
, this, &SearchJobWidget::searchFailed
);
161 connect(this, &QObject::destroyed
, searchHandler
, &QObject::deleteLater
);
164 SearchJobWidget::~SearchJobWidget()
170 void SearchJobWidget::onItemDoubleClicked(const QModelIndex
&index
)
172 setRowColor(index
.row(), QApplication::palette().color(QPalette::LinkVisited
));
173 downloadTorrent(index
);
176 QHeaderView
*SearchJobWidget::header() const
178 return m_ui
->resultsBrowser
->header();
181 // Set the color of a row in data model
182 void SearchJobWidget::setRowColor(int row
, const QColor
&color
)
184 m_proxyModel
->setDynamicSortFilter(false);
185 for (int i
= 0; i
< m_proxyModel
->columnCount(); ++i
)
186 m_proxyModel
->setData(m_proxyModel
->index(row
, i
), color
, Qt::ForegroundRole
);
188 m_proxyModel
->setDynamicSortFilter(true);
191 SearchJobWidget::Status
SearchJobWidget::status() const
196 int SearchJobWidget::visibleResultsCount() const
198 return m_proxyModel
->rowCount();
201 LineEdit
*SearchJobWidget::lineEditSearchResultsFilter() const
203 return m_lineEditSearchResultsFilter
;
206 void SearchJobWidget::cancelSearch()
208 m_searchHandler
->cancelSearch();
211 void SearchJobWidget::downloadTorrents()
213 const QModelIndexList rows
{m_ui
->resultsBrowser
->selectionModel()->selectedRows()};
214 for (const QModelIndex
&rowIndex
: rows
)
215 downloadTorrent(rowIndex
);
218 void SearchJobWidget::openTorrentPages()
220 const QModelIndexList rows
{m_ui
->resultsBrowser
->selectionModel()->selectedRows()};
221 for (const QModelIndex
&rowIndex
: rows
) {
222 const QString descrLink
= m_proxyModel
->data(
223 m_proxyModel
->index(rowIndex
.row(), SearchSortModel::DESC_LINK
)).toString();
224 if (!descrLink
.isEmpty())
225 QDesktopServices::openUrl(QUrl::fromEncoded(descrLink
.toUtf8()));
229 void SearchJobWidget::copyTorrentURLs()
231 const QModelIndexList rows
{m_ui
->resultsBrowser
->selectionModel()->selectedRows()};
233 for (const QModelIndex
&rowIndex
: rows
) {
234 const QString descrLink
= m_proxyModel
->data(
235 m_proxyModel
->index(rowIndex
.row(), SearchSortModel::DESC_LINK
)).toString();
236 if (!descrLink
.isEmpty())
241 QClipboard
*clipboard
= QApplication::clipboard();
242 clipboard
->setText(urls
.join('\n'));
246 void SearchJobWidget::setStatus(Status value
)
248 if (m_status
== value
) return;
251 setStatusTip(statusText(value
));
252 emit
statusChanged();
255 void SearchJobWidget::downloadTorrent(const QModelIndex
&rowIndex
)
257 const QString torrentUrl
= m_proxyModel
->data(
258 m_proxyModel
->index(rowIndex
.row(), SearchSortModel::DL_LINK
)).toString();
259 const QString siteUrl
= m_proxyModel
->data(
260 m_proxyModel
->index(rowIndex
.row(), SearchSortModel::ENGINE_URL
)).toString();
262 if (torrentUrl
.startsWith("bc://bt/", Qt::CaseInsensitive
) || torrentUrl
.startsWith("magnet:", Qt::CaseInsensitive
)) {
263 addTorrentToSession(torrentUrl
);
266 SearchDownloadHandler
*downloadHandler
= m_searchHandler
->manager()->downloadTorrent(siteUrl
, torrentUrl
);
267 connect(downloadHandler
, &SearchDownloadHandler::downloadFinished
, this, &SearchJobWidget::addTorrentToSession
);
268 connect(downloadHandler
, &SearchDownloadHandler::downloadFinished
, downloadHandler
, &SearchDownloadHandler::deleteLater
);
272 void SearchJobWidget::addTorrentToSession(const QString
&source
)
274 if (source
.isEmpty()) return;
276 if (AddNewTorrentDialog::isEnabled())
277 AddNewTorrentDialog::show(source
, this);
279 BitTorrent::Session::instance()->addTorrent(source
);
282 void SearchJobWidget::updateResultsCount()
284 const int totalResults
= m_searchListModel
->rowCount();
285 const int filteredResults
= m_proxyModel
->rowCount();
286 m_ui
->resultsLbl
->setText(tr("Results (showing <i>%1</i> out of <i>%2</i>):", "i.e: Search results")
287 .arg(filteredResults
).arg(totalResults
));
289 m_noSearchResults
= (totalResults
== 0);
290 emit
resultsCountUpdated();
293 void SearchJobWidget::updateFilter()
295 using Utils::Misc::SizeUnit
;
297 m_proxyModel
->enableNameFilter(filteringMode() == NameFilteringMode::OnlyNames
);
298 // we update size and seeds filter parameters in the model even if they are disabled
299 m_proxyModel
->setSeedsFilter(m_ui
->minSeeds
->value(), m_ui
->maxSeeds
->value());
300 m_proxyModel
->setSizeFilter(
301 sizeInBytes(m_ui
->minSize
->value(), static_cast<SizeUnit
>(m_ui
->minSizeUnit
->currentIndex())),
302 sizeInBytes(m_ui
->maxSize
->value(), static_cast<SizeUnit
>(m_ui
->maxSizeUnit
->currentIndex())));
304 nameFilteringModeSetting() = filteringMode();
306 m_proxyModel
->invalidate();
307 updateResultsCount();
310 void SearchJobWidget::fillFilterComboBoxes()
312 using Utils::Misc::SizeUnit
;
313 QStringList unitStrings
;
314 unitStrings
.append(unitString(SizeUnit::Byte
));
315 unitStrings
.append(unitString(SizeUnit::KibiByte
));
316 unitStrings
.append(unitString(SizeUnit::MebiByte
));
317 unitStrings
.append(unitString(SizeUnit::GibiByte
));
318 unitStrings
.append(unitString(SizeUnit::TebiByte
));
319 unitStrings
.append(unitString(SizeUnit::PebiByte
));
320 unitStrings
.append(unitString(SizeUnit::ExbiByte
));
322 m_ui
->minSizeUnit
->clear();
323 m_ui
->maxSizeUnit
->clear();
324 m_ui
->minSizeUnit
->addItems(unitStrings
);
325 m_ui
->maxSizeUnit
->addItems(unitStrings
);
327 m_ui
->minSize
->setValue(0);
328 m_ui
->minSizeUnit
->setCurrentIndex(static_cast<int>(SizeUnit::MebiByte
));
330 m_ui
->maxSize
->setValue(-1);
331 m_ui
->maxSizeUnit
->setCurrentIndex(static_cast<int>(SizeUnit::GibiByte
));
333 m_ui
->filterMode
->clear();
335 m_ui
->filterMode
->addItem(tr("Torrent names only"), static_cast<int>(NameFilteringMode::OnlyNames
));
336 m_ui
->filterMode
->addItem(tr("Everywhere"), static_cast<int>(NameFilteringMode::Everywhere
));
338 QVariant selectedMode
= static_cast<int>(nameFilteringModeSetting().value());
339 int index
= m_ui
->filterMode
->findData(selectedMode
);
340 m_ui
->filterMode
->setCurrentIndex((index
== -1) ? 0 : index
);
343 void SearchJobWidget::filterSearchResults(const QString
&name
)
345 m_proxyModel
->setFilterRegExp(QRegExp(name
, Qt::CaseInsensitive
));
346 updateResultsCount();
349 QString
SearchJobWidget::statusText(SearchJobWidget::Status st
)
352 case Status::Ongoing
:
353 return tr("Searching...");
354 case Status::Finished
:
355 return tr("Search has finished");
356 case Status::Aborted
:
357 return tr("Search aborted");
359 return tr("An error occurred during search...");
360 case Status::NoResults
:
361 return tr("Search returned no results");
367 SearchJobWidget::NameFilteringMode
SearchJobWidget::filteringMode() const
369 return static_cast<NameFilteringMode
>(m_ui
->filterMode
->itemData(m_ui
->filterMode
->currentIndex()).toInt());
372 void SearchJobWidget::loadSettings()
374 header()->restoreState(Preferences::instance()->getSearchTabHeaderState());
377 void SearchJobWidget::saveSettings() const
379 Preferences::instance()->setSearchTabHeaderState(header()->saveState());
382 void SearchJobWidget::displayToggleColumnsMenu(const QPoint
&)
384 QMenu
hideshowColumn(this);
385 hideshowColumn
.setTitle(tr("Column visibility"));
386 QList
<QAction
*> actions
;
387 for (int i
= 0; i
< SearchSortModel::DL_LINK
; ++i
) {
388 QAction
*myAct
= hideshowColumn
.addAction(m_searchListModel
->headerData(i
, Qt::Horizontal
, Qt::DisplayRole
).toString());
389 myAct
->setCheckable(true);
390 myAct
->setChecked(!m_ui
->resultsBrowser
->isColumnHidden(i
));
391 actions
.append(myAct
);
394 for (unsigned int i
= 0; i
< SearchSortModel::DL_LINK
; ++i
) {
395 if (!m_ui
->resultsBrowser
->isColumnHidden(i
))
403 QAction
*act
= hideshowColumn
.exec(QCursor::pos());
405 int col
= actions
.indexOf(act
);
407 Q_ASSERT(visibleCols
> 0);
408 if ((!m_ui
->resultsBrowser
->isColumnHidden(col
)) && (visibleCols
== 1))
410 m_ui
->resultsBrowser
->setColumnHidden(col
, !m_ui
->resultsBrowser
->isColumnHidden(col
));
411 if ((!m_ui
->resultsBrowser
->isColumnHidden(col
)) && (m_ui
->resultsBrowser
->columnWidth(col
) <= 5))
412 m_ui
->resultsBrowser
->resizeColumnToContents(col
);
417 void SearchJobWidget::searchFinished(bool cancelled
)
420 setStatus(Status::Aborted
);
421 else if (m_noSearchResults
)
422 setStatus(Status::NoResults
);
424 setStatus(Status::Finished
);
427 void SearchJobWidget::searchFailed()
429 setStatus(Status::Error
);
432 void SearchJobWidget::appendSearchResults(const QList
<SearchResult
> &results
)
434 for (const SearchResult
&result
: results
) {
435 // Add item to search result list
436 int row
= m_searchListModel
->rowCount();
437 m_searchListModel
->insertRow(row
);
439 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::NAME
), result
.fileName
); // Name
440 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::DL_LINK
), result
.fileUrl
); // download URL
441 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::SIZE
), result
.fileSize
); // Size
442 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::SEEDS
), result
.nbSeeders
); // Seeders
443 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::LEECHES
), result
.nbLeechers
); // Leechers
444 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::ENGINE_URL
), result
.siteUrl
); // Search site URL
445 m_searchListModel
->setData(m_searchListModel
->index(row
, SearchSortModel::DESC_LINK
), result
.descrLink
); // Description Link
448 updateResultsCount();
451 CachedSettingValue
<SearchJobWidget::NameFilteringMode
> &SearchJobWidget::nameFilteringModeSetting()
453 static CachedSettingValue
<NameFilteringMode
> setting("Search/FilteringMode", NameFilteringMode::OnlyNames
);