qBittorrent
searchwidget.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2020, Will Da Silva <will@willdasilva.xyz>
4  * Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
5  * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  * In addition, as a special exception, the copyright holders give permission to
22  * link this program with the OpenSSL project's "OpenSSL" library (or with
23  * modified versions of it that use the same license as the "OpenSSL" library),
24  * and distribute the linked executables. You must obey the GNU General Public
25  * License in all respects for all of the code used other than "OpenSSL". If you
26  * modify file(s), you may extend this exception to your version of the file(s),
27  * but you are not obligated to do so. If you do not wish to do so, delete this
28  * exception statement from your version.
29  */
30 
31 #include "searchwidget.h"
32 
33 #include <QtGlobal>
34 
35 #include <utility>
36 
37 #ifdef Q_OS_WIN
38 #include <cstdlib>
39 #endif
40 
41 #include <QDebug>
42 #include <QEvent>
43 #include <QMessageBox>
44 #include <QMenu>
45 #include <QMouseEvent>
46 #include <QObject>
47 #include <QRegularExpression>
48 #include <QShortcut>
49 #include <QTextStream>
50 #include <QVector>
51 
52 #include "base/global.h"
55 #include "base/utils/foreignapps.h"
56 #include "gui/mainwindow.h"
57 #include "gui/uithememanager.h"
58 #include "pluginselectdialog.h"
59 #include "searchjobwidget.h"
60 #include "ui_searchwidget.h"
61 
62 #define SEARCHHISTORY_MAXSIZE 50
63 #define URL_COLUMN 5
64 
65 namespace
66 {
68  {
69  switch (st)
70  {
72  return QLatin1String("task-ongoing");
74  return QLatin1String("task-complete");
76  return QLatin1String("task-reject");
78  return QLatin1String("task-attention");
80  return QLatin1String("task-attention");
81  default:
82  return {};
83  }
84  }
85 }
86 
88  : QWidget(mainWindow)
89  , m_ui(new Ui::SearchWidget())
90  , m_mainWindow(mainWindow)
91  , m_isNewQueryString(false)
92 {
93  m_ui->setupUi(this);
94  m_ui->tabWidget->tabBar()->installEventFilter(this);
95 
96  QString searchPatternHint;
97  QTextStream stream(&searchPatternHint, QIODevice::WriteOnly);
98  stream << "<html><head/><body><p>"
99  << tr("A phrase to search for.") << "<br>"
100  << tr("Spaces in a search term may be protected by double quotes.")
101  << "</p><p>"
102  << tr("Example:", "Search phrase example")
103  << "<br>"
104  << tr("<b>foo bar</b>: search for <b>foo</b> and <b>bar</b>",
105  "Search phrase example, illustrates quotes usage, a pair of "
106  "space delimited words, individal words are highlighted")
107  << "<br>"
108  << tr("<b>&quot;foo bar&quot;</b>: search for <b>foo bar</b>",
109  "Search phrase example, illustrates quotes usage, double quoted"
110  "pair of space delimited words, the whole pair is highlighted")
111  << "</p></body></html>";
112  m_ui->lineEditSearchPattern->setToolTip(searchPatternHint);
113 
114 #ifndef Q_OS_MACOS
115  // Icons
116  m_ui->searchButton->setIcon(UIThemeManager::instance()->getIcon("edit-find"));
117  m_ui->pluginsButton->setIcon(UIThemeManager::instance()->getIcon("preferences-system-network"));
118 #else
119  // On macOS the icons overlap the text otherwise
120  QSize iconSize = m_ui->tabWidget->iconSize();
121  iconSize.setWidth(iconSize.width() + 16);
122  m_ui->tabWidget->setIconSize(iconSize);
123 #endif
124  connect(m_ui->tabWidget, &QTabWidget::tabCloseRequested, this, &SearchWidget::closeTab);
125  connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &SearchWidget::tabChanged);
126 
127  const auto *searchManager = SearchPluginManager::instance();
128  const auto onPluginChanged = [this]()
129  {
131  fillCatCombobox();
133  };
134  connect(searchManager, &SearchPluginManager::pluginInstalled, this, onPluginChanged);
135  connect(searchManager, &SearchPluginManager::pluginUninstalled, this, onPluginChanged);
136  connect(searchManager, &SearchPluginManager::pluginUpdated, this, onPluginChanged);
137  connect(searchManager, &SearchPluginManager::pluginEnabled, this, onPluginChanged);
138 
139  // Fill in category combobox
140  onPluginChanged();
141 
142  connect(m_ui->lineEditSearchPattern, &LineEdit::returnPressed, m_ui->searchButton, &QPushButton::click);
143  connect(m_ui->lineEditSearchPattern, &LineEdit::textEdited, this, &SearchWidget::searchTextEdited);
144  connect(m_ui->selectPlugin, qOverload<int>(&QComboBox::currentIndexChanged)
146  connect(m_ui->selectPlugin, qOverload<int>(&QComboBox::currentIndexChanged)
148 
149  const auto focusSearchHotkey = new QShortcut(QKeySequence::Find, this);
150  connect(focusSearchHotkey, &QShortcut::activated, this, &SearchWidget::toggleFocusBetweenLineEdits);
151 }
152 
153 bool SearchWidget::eventFilter(QObject *object, QEvent *event)
154 {
155  if (object == m_ui->tabWidget->tabBar())
156  {
157  // Close tabs when middle-clicked
158  if (event->type() != QEvent::MouseButtonRelease)
159  return false;
160 
161  const auto mouseEvent = static_cast<QMouseEvent *>(event);
162  const int tabIndex = m_ui->tabWidget->tabBar()->tabAt(mouseEvent->pos());
163  if ((mouseEvent->button() == Qt::MiddleButton) && (tabIndex >= 0))
164  {
165  closeTab(tabIndex);
166  return true;
167  }
168  if (mouseEvent->button() == Qt::RightButton)
169  {
170  QMenu *menu = new QMenu(this);
171  menu->setAttribute(Qt::WA_DeleteOnClose);
172  menu->addAction(tr("Close tab"), this, [this, tabIndex]() { closeTab(tabIndex); });
173  menu->addAction(tr("Close all tabs"), this, &SearchWidget::closeAllTabs);
174  menu->popup(QCursor::pos());
175  return true;
176  }
177  return false;
178  }
179  return QWidget::eventFilter(object, event);
180 }
181 
183 {
184  m_ui->comboCategory->clear();
185  m_ui->comboCategory->addItem(SearchPluginManager::categoryFullName("all"), "all");
186 
187  using QStrPair = std::pair<QString, QString>;
188  QVector<QStrPair> tmpList;
190  tmpList << std::make_pair(SearchPluginManager::categoryFullName(cat), cat);
191  std::sort(tmpList.begin(), tmpList.end(), [](const QStrPair &l, const QStrPair &r) { return (QString::localeAwareCompare(l.first, r.first) < 0); });
192 
193  for (const QStrPair &p : asConst(tmpList))
194  {
195  qDebug("Supported category: %s", qUtf8Printable(p.second));
196  m_ui->comboCategory->addItem(p.first, p.second);
197  }
198 
199  if (m_ui->comboCategory->count() > 1)
200  m_ui->comboCategory->insertSeparator(1);
201 }
202 
204 {
205  m_ui->selectPlugin->clear();
206  m_ui->selectPlugin->addItem(tr("Only enabled"), "enabled");
207  m_ui->selectPlugin->addItem(tr("All plugins"), "all");
208  m_ui->selectPlugin->addItem(tr("Select..."), "multi");
209 
210  using QStrPair = std::pair<QString, QString>;
211  QVector<QStrPair> tmpList;
212  for (const QString &name : asConst(SearchPluginManager::instance()->enabledPlugins()))
213  tmpList << std::make_pair(SearchPluginManager::instance()->pluginFullName(name), name);
214  std::sort(tmpList.begin(), tmpList.end(), [](const QStrPair &l, const QStrPair &r) { return (l.first < r.first); } );
215 
216  for (const QStrPair &p : asConst(tmpList))
217  m_ui->selectPlugin->addItem(p.first, p.second);
218 
219  if (m_ui->selectPlugin->count() > 3)
220  m_ui->selectPlugin->insertSeparator(3);
221 }
222 
224 {
225  return m_ui->comboCategory->itemData(m_ui->comboCategory->currentIndex()).toString();
226 }
227 
229 {
230  return m_ui->selectPlugin->itemData(m_ui->selectPlugin->currentIndex()).toString();
231 }
232 
234 {
235  if (SearchPluginManager::instance()->allPlugins().isEmpty())
236  {
237  m_ui->stackedPages->setCurrentWidget(m_ui->emptyPage);
238  m_ui->lineEditSearchPattern->setEnabled(false);
239  m_ui->comboCategory->setEnabled(false);
240  m_ui->selectPlugin->setEnabled(false);
241  m_ui->searchButton->setEnabled(false);
242  }
243  else
244  {
245  m_ui->stackedPages->setCurrentWidget(m_ui->searchPage);
246  m_ui->lineEditSearchPattern->setEnabled(true);
247  m_ui->comboCategory->setEnabled(true);
248  m_ui->selectPlugin->setEnabled(true);
249  m_ui->searchButton->setEnabled(true);
250  }
251 }
252 
254 {
255  qDebug("Search destruction");
256  delete m_ui;
257 }
258 
260 {
261  // when we switch from a tab that is not empty to another that is empty
262  // the download button doesn't have to be available
263  m_currentSearchTab = ((index < 0) ? nullptr : m_allTabs.at(m_ui->tabWidget->currentIndex()));
264 }
265 
267 {
268  Q_UNUSED(index);
269  if (selectedPlugin() == "multi")
271 }
272 
274 {
275  if (m_ui->lineEditSearchPattern->hasFocus() && m_currentSearchTab)
276  {
277  m_currentSearchTab->lineEditSearchResultsFilter()->setFocus();
278  m_currentSearchTab->lineEditSearchResultsFilter()->selectAll();
279  }
280  else
281  {
282  m_ui->lineEditSearchPattern->setFocus();
283  m_ui->lineEditSearchPattern->selectAll();
284  }
285 }
286 
288 {
290 }
291 
292 void SearchWidget::searchTextEdited(const QString &)
293 {
294  // Enable search button
295  m_ui->searchButton->setText(tr("Search"));
296  m_isNewQueryString = true;
297 }
298 
300 {
301  m_ui->lineEditSearchPattern->setFocus();
302 }
303 
304 // Function called when we click on search button
306 {
307  if (!Utils::ForeignApps::pythonInfo().isValid())
308  {
309  m_mainWindow->showNotificationBalloon(tr("Search Engine"), tr("Please install Python to use the Search Engine."));
310  return;
311  }
312 
313  if (m_activeSearchTab)
314  {
315  m_activeSearchTab->cancelSearch();
316  if (!m_isNewQueryString)
317  {
318  m_ui->searchButton->setText(tr("Search"));
319  return;
320  }
321  }
322 
323  m_isNewQueryString = false;
324 
325  const QString pattern = m_ui->lineEditSearchPattern->text().trimmed();
326  // No search pattern entered
327  if (pattern.isEmpty())
328  {
329  QMessageBox::critical(this, tr("Empty search pattern"), tr("Please type a search pattern first"));
330  return;
331  }
332 
333  QStringList plugins;
334  if (selectedPlugin() == "all")
336  else if (selectedPlugin() == "enabled")
338  else if (selectedPlugin() == "multi")
340  else
341  plugins << selectedPlugin();
342 
343  qDebug("Search with category: %s", qUtf8Printable(selectedCategory()));
344 
345  // Launch search
346  auto *searchHandler = SearchPluginManager::instance()->startSearch(pattern, selectedCategory(), plugins);
347 
348  // Tab Addition
349  auto *newTab = new SearchJobWidget(searchHandler, this);
350  m_allTabs.append(newTab);
351 
352  QString tabName = pattern;
353  tabName.replace(QRegularExpression("&{1}"), "&&");
354  m_ui->tabWidget->addTab(newTab, tabName);
355  m_ui->tabWidget->setCurrentWidget(newTab);
356 
357  connect(newTab, &SearchJobWidget::statusChanged, this, [this, newTab]() { tabStatusChanged(newTab); });
358 
359  m_ui->searchButton->setText(tr("Stop"));
360  m_activeSearchTab = newTab;
361  tabStatusChanged(newTab);
362 }
363 
365 {
366  const int tabIndex = m_ui->tabWidget->indexOf(tab);
367  m_ui->tabWidget->setTabToolTip(tabIndex, tab->statusTip());
368  m_ui->tabWidget->setTabIcon(tabIndex, UIThemeManager::instance()->getIcon(
369  statusIconName(static_cast<SearchJobWidget *>(tab)->status())));
370 
372  {
374 
376  {
378  m_mainWindow->showNotificationBalloon(tr("Search Engine"), tr("Search has failed"));
379  else
380  m_mainWindow->showNotificationBalloon(tr("Search Engine"), tr("Search has finished"));
381  }
382 
383  m_activeSearchTab = nullptr;
384  m_ui->searchButton->setText(tr("Search"));
385  }
386 }
387 
388 void SearchWidget::closeTab(int index)
389 {
390  SearchJobWidget *tab = m_allTabs.takeAt(index);
391  if (tab == m_activeSearchTab)
392  m_ui->searchButton->setText(tr("Search"));
393 
394  delete tab;
395 }
396 
398 {
399  for (int i = (m_allTabs.size() - 1); i >= 0; --i)
400  closeTab(i);
401 }
void showNotificationBalloon(const QString &title, const QString &msg) const
QWidget * currentTabWidget() const
bool isNotificationsEnabled() const
Definition: mainwindow.cpp:506
void statusChanged()
QStringList enabledPlugins() const
void pluginUpdated(const QString &name)
SearchHandler * startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins)
static QString categoryFullName(const QString &categoryName)
void pluginEnabled(const QString &name, bool enabled)
void pluginInstalled(const QString &name)
QStringList allPlugins() const
void pluginUninstalled(const QString &name)
static SearchPluginManager * instance()
QString selectedPlugin() const
void on_pluginsButton_clicked()
void on_searchButton_clicked()
void fillPluginComboBox()
void closeTab(int index)
void giveFocusToSearchInput()
void fillCatCombobox()
void tabStatusChanged(QWidget *tab)
MainWindow * m_mainWindow
Definition: searchwidget.h:85
void selectActivePage()
QPointer< SearchJobWidget > m_activeSearchTab
Definition: searchwidget.h:83
Ui::SearchWidget * m_ui
Definition: searchwidget.h:81
void selectMultipleBox(int index)
SearchWidget(MainWindow *mainWindow)
void searchTextEdited(const QString &)
QPointer< SearchJobWidget > m_currentSearchTab
Definition: searchwidget.h:82
~SearchWidget() override
bool eventFilter(QObject *object, QEvent *event) override
void toggleFocusBetweenLineEdits()
bool m_isNewQueryString
Definition: searchwidget.h:86
void closeAllTabs()
QString selectedCategory() const
void tabChanged(int index)
QList< SearchJobWidget * > m_allTabs
Definition: searchwidget.h:84
static UIThemeManager * instance()
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
PythonInfo pythonInfo()
QJsonArray getPluginCategories(QStringList categories)
QString statusIconName(SearchJobWidget::Status st)