Add hotkey for toggling focus between the search LineEdits
[qBittorrent.git] / src / gui / search / searchjobwidget.cpp
blob8be2a611d7657dd0afe80c56572f4de436d5f1fd
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 <QMenu>
37 #include <QPalette>
38 #include <QSortFilterProxyModel>
39 #include <QStandardItemModel>
40 #include <QTableView>
41 #include <QTreeView>
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"
52 #include "lineedit.h"
53 #include "searchlistdelegate.h"
54 #include "searchsortmodel.h"
55 #include "ui_searchjobwidget.h"
56 #include "utils.h"
58 SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, QWidget *parent)
59 : QWidget(parent)
60 , m_ui(new Ui::SearchJobWidget)
61 , m_searchHandler(searchHandler)
63 m_ui->setupUi(this);
65 // This hack fixes reordering of first column with Qt5.
66 // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
67 QTableView unused;
68 unused.setVerticalHeader(m_ui->resultsBrowser->header());
69 m_ui->resultsBrowser->header()->setParent(m_ui->resultsBrowser);
70 unused.setVerticalHeader(new QHeaderView(Qt::Horizontal));
72 loadSettings();
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)) {
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 (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();
129 updateFilter();
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()
166 saveSettings();
167 delete m_ui;
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
193 return m_status;
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()};
232 QStringList urls;
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())
237 urls << descrLink;
240 if (!urls.empty()) {
241 QClipboard *clipboard = QApplication::clipboard();
242 clipboard->setText(urls.join('\n'));
246 void SearchJobWidget::setStatus(Status value)
248 if (m_status == value) return;
250 m_status = value;
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);
265 else {
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);
278 else
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)
351 switch (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");
358 case Status::Error:
359 return tr("An error occurred during search...");
360 case Status::NoResults:
361 return tr("Search returned no results");
362 default:
363 return QString();
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);
393 int visibleCols = 0;
394 for (unsigned int i = 0; i < SearchSortModel::DL_LINK; ++i) {
395 if (!m_ui->resultsBrowser->isColumnHidden(i))
396 ++visibleCols;
398 if (visibleCols > 1)
399 break;
402 // Call menu
403 QAction *act = hideshowColumn.exec(QCursor::pos());
404 if (act) {
405 int col = actions.indexOf(act);
406 Q_ASSERT(col >= 0);
407 Q_ASSERT(visibleCols > 0);
408 if ((!m_ui->resultsBrowser->isColumnHidden(col)) && (visibleCols == 1))
409 return;
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);
413 saveSettings();
417 void SearchJobWidget::searchFinished(bool cancelled)
419 if (cancelled)
420 setStatus(Status::Aborted);
421 else if (m_noSearchResults)
422 setStatus(Status::NoResults);
423 else
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);
454 return setting;