diff --git a/CMakeLists.txt b/CMakeLists.txt index 34720190ad6..c60f39ec97e 100644 --- a/CMakeLists.txt +++ b/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. diff --git a/Meta/gn/secondary/Ladybird/BUILD.gn b/Meta/gn/secondary/Ladybird/BUILD.gn index 2debfeb6019..627d079c058 100644 --- a/Meta/gn/secondary/Ladybird/BUILD.gn +++ b/Meta/gn/secondary/Ladybird/BUILD.gn @@ -39,7 +39,6 @@ link_qt("ladybird_qt_components") { "Core", "Gui", "Widgets", - "Network", ] } diff --git a/UI/Qt/AutoComplete.cpp b/UI/Qt/AutoComplete.cpp deleted file mode 100644 index 02e3f724bc6..00000000000 --- a/UI/Qt/AutoComplete.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2023, Cameron Youell - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include -#include -#include - -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::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> 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 results; - results.ensure_capacity(suggestions_array.size()); - for (auto const& suggestion : suggestions_array) - results.unchecked_append(suggestion.as_string()); - - return results; -} - -ErrorOr> 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 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> 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 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 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 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); -} - -} diff --git a/UI/Qt/AutoComplete.h b/UI/Qt/AutoComplete.h deleted file mode 100644 index c655bee5afa..00000000000 --- a/UI/Qt/AutoComplete.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2023, Cameron Youell - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include - -#include -#include -#include - -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 suggestions) - { - beginInsertRows({}, m_suggestions.size(), m_suggestions.size()); - m_suggestions = suggestions; - endInsertRows(); - } - -private: - AK::Vector 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 got_network_response(QNetworkReply* reply); - - ErrorOr> parse_google_autocomplete(JsonValue const&); - ErrorOr> parse_duckduckgo_autocomplete(JsonValue const&); - ErrorOr> parse_yahoo_autocomplete(JsonValue const&); - - QNetworkAccessManager* m_manager; - AutoCompleteModel* m_auto_complete_model; - QTreeView* m_tree_view; - QNetworkReply* m_reply { nullptr }; - - String m_query; -}; - -} diff --git a/UI/Qt/Autocomplete.cpp b/UI/Qt/Autocomplete.cpp new file mode 100644 index 00000000000..d6e4c22aadd --- /dev/null +++ b/UI/Qt/Autocomplete.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023, Cameron Youell + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Ladybird { + +Autocomplete::Autocomplete(QWidget* parent) + : QCompleter(parent) + , m_autocomplete(make()) + , 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)); +} + +} diff --git a/UI/Qt/Autocomplete.h b/UI/Qt/Autocomplete.h new file mode 100644 index 00000000000..f6c166955be --- /dev/null +++ b/UI/Qt/Autocomplete.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, Cameron Youell + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace Ladybird { + +class Autocomplete final : public QCompleter { + Q_OBJECT + +public: + explicit Autocomplete(QWidget* parent); + + void query_autocomplete_engine(String); + +private: + NonnullOwnPtr m_autocomplete; + + QStringListModel* m_model { nullptr }; + QListView* m_popup { nullptr }; +}; + +} diff --git a/UI/Qt/CMakeLists.txt b/UI/Qt/CMakeLists.txt index 79ba60b1f91..04768a5ad86 100644 --- a/UI/Qt/CMakeLists.txt +++ b/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) diff --git a/UI/Qt/LocationEdit.cpp b/UI/Qt/LocationEdit.cpp index 11eb2aeec50..642e9d7a7c9 100644 --- a/UI/Qt/LocationEdit.cpp +++ b/UI/Qt/LocationEdit.cpp @@ -7,8 +7,8 @@ #include #include #include +#include #include -#include #include #include @@ -20,13 +20,13 @@ namespace Ladybird { LocationEdit::LocationEdit(QWidget* parent) : QLineEdit(parent) + , m_autocomplete(new Autocomplete(this)) { update_placeholder(); - m_autocomplete = make(this); - this->setCompleter(m_autocomplete); + setCompleter(m_autocomplete); - connect(m_autocomplete, &AutoComplete::activated, [&](QModelIndex const&) { + connect(m_autocomplete, QOverload::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); } } diff --git a/UI/Qt/LocationEdit.h b/UI/Qt/LocationEdit.h index bc8f51ca8c7..365c5b6c080 100644 --- a/UI/Qt/LocationEdit.h +++ b/UI/Qt/LocationEdit.h @@ -8,12 +8,13 @@ #include #include -#include #include 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 m_autocomplete; + + Autocomplete* m_autocomplete { nullptr }; URL::URL m_url; bool m_url_is_hidden { false }; diff --git a/UI/Qt/Settings.cpp b/UI/Qt/Settings.cpp index f8734de3376..be7e09fa0f0 100644 --- a/UI/Qt/Settings.cpp +++ b/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(); diff --git a/UI/Qt/Settings.h b/UI/Qt/Settings.h index d8919cd5a7a..61e3251ef15 100644 --- a/UI/Qt/Settings.h +++ b/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); diff --git a/UI/Qt/SettingsDialog.cpp b/UI/Qt/SettingsDialog.cpp index 4e16aa56451..0ab17158b0b 100644 --- a/UI/Qt/SettingsDialog.cpp +++ b/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 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); - }); -} - } diff --git a/UI/Qt/SettingsDialog.h b/UI/Qt/SettingsDialog.h index 1532fea7698..51522ba445c 100644 --- a/UI/Qt/SettingsDialog.h +++ b/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 }; };