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>
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.
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.
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.
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.
31 #include "searchwidget.h"
33 #include <QtSystemDetection>
44 #include <QMessageBox>
46 #include <QMouseEvent>
48 #include <QRegularExpression>
51 #include "base/global.h"
52 #include "base/search/searchhandler.h"
53 #include "base/search/searchpluginmanager.h"
54 #include "base/utils/foreignapps.h"
55 #include "gui/desktopintegration.h"
56 #include "gui/interfaces/iguiapplication.h"
57 #include "gui/mainwindow.h"
58 #include "gui/uithememanager.h"
59 #include "pluginselectdialog.h"
60 #include "searchjobwidget.h"
61 #include "ui_searchwidget.h"
63 #define SEARCHHISTORY_MAXSIZE 50
68 QString
statusIconName(const SearchJobWidget::Status st
)
72 case SearchJobWidget::Status::Ongoing
:
74 case SearchJobWidget::Status::Finished
:
75 return u
"task-complete"_s
;
76 case SearchJobWidget::Status::Aborted
:
77 return u
"task-reject"_s
;
78 case SearchJobWidget::Status::Error
:
79 case SearchJobWidget::Status::NoResults
:
80 return u
"dialog-warning"_s
;
87 SearchWidget::SearchWidget(IGUIApplication
*app
, MainWindow
*mainWindow
)
88 : GUIApplicationComponent(app
, mainWindow
)
89 , m_ui
{new Ui::SearchWidget()}
90 , m_mainWindow
{mainWindow
}
93 m_ui
->tabWidget
->tabBar()->installEventFilter(this);
95 const QString searchPatternHint
= u
"<html><head/><body><p>"
96 + tr("A phrase to search for.") + u
"<br>"
97 + tr("Spaces in a search term may be protected by double quotes.")
99 + tr("Example:", "Search phrase example")
101 + tr("<b>foo bar</b>: search for <b>foo</b> and <b>bar</b>",
102 "Search phrase example, illustrates quotes usage, a pair of "
103 "space delimited words, individual words are highlighted")
105 + tr("<b>"foo bar"</b>: search for <b>foo bar</b>",
106 "Search phrase example, illustrates quotes usage, double quoted"
107 "pair of space delimited words, the whole pair is highlighted")
108 + u
"</p></body></html>";
109 m_ui
->lineEditSearchPattern
->setToolTip(searchPatternHint
);
113 m_ui
->searchButton
->setIcon(UIThemeManager::instance()->getIcon(u
"edit-find"_s
));
114 m_ui
->pluginsButton
->setIcon(UIThemeManager::instance()->getIcon(u
"plugins"_s
, u
"preferences-system-network"_s
));
116 // On macOS the icons overlap the text otherwise
117 QSize iconSize
= m_ui
->tabWidget
->iconSize();
118 iconSize
.setWidth(iconSize
.width() + 16);
119 m_ui
->tabWidget
->setIconSize(iconSize
);
121 connect(m_ui
->tabWidget
, &QTabWidget::tabCloseRequested
, this, &SearchWidget::closeTab
);
122 connect(m_ui
->tabWidget
, &QTabWidget::currentChanged
, this, &SearchWidget::tabChanged
);
124 const auto *searchManager
= SearchPluginManager::instance();
125 const auto onPluginChanged
= [this]()
127 fillPluginComboBox();
131 connect(searchManager
, &SearchPluginManager::pluginInstalled
, this, onPluginChanged
);
132 connect(searchManager
, &SearchPluginManager::pluginUninstalled
, this, onPluginChanged
);
133 connect(searchManager
, &SearchPluginManager::pluginUpdated
, this, onPluginChanged
);
134 connect(searchManager
, &SearchPluginManager::pluginEnabled
, this, onPluginChanged
);
136 // Fill in category combobox
139 connect(m_ui
->lineEditSearchPattern
, &LineEdit::returnPressed
, m_ui
->searchButton
, &QPushButton::click
);
140 connect(m_ui
->lineEditSearchPattern
, &LineEdit::textEdited
, this, &SearchWidget::searchTextEdited
);
141 connect(m_ui
->selectPlugin
, qOverload
<int>(&QComboBox::currentIndexChanged
)
142 , this, &SearchWidget::selectMultipleBox
);
143 connect(m_ui
->selectPlugin
, qOverload
<int>(&QComboBox::currentIndexChanged
)
144 , this, &SearchWidget::fillCatCombobox
);
146 const auto *focusSearchHotkey
= new QShortcut(QKeySequence::Find
, this);
147 connect(focusSearchHotkey
, &QShortcut::activated
, this, &SearchWidget::toggleFocusBetweenLineEdits
);
148 const auto *focusSearchHotkeyAlternative
= new QShortcut((Qt::CTRL
| Qt::Key_E
), this);
149 connect(focusSearchHotkeyAlternative
, &QShortcut::activated
, this, &SearchWidget::toggleFocusBetweenLineEdits
);
152 bool SearchWidget::eventFilter(QObject
*object
, QEvent
*event
)
154 if (object
== m_ui
->tabWidget
->tabBar())
156 // Close tabs when middle-clicked
157 if (event
->type() != QEvent::MouseButtonRelease
)
160 const auto *mouseEvent
= static_cast<QMouseEvent
*>(event
);
161 const int tabIndex
= m_ui
->tabWidget
->tabBar()->tabAt(mouseEvent
->pos());
162 if ((mouseEvent
->button() == Qt::MiddleButton
) && (tabIndex
>= 0))
167 if (mouseEvent
->button() == Qt::RightButton
)
169 QMenu
*menu
= new QMenu(this);
170 menu
->setAttribute(Qt::WA_DeleteOnClose
);
171 menu
->addAction(tr("Close tab"), this, [this, tabIndex
]() { closeTab(tabIndex
); });
172 menu
->addAction(tr("Close all tabs"), this, &SearchWidget::closeAllTabs
);
173 menu
->popup(QCursor::pos());
178 return QWidget::eventFilter(object
, event
);
181 void SearchWidget::fillCatCombobox()
183 m_ui
->comboCategory
->clear();
184 m_ui
->comboCategory
->addItem(SearchPluginManager::categoryFullName(u
"all"_s
), u
"all"_s
);
186 using QStrPair
= std::pair
<QString
, QString
>;
187 QList
<QStrPair
> tmpList
;
188 for (const QString
&cat
: asConst(SearchPluginManager::instance()->getPluginCategories(selectedPlugin())))
189 tmpList
<< std::make_pair(SearchPluginManager::categoryFullName(cat
), cat
);
190 std::sort(tmpList
.begin(), tmpList
.end(), [](const QStrPair
&l
, const QStrPair
&r
) { return (QString::localeAwareCompare(l
.first
, r
.first
) < 0); });
192 for (const QStrPair
&p
: asConst(tmpList
))
194 qDebug("Supported category: %s", qUtf8Printable(p
.second
));
195 m_ui
->comboCategory
->addItem(p
.first
, p
.second
);
198 if (m_ui
->comboCategory
->count() > 1)
199 m_ui
->comboCategory
->insertSeparator(1);
202 void SearchWidget::fillPluginComboBox()
204 m_ui
->selectPlugin
->clear();
205 m_ui
->selectPlugin
->addItem(tr("Only enabled"), u
"enabled"_s
);
206 m_ui
->selectPlugin
->addItem(tr("All plugins"), u
"all"_s
);
207 m_ui
->selectPlugin
->addItem(tr("Select..."), u
"multi"_s
);
209 using QStrPair
= std::pair
<QString
, QString
>;
210 QList
<QStrPair
> tmpList
;
211 for (const QString
&name
: asConst(SearchPluginManager::instance()->enabledPlugins()))
212 tmpList
<< std::make_pair(SearchPluginManager::instance()->pluginFullName(name
), name
);
213 std::sort(tmpList
.begin(), tmpList
.end(), [](const QStrPair
&l
, const QStrPair
&r
) { return (l
.first
< r
.first
); } );
215 for (const QStrPair
&p
: asConst(tmpList
))
216 m_ui
->selectPlugin
->addItem(p
.first
, p
.second
);
218 if (m_ui
->selectPlugin
->count() > 3)
219 m_ui
->selectPlugin
->insertSeparator(3);
222 QString
SearchWidget::selectedCategory() const
224 return m_ui
->comboCategory
->itemData(m_ui
->comboCategory
->currentIndex()).toString();
227 QString
SearchWidget::selectedPlugin() const
229 return m_ui
->selectPlugin
->itemData(m_ui
->selectPlugin
->currentIndex()).toString();
232 void SearchWidget::selectActivePage()
234 if (SearchPluginManager::instance()->allPlugins().isEmpty())
236 m_ui
->stackedPages
->setCurrentWidget(m_ui
->emptyPage
);
237 m_ui
->lineEditSearchPattern
->setEnabled(false);
238 m_ui
->comboCategory
->setEnabled(false);
239 m_ui
->selectPlugin
->setEnabled(false);
240 m_ui
->searchButton
->setEnabled(false);
244 m_ui
->stackedPages
->setCurrentWidget(m_ui
->searchPage
);
245 m_ui
->lineEditSearchPattern
->setEnabled(true);
246 m_ui
->comboCategory
->setEnabled(true);
247 m_ui
->selectPlugin
->setEnabled(true);
248 m_ui
->searchButton
->setEnabled(true);
252 SearchWidget::~SearchWidget()
254 qDebug("Search destruction");
258 void SearchWidget::tabChanged(const int index
)
260 // when we switch from a tab that is not empty to another that is empty
261 // the download button doesn't have to be available
262 m_currentSearchTab
= ((index
< 0) ? nullptr : m_allTabs
.at(m_ui
->tabWidget
->currentIndex()));
265 void SearchWidget::selectMultipleBox([[maybe_unused
]] const int index
)
267 if (selectedPlugin() == u
"multi")
268 on_pluginsButton_clicked();
271 void SearchWidget::toggleFocusBetweenLineEdits()
273 if (m_ui
->lineEditSearchPattern
->hasFocus() && m_currentSearchTab
)
275 m_currentSearchTab
->lineEditSearchResultsFilter()->setFocus();
276 m_currentSearchTab
->lineEditSearchResultsFilter()->selectAll();
280 m_ui
->lineEditSearchPattern
->setFocus();
281 m_ui
->lineEditSearchPattern
->selectAll();
285 void SearchWidget::on_pluginsButton_clicked()
287 auto *dlg
= new PluginSelectDialog(SearchPluginManager::instance(), this);
288 dlg
->setAttribute(Qt::WA_DeleteOnClose
);
292 void SearchWidget::searchTextEdited(const QString
&)
294 // Enable search button
295 m_ui
->searchButton
->setText(tr("Search"));
296 m_isNewQueryString
= true;
299 void SearchWidget::giveFocusToSearchInput()
301 m_ui
->lineEditSearchPattern
->setFocus();
304 // Function called when we click on search button
305 void SearchWidget::on_searchButton_clicked()
307 if (!Utils::ForeignApps::pythonInfo().isValid())
309 app()->desktopIntegration()->showNotification(tr("Search Engine"), tr("Please install Python to use the Search Engine."));
313 if (m_activeSearchTab
)
315 m_activeSearchTab
->cancelSearch();
316 if (!m_isNewQueryString
)
318 m_ui
->searchButton
->setText(tr("Search"));
323 m_isNewQueryString
= false;
325 const QString pattern
= m_ui
->lineEditSearchPattern
->text().trimmed();
326 // No search pattern entered
327 if (pattern
.isEmpty())
329 QMessageBox::critical(this, tr("Empty search pattern"), tr("Please type a search pattern first"));
333 const QString plugin
= selectedPlugin();
336 if (plugin
== u
"all")
337 plugins
= SearchPluginManager::instance()->allPlugins();
338 else if ((plugin
== u
"enabled") || (plugin
== u
"multi"))
339 plugins
= SearchPluginManager::instance()->enabledPlugins();
343 qDebug("Search with category: %s", qUtf8Printable(selectedCategory()));
346 auto *searchHandler
= SearchPluginManager::instance()->startSearch(pattern
, selectedCategory(), plugins
);
349 auto *newTab
= new SearchJobWidget(searchHandler
, app(), this);
350 m_allTabs
.append(newTab
);
352 QString tabName
= pattern
;
353 tabName
.replace(QRegularExpression(u
"&{1}"_s
), u
"&&"_s
);
354 m_ui
->tabWidget
->addTab(newTab
, tabName
);
355 m_ui
->tabWidget
->setCurrentWidget(newTab
);
357 connect(newTab
, &SearchJobWidget::statusChanged
, this, [this, newTab
]() { tabStatusChanged(newTab
); });
359 m_ui
->searchButton
->setText(tr("Stop"));
360 m_activeSearchTab
= newTab
;
361 tabStatusChanged(newTab
);
364 void SearchWidget::tabStatusChanged(QWidget
*tab
)
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())));
371 if ((tab
== m_activeSearchTab
) && (m_activeSearchTab
->status() != SearchJobWidget::Status::Ongoing
))
373 Q_ASSERT(m_activeSearchTab
->status() != SearchJobWidget::Status::Ongoing
);
375 if (app()->desktopIntegration()->isNotificationsEnabled() && (m_mainWindow
->currentTabWidget() != this))
377 if (m_activeSearchTab
->status() == SearchJobWidget::Status::Error
)
378 app()->desktopIntegration()->showNotification(tr("Search Engine"), tr("Search has failed"));
380 app()->desktopIntegration()->showNotification(tr("Search Engine"), tr("Search has finished"));
383 m_activeSearchTab
= nullptr;
384 m_ui
->searchButton
->setText(tr("Search"));
388 void SearchWidget::closeTab(int index
)
390 SearchJobWidget
*tab
= m_allTabs
.takeAt(index
);
391 if (tab
== m_activeSearchTab
)
392 m_ui
->searchButton
->setText(tr("Search"));
397 void SearchWidget::closeAllTabs()
399 for (int i
= (m_allTabs
.size() - 1); i
>= 0; --i
)