UI/Qt: Migrate to LibWebView's autocomplete engine

As a result, we now no longer depend on Qt::Network.
This commit is contained in:
Timothy Flynn 2025-03-30 16:25:39 -04:00
parent 24ae3cf4e7
commit 398f615300
13 changed files with 100 additions and 371 deletions

View file

@ -79,7 +79,7 @@ if (ENABLE_QT AND ENABLE_GUI_TARGETS)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
endif()
# We need to find OpenSSL in order to link it explicitly with all targets.

View file

@ -39,7 +39,6 @@ link_qt("ladybird_qt_components") {
"Core",
"Gui",
"Widgets",
"Network",
]
}

View file

@ -1,172 +0,0 @@
/*
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibURL/URL.h>
#include <UI/Qt/AutoComplete.h>
#include <UI/Qt/Settings.h>
namespace Ladybird {
AutoComplete::AutoComplete(QWidget* parent)
: QCompleter(parent)
{
m_tree_view = new QTreeView(parent);
m_manager = new QNetworkAccessManager(this);
m_auto_complete_model = new AutoCompleteModel(this);
setCompletionMode(QCompleter::UnfilteredPopupCompletion);
setModel(m_auto_complete_model);
setPopup(m_tree_view);
m_tree_view->setRootIsDecorated(false);
m_tree_view->setHeaderHidden(true);
connect(this, QOverload<QModelIndex const&>::of(&QCompleter::activated), this, [&](QModelIndex const& index) {
emit activated(index);
});
connect(m_manager, &QNetworkAccessManager::finished, this, [&](QNetworkReply* reply) {
auto result = got_network_response(reply);
if (result.is_error())
dbgln("AutoComplete::got_network_response: Error {}", result.error());
});
}
ErrorOr<Vector<String>> AutoComplete::parse_google_autocomplete(JsonValue const& json)
{
if (!json.is_array())
return Error::from_string_literal("Expected Google autocomplete response to be a JSON array");
auto const& values = json.as_array();
if (values.size() != 5)
return Error::from_string_literal("Invalid Google autocomplete response, expected 5 elements in array");
if (!values[0].is_string())
return Error::from_string_literal("Invalid Google autocomplete response, expected first element to be a string");
auto const& query = values[0].as_string();
if (query != m_query)
return Error::from_string_literal("Invalid Google autocomplete response, query does not match");
if (!values[1].is_array())
return Error::from_string_literal("Invalid Google autocomplete response, expected second element to be an array");
auto const& suggestions_array = values[1].as_array().values();
Vector<String> results;
results.ensure_capacity(suggestions_array.size());
for (auto const& suggestion : suggestions_array)
results.unchecked_append(suggestion.as_string());
return results;
}
ErrorOr<Vector<String>> AutoComplete::parse_duckduckgo_autocomplete(JsonValue const& json)
{
if (!json.is_array())
return Error::from_string_literal("Expected DuckDuckGo autocomplete response to be a JSON array");
Vector<String> results;
results.ensure_capacity(json.as_array().size());
for (auto const& suggestion : json.as_array().values()) {
if (!suggestion.is_object())
return Error::from_string_literal("Invalid DuckDuckGo autocomplete response, expected value to be an object");
if (auto value = suggestion.as_object().get_string("phrase"sv); value.has_value())
results.unchecked_append(*value);
}
return results;
}
ErrorOr<Vector<String>> AutoComplete::parse_yahoo_autocomplete(JsonValue const& json)
{
if (!json.is_object())
return Error::from_string_literal("Expected Yahoo autocomplete response to be a JSON array");
auto query = json.as_object().get_string("q"sv);
if (!query.has_value())
return Error::from_string_literal("Invalid Yahoo autocomplete response, expected \"q\" to be a string");
if (query != m_query)
return Error::from_string_literal("Invalid Yahoo autocomplete response, query does not match");
auto suggestions = json.as_object().get_array("r"sv);
if (!suggestions.has_value())
return Error::from_string_literal("Invalid Yahoo autocomplete response, expected \"r\" to be an object");
Vector<String> results;
results.ensure_capacity(suggestions->size());
for (auto const& suggestion : suggestions->values()) {
if (!suggestion.is_object())
return Error::from_string_literal("Invalid Yahoo autocomplete response, expected value to be an object");
auto result = suggestion.as_object().get_string("k"sv);
if (!result.has_value())
return Error::from_string_literal("Invalid Yahoo autocomplete response, expected \"k\" to be a string");
results.unchecked_append(*result);
}
return results;
}
ErrorOr<void> AutoComplete::got_network_response(QNetworkReply* reply)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError)
return {};
auto reply_data = ak_string_from_qstring(reply->readAll());
auto json = TRY(JsonValue::from_string(reply_data));
auto const& engine_name = Settings::the()->autocomplete_engine().name;
Vector<String> results;
if (engine_name == "Google")
results = TRY(parse_google_autocomplete(json));
else if (engine_name == "DuckDuckGo")
results = TRY(parse_duckduckgo_autocomplete(json));
else if (engine_name == "Yahoo")
results = TRY(parse_yahoo_autocomplete(json));
else
return Error::from_string_literal("Invalid engine name");
constexpr size_t MAX_AUTOCOMPLETE_RESULTS = 6;
if (results.is_empty()) {
results.append(m_query);
} else if (results.size() > MAX_AUTOCOMPLETE_RESULTS) {
results.shrink(MAX_AUTOCOMPLETE_RESULTS);
}
m_auto_complete_model->replace_suggestions(move(results));
return {};
}
String AutoComplete::auto_complete_url_from_query(StringView query)
{
auto autocomplete_engine = ak_string_from_qstring(Settings::the()->autocomplete_engine().url);
return MUST(autocomplete_engine.replace("{}"sv, URL::percent_encode(query), ReplaceMode::FirstOnly));
}
void AutoComplete::clear_suggestions()
{
m_auto_complete_model->clear();
}
void AutoComplete::get_search_suggestions(String search_string)
{
m_query = move(search_string);
if (m_reply)
m_reply->abort();
QNetworkRequest request { QUrl(qstring_from_ak_string(auto_complete_url_from_query(m_query))) };
m_reply = m_manager->get(request);
}
}

View file

@ -1,94 +0,0 @@
/*
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/String.h>
#include <UI/Qt/StringUtils.h>
#include <QCompleter>
#include <QNetworkReply>
#include <QTreeView>
namespace Ladybird {
class AutoCompleteModel final : public QAbstractListModel {
Q_OBJECT
public:
explicit AutoCompleteModel(QObject* parent)
: QAbstractListModel(parent)
{
}
virtual int rowCount(QModelIndex const& parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_suggestions.size(); }
virtual QVariant data(QModelIndex const& index, int role = Qt::DisplayRole) const override
{
if (role == Qt::DisplayRole || role == Qt::EditRole)
return qstring_from_ak_string(m_suggestions[index.row()]);
return {};
}
void add(String const& result)
{
beginInsertRows({}, m_suggestions.size(), m_suggestions.size());
m_suggestions.append(result);
endInsertRows();
}
void clear()
{
beginResetModel();
m_suggestions.clear();
endResetModel();
}
void replace_suggestions(Vector<String> suggestions)
{
beginInsertRows({}, m_suggestions.size(), m_suggestions.size());
m_suggestions = suggestions;
endInsertRows();
}
private:
AK::Vector<String> m_suggestions;
};
class AutoComplete final : public QCompleter {
Q_OBJECT
public:
AutoComplete(QWidget* parent);
virtual QString pathFromIndex(QModelIndex const& index) const override
{
return index.data(Qt::DisplayRole).toString();
}
void get_search_suggestions(String);
void clear_suggestions();
signals:
void activated(QModelIndex const&);
private:
static String auto_complete_url_from_query(StringView query);
ErrorOr<void> got_network_response(QNetworkReply* reply);
ErrorOr<Vector<String>> parse_google_autocomplete(JsonValue const&);
ErrorOr<Vector<String>> parse_duckduckgo_autocomplete(JsonValue const&);
ErrorOr<Vector<String>> parse_yahoo_autocomplete(JsonValue const&);
QNetworkAccessManager* m_manager;
AutoCompleteModel* m_auto_complete_model;
QTreeView* m_tree_view;
QNetworkReply* m_reply { nullptr };
String m_query;
};
}

43
UI/Qt/Autocomplete.cpp Normal file
View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWebView/Autocomplete.h>
#include <UI/Qt/Autocomplete.h>
#include <UI/Qt/StringUtils.h>
namespace Ladybird {
Autocomplete::Autocomplete(QWidget* parent)
: QCompleter(parent)
, m_autocomplete(make<WebView::Autocomplete>())
, m_model(new QStringListModel(this))
, m_popup(new QListView(parent))
{
m_autocomplete->on_autocomplete_query_complete = [this](auto const& suggestions) {
if (suggestions.is_empty()) {
m_model->setStringList({});
} else {
QStringList list;
for (auto const& suggestion : suggestions)
list.append(qstring_from_ak_string(suggestion));
m_model->setStringList(list);
complete();
}
};
setCompletionMode(QCompleter::UnfilteredPopupCompletion);
setModel(m_model);
setPopup(m_popup);
}
void Autocomplete::query_autocomplete_engine(String search_string)
{
m_autocomplete->query_autocomplete_engine(move(search_string));
}
}

35
UI/Qt/Autocomplete.h Normal file
View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullOwnPtr.h>
#include <AK/String.h>
#include <LibWebView/Forward.h>
#include <QCompleter>
#include <QListView>
#include <QStringListModel>
namespace Ladybird {
class Autocomplete final : public QCompleter {
Q_OBJECT
public:
explicit Autocomplete(QWidget* parent);
void query_autocomplete_engine(String);
private:
NonnullOwnPtr<WebView::Autocomplete> m_autocomplete;
QStringListModel* m_model { nullptr };
QListView* m_popup { nullptr };
};
}

View file

@ -1,7 +1,7 @@
qt_add_executable(ladybird ${LADYBIRD_SOURCES})
target_sources(ladybird PRIVATE
Application.cpp
AutoComplete.cpp
Autocomplete.cpp
BrowserWindow.cpp
FindInPageWidget.cpp
Icon.cpp
@ -16,5 +16,5 @@ target_sources(ladybird PRIVATE
ladybird.qrc
main.cpp
)
target_link_libraries(ladybird PRIVATE Qt::Core Qt::Gui Qt::Network Qt::Widgets)
target_link_libraries(ladybird PRIVATE Qt::Core Qt::Gui Qt::Widgets)
create_ladybird_bundle(ladybird)

View file

@ -7,8 +7,8 @@
#include <LibURL/URL.h>
#include <LibWebView/Application.h>
#include <LibWebView/URL.h>
#include <UI/Qt/Autocomplete.h>
#include <UI/Qt/LocationEdit.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/StringUtils.h>
#include <QApplication>
@ -20,13 +20,13 @@ namespace Ladybird {
LocationEdit::LocationEdit(QWidget* parent)
: QLineEdit(parent)
, m_autocomplete(new Autocomplete(this))
{
update_placeholder();
m_autocomplete = make<AutoComplete>(this);
this->setCompleter(m_autocomplete);
setCompleter(m_autocomplete);
connect(m_autocomplete, &AutoComplete::activated, [&](QModelIndex const&) {
connect(m_autocomplete, QOverload<QModelIndex const&>::of(&QCompleter::activated), [&](QModelIndex const&) {
emit returnPressed();
});
@ -50,15 +50,7 @@ LocationEdit::LocationEdit(QWidget* parent)
});
connect(this, &QLineEdit::textEdited, [this] {
if (!Settings::the()->enable_autocomplete()) {
m_autocomplete->clear_suggestions();
return;
}
auto cursor_position = cursorPosition();
m_autocomplete->get_search_suggestions(ak_string_from_qstring(text()));
setCursorPosition(cursor_position);
m_autocomplete->query_autocomplete_engine(ak_string_from_qstring(text()));
});
connect(this, &QLineEdit::textChanged, this, &LocationEdit::highlight_location);
@ -68,12 +60,15 @@ void LocationEdit::focusInEvent(QFocusEvent* event)
{
QLineEdit::focusInEvent(event);
highlight_location();
QTimer::singleShot(0, this, &QLineEdit::selectAll);
if (event->reason() != Qt::PopupFocusReason)
QTimer::singleShot(0, this, &QLineEdit::selectAll);
}
void LocationEdit::focusOutEvent(QFocusEvent* event)
{
QLineEdit::focusOutEvent(event);
if (m_url_is_hidden) {
m_url_is_hidden = false;
if (text().isEmpty())
@ -142,13 +137,14 @@ void LocationEdit::highlight_location()
QCoreApplication::sendEvent(this, &event);
}
void LocationEdit::set_url(URL::URL const& url)
void LocationEdit::set_url(URL::URL url)
{
m_url = url;
m_url = AK::move(url);
if (m_url_is_hidden) {
clear();
} else {
setText(qstring_from_ak_string(url.serialize()));
setText(qstring_from_ak_string(m_url.serialize()));
setCursorPosition(0);
}
}

View file

@ -8,12 +8,13 @@
#include <AK/OwnPtr.h>
#include <LibWebView/Settings.h>
#include <UI/Qt/AutoComplete.h>
#include <QLineEdit>
namespace Ladybird {
class Autocomplete;
class LocationEdit final
: public QLineEdit
, public WebView::SettingsObserver {
@ -22,8 +23,8 @@ class LocationEdit final
public:
explicit LocationEdit(QWidget*);
URL::URL url() const { return m_url; }
void set_url(URL::URL const&);
URL::URL const& url() const { return m_url; }
void set_url(URL::URL);
bool url_is_hidden() const { return m_url_is_hidden; }
void set_url_is_hidden(bool url_is_hidden) { m_url_is_hidden = url_is_hidden; }
@ -36,7 +37,8 @@ private:
void update_placeholder();
void highlight_location();
AK::OwnPtr<AutoComplete> m_autocomplete;
Autocomplete* m_autocomplete { nullptr };
URL::URL m_url;
bool m_url_is_hidden { false };

View file

@ -65,30 +65,6 @@ void Settings::set_preferred_languages(QStringList const& languages)
emit preferred_languages_changed(languages);
}
Settings::EngineProvider Settings::autocomplete_engine()
{
EngineProvider engine_provider;
engine_provider.name = m_qsettings->value("autocomplete_engine_name", "Google").toString();
engine_provider.url = m_qsettings->value("autocomplete_engine", "https://www.google.com/complete/search?client=chrome&q={}").toString();
return engine_provider;
}
void Settings::set_autocomplete_engine(EngineProvider const& engine_provider)
{
m_qsettings->setValue("autocomplete_engine_name", engine_provider.name);
m_qsettings->setValue("autocomplete_engine", engine_provider.url);
}
bool Settings::enable_autocomplete()
{
return m_qsettings->value("enable_autocomplete", false).toBool();
}
void Settings::set_enable_autocomplete(bool enable)
{
m_qsettings->setValue("enable_autocomplete", enable);
}
bool Settings::enable_do_not_track()
{
return m_qsettings->value("enable_do_not_track", false).toBool();

View file

@ -44,16 +44,6 @@ public:
QStringList preferred_languages();
void set_preferred_languages(QStringList const& languages);
struct EngineProvider {
QString name;
QString url;
};
EngineProvider autocomplete_engine();
void set_autocomplete_engine(EngineProvider const& engine);
bool enable_autocomplete();
void set_enable_autocomplete(bool enable);
bool enable_do_not_track();
void set_enable_do_not_track(bool enable);

View file

@ -32,13 +32,6 @@ SettingsDialog::SettingsDialog(QMainWindow* window)
close();
});
m_enable_autocomplete = new QCheckBox(this);
m_enable_autocomplete->setChecked(Settings::the()->enable_autocomplete());
m_autocomplete_engine_dropdown = new QPushButton(this);
m_autocomplete_engine_dropdown->setText(Settings::the()->autocomplete_engine().name);
m_autocomplete_engine_dropdown->setMaximumWidth(200);
m_enable_do_not_track = new QCheckBox(this);
m_enable_do_not_track->setChecked(Settings::the()->enable_do_not_track());
#if (QT_VERSION > QT_VERSION_CHECK(6, 7, 0))
@ -49,11 +42,7 @@ SettingsDialog::SettingsDialog(QMainWindow* window)
Settings::the()->set_enable_do_not_track(state == Qt::Checked);
});
setup_autocomplete_engine();
m_layout->addRow(new QLabel("Preferred Language(s)", this), m_preferred_languages);
m_layout->addRow(new QLabel("Enable Autocomplete", this), m_enable_autocomplete);
m_layout->addRow(new QLabel("Autocomplete Engine", this), m_autocomplete_engine_dropdown);
m_layout->addRow(new QLabel("Send web sites a \"Do Not Track\" request", this), m_enable_do_not_track);
setWindowTitle("Settings");
@ -61,35 +50,4 @@ SettingsDialog::SettingsDialog(QMainWindow* window)
resize(600, 250);
}
void SettingsDialog::setup_autocomplete_engine()
{
// FIXME: These should be centralized in LibWebView.
Vector<Settings::EngineProvider> autocomplete_engines = {
{ "DuckDuckGo", "https://duckduckgo.com/ac/?q={}" },
{ "Google", "https://www.google.com/complete/search?client=chrome&q={}" },
{ "Yahoo", "https://search.yahoo.com/sugg/gossip/gossip-us-ura/?output=sd1&command={}" },
};
QMenu* autocomplete_engine_menu = new QMenu(this);
for (auto& autocomplete_engine : autocomplete_engines) {
QAction* action = new QAction(autocomplete_engine.name, this);
connect(action, &QAction::triggered, this, [&, autocomplete_engine] {
Settings::the()->set_autocomplete_engine(autocomplete_engine);
m_autocomplete_engine_dropdown->setText(autocomplete_engine.name);
});
autocomplete_engine_menu->addAction(action);
}
m_autocomplete_engine_dropdown->setMenu(autocomplete_engine_menu);
m_autocomplete_engine_dropdown->setEnabled(Settings::the()->enable_autocomplete());
#if (QT_VERSION > QT_VERSION_CHECK(6, 7, 0))
connect(m_enable_autocomplete, &QCheckBox::checkStateChanged, this, [&](int state) {
#else
connect(m_enable_autocomplete, &QCheckBox::stateChanged, this, [&](int state) {
#endif
Settings::the()->set_enable_autocomplete(state == Qt::Checked);
m_autocomplete_engine_dropdown->setEnabled(state == Qt::Checked);
});
}
}

View file

@ -23,13 +23,9 @@ public:
explicit SettingsDialog(QMainWindow* window);
private:
void setup_autocomplete_engine();
QFormLayout* m_layout;
QMainWindow* m_window { nullptr };
QLineEdit* m_preferred_languages { nullptr };
QCheckBox* m_enable_autocomplete { nullptr };
QPushButton* m_autocomplete_engine_dropdown { nullptr };
QCheckBox* m_enable_do_not_track { nullptr };
};