2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2020, Will Da Silva <will@willdasilva.xyz>
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>
45 #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/uithememanager.h"
58 #include "pluginselectdialog.h"
59 #include "searchjobwidget.h"
60 #include "ui_searchwidget.h"
62 #define SEARCHHISTORY_MAXSIZE 50
67 QString
statusIconName(const SearchJobWidget::Status st
)
71 case SearchJobWidget::Status::Ongoing
:
73 case SearchJobWidget::Status::Finished
:
74 return u
"task-complete"_s
;
75 case SearchJobWidget::Status::Aborted
:
76 return u
"task-reject"_s
;
77 case SearchJobWidget::Status::Error
:
78 case SearchJobWidget::Status::NoResults
:
79 return u
"dialog-warning"_s
;
86 SearchWidget::SearchWidget(IGUIApplication
*app
, QWidget
*parent
)
87 : GUIApplicationComponent(app
, parent
)
88 , m_ui
{new Ui::SearchWidget()}
91 m_ui
->tabWidget
->tabBar()->installEventFilter(this);
93 const QString searchPatternHint
= u
"<html><head/><body><p>"
94 + tr("A phrase to search for.") + u
"<br>"
95 + tr("Spaces in a search term may be protected by double quotes.")
97 + tr("Example:", "Search phrase example")
99 + tr("<b>foo bar</b>: search for <b>foo</b> and <b>bar</b>",
100 "Search phrase example, illustrates quotes usage, a pair of "
101 "space delimited words, individual words are highlighted")
103 + tr("<b>"foo bar"</b>: search for <b>foo bar</b>",
104 "Search phrase example, illustrates quotes usage, double quoted"
105 "pair of space delimited words, the whole pair is highlighted")
106 + u
"</p></body></html>";
107 m_ui
->lineEditSearchPattern
->setToolTip(searchPatternHint
);
111 m_ui
->searchButton
->setIcon(UIThemeManager::instance()->getIcon(u
"edit-find"_s
));
112 m_ui
->pluginsButton
->setIcon(UIThemeManager::instance()->getIcon(u
"plugins"_s
, u
"preferences-system-network"_s
));
114 // On macOS the icons overlap the text otherwise
115 QSize iconSize
= m_ui
->tabWidget
->iconSize();
116 iconSize
.setWidth(iconSize
.width() + 16);
117 m_ui
->tabWidget
->setIconSize(iconSize
);
119 connect(m_ui
->tabWidget
, &QTabWidget::tabCloseRequested
, this, &SearchWidget::closeTab
);
120 connect(m_ui
->tabWidget
, &QTabWidget::currentChanged
, this, &SearchWidget::tabChanged
);
122 const auto *searchManager
= SearchPluginManager::instance();
123 const auto onPluginChanged
= [this]()
125 fillPluginComboBox();
129 connect(searchManager
, &SearchPluginManager::pluginInstalled
, this, onPluginChanged
);
130 connect(searchManager
, &SearchPluginManager::pluginUninstalled
, this, onPluginChanged
);
131 connect(searchManager
, &SearchPluginManager::pluginUpdated
, this, onPluginChanged
);
132 connect(searchManager
, &SearchPluginManager::pluginEnabled
, this, onPluginChanged
);
134 // Fill in category combobox
137 connect(m_ui
->lineEditSearchPattern
, &LineEdit::returnPressed
, m_ui
->searchButton
, &QPushButton::click
);
138 connect(m_ui
->lineEditSearchPattern
, &LineEdit::textEdited
, this, &SearchWidget::searchTextEdited
);
139 connect(m_ui
->selectPlugin
, qOverload
<int>(&QComboBox::currentIndexChanged
)
140 , this, &SearchWidget::selectMultipleBox
);
141 connect(m_ui
->selectPlugin
, qOverload
<int>(&QComboBox::currentIndexChanged
)
142 , this, &SearchWidget::fillCatCombobox
);
144 const auto *focusSearchHotkey
= new QShortcut(QKeySequence::Find
, this);
145 connect(focusSearchHotkey
, &QShortcut::activated
, this, &SearchWidget::toggleFocusBetweenLineEdits
);
146 const auto *focusSearchHotkeyAlternative
= new QShortcut((Qt::CTRL
| Qt::Key_E
), this);
147 connect(focusSearchHotkeyAlternative
, &QShortcut::activated
, this, &SearchWidget::toggleFocusBetweenLineEdits
);
150 bool SearchWidget::eventFilter(QObject
*object
, QEvent
*event
)
152 if (object
== m_ui
->tabWidget
->tabBar())
154 // Close tabs when middle-clicked
155 if (event
->type() != QEvent::MouseButtonRelease
)
158 const auto *mouseEvent
= static_cast<QMouseEvent
*>(event
);
159 const int tabIndex
= m_ui
->tabWidget
->tabBar()->tabAt(mouseEvent
->pos());
160 if ((mouseEvent
->button() == Qt::MiddleButton
) && (tabIndex
>= 0))
165 if (mouseEvent
->button() == Qt::RightButton
)
167 QMenu
*menu
= new QMenu(this);
168 menu
->setAttribute(Qt::WA_DeleteOnClose
);
169 menu
->addAction(tr("Close tab"), this, [this, tabIndex
]() { closeTab(tabIndex
); });
170 menu
->addAction(tr("Close all tabs"), this, &SearchWidget::closeAllTabs
);
171 menu
->popup(QCursor::pos());
177 return QWidget::eventFilter(object
, event
);
180 void SearchWidget::fillCatCombobox()
182 m_ui
->comboCategory
->clear();
183 m_ui
->comboCategory
->addItem(SearchPluginManager::categoryFullName(u
"all"_s
), u
"all"_s
);
185 using QStrPair
= std::pair
<QString
, QString
>;
186 QList
<QStrPair
> tmpList
;
187 for (const QString
&cat
: asConst(SearchPluginManager::instance()->getPluginCategories(selectedPlugin())))
188 tmpList
<< std::make_pair(SearchPluginManager::categoryFullName(cat
), cat
);
189 std::sort(tmpList
.begin(), tmpList
.end(), [](const QStrPair
&l
, const QStrPair
&r
) { return (QString::localeAwareCompare(l
.first
, r
.first
) < 0); });
191 for (const QStrPair
&p
: asConst(tmpList
))
193 qDebug("Supported category: %s", qUtf8Printable(p
.second
));
194 m_ui
->comboCategory
->addItem(p
.first
, p
.second
);
197 if (m_ui
->comboCategory
->count() > 1)
198 m_ui
->comboCategory
->insertSeparator(1);
201 void SearchWidget::fillPluginComboBox()
203 m_ui
->selectPlugin
->clear();
204 m_ui
->selectPlugin
->addItem(tr("Only enabled"), u
"enabled"_s
);
205 m_ui
->selectPlugin
->addItem(tr("All plugins"), u
"all"_s
);
206 m_ui
->selectPlugin
->addItem(tr("Select..."), u
"multi"_s
);
208 using QStrPair
= std::pair
<QString
, QString
>;
209 QList
<QStrPair
> tmpList
;
210 for (const QString
&name
: asConst(SearchPluginManager::instance()->enabledPlugins()))
211 tmpList
<< std::make_pair(SearchPluginManager::instance()->pluginFullName(name
), name
);
212 std::sort(tmpList
.begin(), tmpList
.end(), [](const QStrPair
&l
, const QStrPair
&r
) { return (l
.first
< r
.first
); } );
214 for (const QStrPair
&p
: asConst(tmpList
))
215 m_ui
->selectPlugin
->addItem(p
.first
, p
.second
);
217 if (m_ui
->selectPlugin
->count() > 3)
218 m_ui
->selectPlugin
->insertSeparator(3);
221 QString
SearchWidget::selectedCategory() const
223 return m_ui
->comboCategory
->itemData(m_ui
->comboCategory
->currentIndex()).toString();
226 QString
SearchWidget::selectedPlugin() const
228 return m_ui
->selectPlugin
->itemData(m_ui
->selectPlugin
->currentIndex()).toString();
231 void SearchWidget::selectActivePage()
233 if (SearchPluginManager::instance()->allPlugins().isEmpty())
235 m_ui
->stackedPages
->setCurrentWidget(m_ui
->emptyPage
);
236 m_ui
->lineEditSearchPattern
->setEnabled(false);
237 m_ui
->comboCategory
->setEnabled(false);
238 m_ui
->selectPlugin
->setEnabled(false);
239 m_ui
->searchButton
->setEnabled(false);
243 m_ui
->stackedPages
->setCurrentWidget(m_ui
->searchPage
);
244 m_ui
->lineEditSearchPattern
->setEnabled(true);
245 m_ui
->comboCategory
->setEnabled(true);
246 m_ui
->selectPlugin
->setEnabled(true);
247 m_ui
->searchButton
->setEnabled(true);
251 SearchWidget::~SearchWidget()
253 qDebug("Search destruction");
257 void SearchWidget::tabChanged(const int index
)
259 // when we switch from a tab that is not empty to another that is empty
260 // the download button doesn't have to be available
261 m_currentSearchTab
= (index
>= 0)
262 ? static_cast<SearchJobWidget
*>(m_ui
->tabWidget
->widget(index
))
266 void SearchWidget::selectMultipleBox([[maybe_unused
]] const int index
)
268 if (selectedPlugin() == u
"multi")
269 on_pluginsButton_clicked();
272 void SearchWidget::toggleFocusBetweenLineEdits()
274 if (m_ui
->lineEditSearchPattern
->hasFocus() && m_currentSearchTab
)
276 m_currentSearchTab
->lineEditSearchResultsFilter()->setFocus();
277 m_currentSearchTab
->lineEditSearchResultsFilter()->selectAll();
281 m_ui
->lineEditSearchPattern
->setFocus();
282 m_ui
->lineEditSearchPattern
->selectAll();
286 void SearchWidget::on_pluginsButton_clicked()
288 auto *dlg
= new PluginSelectDialog(SearchPluginManager::instance(), this);
289 dlg
->setAttribute(Qt::WA_DeleteOnClose
);
293 void SearchWidget::searchTextEdited(const QString
&)
295 // Enable search button
296 m_ui
->searchButton
->setText(tr("Search"));
297 m_isNewQueryString
= true;
300 void SearchWidget::giveFocusToSearchInput()
302 m_ui
->lineEditSearchPattern
->setFocus();
305 // Function called when we click on search button
306 void SearchWidget::on_searchButton_clicked()
308 if (!Utils::ForeignApps::pythonInfo().isValid())
310 app()->desktopIntegration()->showNotification(tr("Search Engine"), tr("Please install Python to use the Search Engine."));
314 if (m_activeSearchTab
)
316 m_activeSearchTab
->cancelSearch();
317 if (!m_isNewQueryString
)
319 m_ui
->searchButton
->setText(tr("Search"));
324 m_isNewQueryString
= false;
326 const QString pattern
= m_ui
->lineEditSearchPattern
->text().trimmed();
327 // No search pattern entered
328 if (pattern
.isEmpty())
330 QMessageBox::critical(this, tr("Empty search pattern"), tr("Please type a search pattern first"));
334 const QString plugin
= selectedPlugin();
337 if (plugin
== u
"all")
338 plugins
= SearchPluginManager::instance()->allPlugins();
339 else if ((plugin
== u
"enabled") || (plugin
== u
"multi"))
340 plugins
= SearchPluginManager::instance()->enabledPlugins();
344 qDebug("Search with category: %s", qUtf8Printable(selectedCategory()));
347 auto *searchHandler
= SearchPluginManager::instance()->startSearch(pattern
, selectedCategory(), plugins
);
350 auto *newTab
= new SearchJobWidget(searchHandler
, app(), this);
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 emit
activeSearchFinished(m_activeSearchTab
->status() == SearchJobWidget::Status::Error
);
377 m_activeSearchTab
= nullptr;
378 m_ui
->searchButton
->setText(tr("Search"));
382 void SearchWidget::closeTab(const int index
)
384 const QWidget
*tab
= m_ui
->tabWidget
->widget(index
);
385 if (tab
== m_activeSearchTab
)
386 m_ui
->searchButton
->setText(tr("Search"));
391 void SearchWidget::closeAllTabs()
393 for (int i
= (m_ui
->tabWidget
->count() - 1); i
>= 0; --i
)