Sync translations from Transifex and run lupdate
[qBittorrent.git] / src / gui / search / searchjobwidget.cpp
blob54272432a795aa768598a0ffe8e2c3859db4a62a
1 /*
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>
33 #include <QClipboard>
34 #include <QDesktopServices>
35 #include <QHeaderView>
36 #include <QKeyEvent>
37 #include <QMenu>
38 #include <QPalette>
39 #include <QStandardItemModel>
40 #include <QTableView>
41 #include <QUrl>
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 "gui/addnewtorrentdialog.h"
51 #include "gui/lineedit.h"
52 #include "gui/uithememanager.h"
53 #include "gui/utils.h"
54 #include "searchsortmodel.h"
55 #include "ui_searchjobwidget.h"
57 SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, QWidget *parent)
58 : QWidget(parent)
59 , m_ui(new Ui::SearchJobWidget)
60 , m_searchHandler(searchHandler)
62 m_ui->setupUi(this);
64 // This hack fixes reordering of first column with Qt5.
65 // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
66 QTableView unused;
67 unused.setVerticalHeader(m_ui->resultsBrowser->header());
68 m_ui->resultsBrowser->header()->setParent(m_ui->resultsBrowser);
69 unused.setVerticalHeader(new QHeaderView(Qt::Horizontal));
71 loadSettings();
73 header()->setStretchLastSection(false);
75 // Set Search results list model
76 m_searchListModel = new QStandardItemModel(0, SearchSortModel::NB_SEARCH_COLUMNS, this);
77 m_searchListModel->setHeaderData(SearchSortModel::NAME, Qt::Horizontal, tr("Name", "i.e: file name"));
78 m_searchListModel->setHeaderData(SearchSortModel::SIZE, Qt::Horizontal, tr("Size", "i.e: file size"));
79 m_searchListModel->setHeaderData(SearchSortModel::SEEDS, Qt::Horizontal, tr("Seeders", "i.e: Number of full sources"));
80 m_searchListModel->setHeaderData(SearchSortModel::LEECHES, Qt::Horizontal, tr("Leechers", "i.e: Number of partial sources"));
81 m_searchListModel->setHeaderData(SearchSortModel::ENGINE_URL, Qt::Horizontal, tr("Search engine"));
82 // Set columns text alignment
83 m_searchListModel->setHeaderData(SearchSortModel::SIZE, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
84 m_searchListModel->setHeaderData(SearchSortModel::SEEDS, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
85 m_searchListModel->setHeaderData(SearchSortModel::LEECHES, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
87 m_proxyModel = new SearchSortModel(this);
88 m_proxyModel->setDynamicSortFilter(true);
89 m_proxyModel->setSourceModel(m_searchListModel);
90 m_proxyModel->setNameFilter(searchHandler->pattern());
91 m_ui->resultsBrowser->setModel(m_proxyModel);
93 m_ui->resultsBrowser->hideColumn(SearchSortModel::DL_LINK); // Hide url column
94 m_ui->resultsBrowser->hideColumn(SearchSortModel::DESC_LINK);
96 m_ui->resultsBrowser->setSelectionMode(QAbstractItemView::ExtendedSelection);
97 m_ui->resultsBrowser->setRootIsDecorated(false);
98 m_ui->resultsBrowser->setAllColumnsShowFocus(true);
99 m_ui->resultsBrowser->setSortingEnabled(true);
100 m_ui->resultsBrowser->setEditTriggers(QAbstractItemView::NoEditTriggers);
102 // Ensure that at least one column is visible at all times
103 bool atLeastOne = false;
104 for (int i = 0; i < SearchSortModel::DL_LINK; ++i)
106 if (!m_ui->resultsBrowser->isColumnHidden(i))
108 atLeastOne = true;
109 break;
112 if (!atLeastOne)
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 (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();
129 updateFilter();
131 m_lineEditSearchResultsFilter = new LineEdit(this);
132 m_lineEditSearchResultsFilter->setPlaceholderText(tr("Filter search results..."));
133 m_lineEditSearchResultsFilter->setContextMenuPolicy(Qt::CustomContextMenu);
134 connect(m_lineEditSearchResultsFilter, &QWidget::customContextMenuRequested, this, &SearchJobWidget::showFilterContextMenu);
135 m_ui->horizontalLayout->insertWidget(0, m_lineEditSearchResultsFilter);
137 connect(m_lineEditSearchResultsFilter, &LineEdit::textChanged, this, &SearchJobWidget::filterSearchResults);
138 connect(m_ui->filterMode, qOverload<int>(&QComboBox::currentIndexChanged)
139 , this, &SearchJobWidget::updateFilter);
140 connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
141 connect(m_ui->minSeeds, qOverload<int>(&QSpinBox::valueChanged)
142 , this, &SearchJobWidget::updateFilter);
143 connect(m_ui->maxSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
144 connect(m_ui->maxSeeds, qOverload<int>(&QSpinBox::valueChanged)
145 , this, &SearchJobWidget::updateFilter);
146 connect(m_ui->minSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
147 connect(m_ui->minSize, qOverload<double>(&QDoubleSpinBox::valueChanged)
148 , this, &SearchJobWidget::updateFilter);
149 connect(m_ui->maxSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
150 connect(m_ui->maxSize, qOverload<double>(&QDoubleSpinBox::valueChanged)
151 , this, &SearchJobWidget::updateFilter);
152 connect(m_ui->minSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged)
153 , this, &SearchJobWidget::updateFilter);
154 connect(m_ui->maxSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged)
155 , this, &SearchJobWidget::updateFilter);
157 connect(m_ui->resultsBrowser, &QAbstractItemView::doubleClicked, this, &SearchJobWidget::onItemDoubleClicked);
159 connect(searchHandler, &SearchHandler::newSearchResults, this, &SearchJobWidget::appendSearchResults);
160 connect(searchHandler, &SearchHandler::searchFinished, this, &SearchJobWidget::searchFinished);
161 connect(searchHandler, &SearchHandler::searchFailed, this, &SearchJobWidget::searchFailed);
162 connect(this, &QObject::destroyed, searchHandler, &QObject::deleteLater);
164 setStatusTip(statusText(m_status));
167 SearchJobWidget::~SearchJobWidget()
169 saveSettings();
170 delete m_ui;
173 void SearchJobWidget::onItemDoubleClicked(const QModelIndex &index)
175 downloadTorrent(index);
178 QHeaderView *SearchJobWidget::header() const
180 return m_ui->resultsBrowser->header();
183 // Set the color of a row in data model
184 void SearchJobWidget::setRowColor(int row, const QColor &color)
186 m_proxyModel->setDynamicSortFilter(false);
187 for (int i = 0; i < m_proxyModel->columnCount(); ++i)
188 m_proxyModel->setData(m_proxyModel->index(row, i), color, Qt::ForegroundRole);
190 m_proxyModel->setDynamicSortFilter(true);
193 SearchJobWidget::Status SearchJobWidget::status() const
195 return m_status;
198 int SearchJobWidget::visibleResultsCount() const
200 return m_proxyModel->rowCount();
203 LineEdit *SearchJobWidget::lineEditSearchResultsFilter() const
205 return m_lineEditSearchResultsFilter;
208 void SearchJobWidget::cancelSearch()
210 m_searchHandler->cancelSearch();
213 void SearchJobWidget::downloadTorrents()
215 const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()};
216 for (const QModelIndex &rowIndex : rows)
217 downloadTorrent(rowIndex);
220 void SearchJobWidget::openTorrentPages() const
222 const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()};
223 for (const QModelIndex &rowIndex : rows)
225 const QString descrLink = m_proxyModel->data(
226 m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK)).toString();
227 if (!descrLink.isEmpty())
228 QDesktopServices::openUrl(QUrl::fromEncoded(descrLink.toUtf8()));
232 void SearchJobWidget::copyTorrentURLs() const
234 copyField(SearchSortModel::DESC_LINK);
237 void SearchJobWidget::copyTorrentDownloadLinks() const
239 copyField(SearchSortModel::DL_LINK);
242 void SearchJobWidget::copyTorrentNames() const
244 copyField(SearchSortModel::NAME);
247 void SearchJobWidget::copyField(const int column) const
249 const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()};
250 QStringList list;
252 for (const QModelIndex &rowIndex : rows)
254 const QString field = m_proxyModel->data(
255 m_proxyModel->index(rowIndex.row(), column)).toString();
256 if (!field.isEmpty())
257 list << field;
260 if (!list.empty())
261 QApplication::clipboard()->setText(list.join('\n'));
264 void SearchJobWidget::setStatus(Status value)
266 if (m_status == value) return;
268 m_status = value;
269 setStatusTip(statusText(value));
270 emit statusChanged();
273 void SearchJobWidget::downloadTorrent(const QModelIndex &rowIndex)
275 const QString torrentUrl = m_proxyModel->data(
276 m_proxyModel->index(rowIndex.row(), SearchSortModel::DL_LINK)).toString();
277 const QString siteUrl = m_proxyModel->data(
278 m_proxyModel->index(rowIndex.row(), SearchSortModel::ENGINE_URL)).toString();
280 if (torrentUrl.startsWith("magnet:", Qt::CaseInsensitive))
282 addTorrentToSession(torrentUrl);
284 else
286 SearchDownloadHandler *downloadHandler = m_searchHandler->manager()->downloadTorrent(siteUrl, torrentUrl);
287 connect(downloadHandler, &SearchDownloadHandler::downloadFinished, this, &SearchJobWidget::addTorrentToSession);
288 connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater);
290 setRowColor(rowIndex.row(), QApplication::palette().color(QPalette::LinkVisited));
293 void SearchJobWidget::addTorrentToSession(const QString &source)
295 if (source.isEmpty()) return;
297 if (AddNewTorrentDialog::isEnabled())
298 AddNewTorrentDialog::show(source, this);
299 else
300 BitTorrent::Session::instance()->addTorrent(source);
303 void SearchJobWidget::updateResultsCount()
305 const int totalResults = m_searchListModel->rowCount();
306 const int filteredResults = m_proxyModel->rowCount();
307 m_ui->resultsLbl->setText(tr("Results (showing <i>%1</i> out of <i>%2</i>):", "i.e: Search results")
308 .arg(filteredResults).arg(totalResults));
310 m_noSearchResults = (totalResults == 0);
311 emit resultsCountUpdated();
314 void SearchJobWidget::updateFilter()
316 using Utils::Misc::SizeUnit;
318 m_proxyModel->enableNameFilter(filteringMode() == NameFilteringMode::OnlyNames);
319 // we update size and seeds filter parameters in the model even if they are disabled
320 m_proxyModel->setSeedsFilter(m_ui->minSeeds->value(), m_ui->maxSeeds->value());
321 m_proxyModel->setSizeFilter(
322 sizeInBytes(m_ui->minSize->value(), static_cast<SizeUnit>(m_ui->minSizeUnit->currentIndex())),
323 sizeInBytes(m_ui->maxSize->value(), static_cast<SizeUnit>(m_ui->maxSizeUnit->currentIndex())));
325 nameFilteringModeSetting() = filteringMode();
327 m_proxyModel->invalidate();
328 updateResultsCount();
331 void SearchJobWidget::fillFilterComboBoxes()
333 using Utils::Misc::SizeUnit;
334 using Utils::Misc::unitString;
336 QStringList unitStrings;
337 unitStrings.append(unitString(SizeUnit::Byte));
338 unitStrings.append(unitString(SizeUnit::KibiByte));
339 unitStrings.append(unitString(SizeUnit::MebiByte));
340 unitStrings.append(unitString(SizeUnit::GibiByte));
341 unitStrings.append(unitString(SizeUnit::TebiByte));
342 unitStrings.append(unitString(SizeUnit::PebiByte));
343 unitStrings.append(unitString(SizeUnit::ExbiByte));
345 m_ui->minSizeUnit->clear();
346 m_ui->maxSizeUnit->clear();
347 m_ui->minSizeUnit->addItems(unitStrings);
348 m_ui->maxSizeUnit->addItems(unitStrings);
350 m_ui->minSize->setValue(0);
351 m_ui->minSizeUnit->setCurrentIndex(static_cast<int>(SizeUnit::MebiByte));
353 m_ui->maxSize->setValue(-1);
354 m_ui->maxSizeUnit->setCurrentIndex(static_cast<int>(SizeUnit::GibiByte));
356 m_ui->filterMode->clear();
358 m_ui->filterMode->addItem(tr("Torrent names only"), static_cast<int>(NameFilteringMode::OnlyNames));
359 m_ui->filterMode->addItem(tr("Everywhere"), static_cast<int>(NameFilteringMode::Everywhere));
361 QVariant selectedMode = static_cast<int>(nameFilteringModeSetting().get(NameFilteringMode::OnlyNames));
362 int index = m_ui->filterMode->findData(selectedMode);
363 m_ui->filterMode->setCurrentIndex((index == -1) ? 0 : index);
366 void SearchJobWidget::filterSearchResults(const QString &name)
368 const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForSearchJob()
369 ? name : Utils::String::wildcardToRegexPattern(name));
370 m_proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
371 updateResultsCount();
374 void SearchJobWidget::showFilterContextMenu(const QPoint &)
376 const Preferences *pref = Preferences::instance();
378 QMenu *menu = m_lineEditSearchResultsFilter->createStandardContextMenu();
379 menu->setAttribute(Qt::WA_DeleteOnClose);
380 menu->addSeparator();
382 QAction *useRegexAct = menu->addAction(tr("Use regular expressions"));
383 useRegexAct->setCheckable(true);
384 useRegexAct->setChecked(pref->getRegexAsFilteringPatternForSearchJob());
385 connect(useRegexAct, &QAction::toggled, pref, &Preferences::setRegexAsFilteringPatternForSearchJob);
386 connect(useRegexAct, &QAction::toggled, this, [this]() { filterSearchResults(m_lineEditSearchResultsFilter->text()); });
388 menu->popup(QCursor::pos());
391 void SearchJobWidget::contextMenuEvent(QContextMenuEvent *event)
393 auto *menu = new QMenu(this);
394 menu->setAttribute(Qt::WA_DeleteOnClose);
396 menu->addAction(UIThemeManager::instance()->getIcon("download"), tr("Download")
397 , this, &SearchJobWidget::downloadTorrents);
398 menu->addSeparator();
399 menu->addAction(UIThemeManager::instance()->getIcon("application-x-mswinurl"), tr("Open description page")
400 , this, &SearchJobWidget::openTorrentPages);
402 QMenu *copySubMenu = menu->addMenu(
403 UIThemeManager::instance()->getIcon("edit-copy"), tr("Copy"));
405 copySubMenu->addAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Name")
406 , this, &SearchJobWidget::copyTorrentNames);
407 copySubMenu->addAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Download link")
408 , this, &SearchJobWidget::copyTorrentDownloadLinks);
409 copySubMenu->addAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Description page URL")
410 , this, &SearchJobWidget::copyTorrentURLs);
412 menu->popup(event->globalPos());
415 QString SearchJobWidget::statusText(SearchJobWidget::Status st)
417 switch (st)
419 case Status::Ongoing:
420 return tr("Searching...");
421 case Status::Finished:
422 return tr("Search has finished");
423 case Status::Aborted:
424 return tr("Search aborted");
425 case Status::Error:
426 return tr("An error occurred during search...");
427 case Status::NoResults:
428 return tr("Search returned no results");
429 default:
430 return {};
434 SearchJobWidget::NameFilteringMode SearchJobWidget::filteringMode() const
436 return static_cast<NameFilteringMode>(m_ui->filterMode->itemData(m_ui->filterMode->currentIndex()).toInt());
439 void SearchJobWidget::loadSettings()
441 header()->restoreState(Preferences::instance()->getSearchTabHeaderState());
444 void SearchJobWidget::saveSettings() const
446 Preferences::instance()->setSearchTabHeaderState(header()->saveState());
449 void SearchJobWidget::displayToggleColumnsMenu(const QPoint &)
451 auto menu = new QMenu(this);
452 menu->setAttribute(Qt::WA_DeleteOnClose);
453 menu->setTitle(tr("Column visibility"));
455 for (int i = 0; i < SearchSortModel::DL_LINK; ++i)
457 QAction *myAct = menu->addAction(m_searchListModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString());
458 myAct->setCheckable(true);
459 myAct->setChecked(!m_ui->resultsBrowser->isColumnHidden(i));
460 myAct->setData(i);
463 connect(menu, &QMenu::triggered, this, [this](const QAction *action)
465 int visibleCols = 0;
466 for (int i = 0; i < SearchSortModel::DL_LINK; ++i)
468 if (!m_ui->resultsBrowser->isColumnHidden(i))
469 ++visibleCols;
471 if (visibleCols > 1)
472 break;
475 const int col = action->data().toInt();
477 if ((!m_ui->resultsBrowser->isColumnHidden(col)) && (visibleCols == 1))
478 return;
480 m_ui->resultsBrowser->setColumnHidden(col, !m_ui->resultsBrowser->isColumnHidden(col));
482 if ((!m_ui->resultsBrowser->isColumnHidden(col)) && (m_ui->resultsBrowser->columnWidth(col) <= 5))
483 m_ui->resultsBrowser->resizeColumnToContents(col);
485 saveSettings();
488 menu->popup(QCursor::pos());
491 void SearchJobWidget::searchFinished(bool cancelled)
493 if (cancelled)
494 setStatus(Status::Aborted);
495 else if (m_noSearchResults)
496 setStatus(Status::NoResults);
497 else
498 setStatus(Status::Finished);
501 void SearchJobWidget::searchFailed()
503 setStatus(Status::Error);
506 void SearchJobWidget::appendSearchResults(const QVector<SearchResult> &results)
508 for (const SearchResult &result : results)
510 // Add item to search result list
511 int row = m_searchListModel->rowCount();
512 m_searchListModel->insertRow(row);
514 const auto setModelData = [this, row] (const int column, const QString &displayData
515 , const QVariant &underlyingData, const Qt::Alignment textAlignmentData = {})
517 const QMap<int, QVariant> data =
519 {Qt::DisplayRole, displayData},
520 {SearchSortModel::UnderlyingDataRole, underlyingData},
521 {Qt::TextAlignmentRole, QVariant {textAlignmentData}}
523 m_searchListModel->setItemData(m_searchListModel->index(row, column), data);
526 setModelData(SearchSortModel::NAME, result.fileName, result.fileName);
527 setModelData(SearchSortModel::DL_LINK, result.fileUrl, result.fileUrl);
528 setModelData(SearchSortModel::ENGINE_URL, result.siteUrl, result.siteUrl);
529 setModelData(SearchSortModel::DESC_LINK, result.descrLink, result.descrLink);
530 setModelData(SearchSortModel::SIZE, Utils::Misc::friendlyUnit(result.fileSize), result.fileSize, (Qt::AlignRight | Qt::AlignVCenter));
531 setModelData(SearchSortModel::SEEDS, QString::number(result.nbSeeders), result.nbSeeders, (Qt::AlignRight | Qt::AlignVCenter));
532 setModelData(SearchSortModel::LEECHES, QString::number(result.nbLeechers), result.nbLeechers, (Qt::AlignRight | Qt::AlignVCenter));
535 updateResultsCount();
538 SettingValue<SearchJobWidget::NameFilteringMode> &SearchJobWidget::nameFilteringModeSetting()
540 static SettingValue<NameFilteringMode> setting {"Search/FilteringMode"};
541 return setting;
544 void SearchJobWidget::keyPressEvent(QKeyEvent *event)
546 switch (event->key())
548 case Qt::Key_Enter:
549 case Qt::Key_Return:
550 downloadTorrents();
551 break;
552 default:
553 QWidget::keyPressEvent(event);