Browse Source

UI/Qt: Migrate to LibWebView's autocomplete engine

As a result, we now no longer depend on Qt::Network.
Timothy Flynn 1 week ago
parent
commit
60dd5cc4ef

+ 1 - 1
CMakeLists.txt

@@ -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.

+ 0 - 1
Meta/gn/secondary/Ladybird/BUILD.gn

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

+ 0 - 172
UI/Qt/AutoComplete.cpp

@@ -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);
-}
-
-}

+ 0 - 94
UI/Qt/AutoComplete.h

@@ -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 - 0
UI/Qt/Autocomplete.cpp

@@ -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 - 0
UI/Qt/Autocomplete.h

@@ -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 };
+};
+
+}

+ 2 - 2
UI/Qt/CMakeLists.txt

@@ -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)

+ 13 - 17
UI/Qt/LocationEdit.cpp

@@ -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);
     }
 }

+ 6 - 4
UI/Qt/LocationEdit.h

@@ -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 };

+ 0 - 24
UI/Qt/Settings.cpp

@@ -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();

+ 0 - 10
UI/Qt/Settings.h

@@ -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);
 

+ 0 - 42
UI/Qt/SettingsDialog.cpp

@@ -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);
-    });
-}
-
 }

+ 0 - 4
UI/Qt/SettingsDialog.h

@@ -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 };
 };