diff options
32 files changed, 1712 insertions, 30 deletions
diff --git a/src/3rdparty b/src/3rdparty -Subproject 88398c89a7b34606120ff919f873cb59ce3bcf2 +Subproject 185dfeb21f478a8503f424570eff01177a46721 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 18078ba5a..904baf7d2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -76,6 +76,8 @@ foreach(arch ${archs}) accessibility_activation_observer.cpp accessibility_activation_observer.h accessibility_tree_formatter_qt.cpp authentication_dialog_controller.cpp authentication_dialog_controller.h authentication_dialog_controller_p.h + autofill_client_qt.cpp autofill_client_qt.h + autofill_popup_controller.cpp autofill_popup_controller.h autofill_popup_controller_p.h browser_accessibility_manager_qt.cpp browser_accessibility_manager_qt.h browser_accessibility_qt.cpp browser_accessibility_qt.h browser_main_parts_qt.cpp browser_main_parts_qt.h diff --git a/src/core/api/qwebenginepage.cpp b/src/core/api/qwebenginepage.cpp index 8a49e6577..332a612fc 100644 --- a/src/core/api/qwebenginepage.cpp +++ b/src/core/api/qwebenginepage.cpp @@ -59,6 +59,7 @@ #include "qwebenginesettings.h" #include "authentication_dialog_controller.h" +#include "autofill_popup_controller.h" #include "color_chooser_controller.h" #include "find_text_helper.h" #include "file_picker_controller.h" @@ -692,6 +693,19 @@ void QWebEnginePagePrivate::findTextFinished(const QWebEngineFindTextResult &res Q_EMIT q->findTextFinished(result); } +void QWebEnginePagePrivate::showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + const QRect &bounds, bool autoselectFirstSuggestion) +{ + if (view) + view->showAutofillPopup(controller, bounds, autoselectFirstSuggestion); +} + +void QWebEnginePagePrivate::hideAutofillPopup() +{ + if (view) + view->hideAutofillPopup(); +} + void QWebEnginePagePrivate::ensureInitialized() const { if (!adapter->isInitialized()) diff --git a/src/core/api/qwebenginepage_p.h b/src/core/api/qwebenginepage_p.h index 2b76795b5..68203ba76 100644 --- a/src/core/api/qwebenginepage_p.h +++ b/src/core/api/qwebenginepage_p.h @@ -61,6 +61,7 @@ #include <QtCore/QTimer> namespace QtWebEngineCore { +class AutofillPopupController; class RenderWidgetHostViewQtDelegate; class RenderWidgetHostViewQtDelegateClient; class RenderWidgetHostViewQtDelegateItem; @@ -109,6 +110,9 @@ public: virtual void didPrintPage(QPrinter *&printer, QSharedPointer<QByteArray> result) = 0; virtual void didPrintPageToPdf(const QString &filePath, bool success) = 0; virtual void printRequested() = 0; + virtual void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + const QRect &bounds, bool autoselectFirstSuggestion) = 0; + virtual void hideAutofillPopup() = 0; }; class Q_WEBENGINECORE_PRIVATE_EXPORT QWebEnginePagePrivate : public QtWebEngineCore::WebContentsAdapterClient @@ -195,6 +199,9 @@ public: const QObject *holdingQObject() const override; ClientType clientType() override { return QtWebEngineCore::WebContentsAdapterClient::WidgetsClient; } void findTextFinished(const QWebEngineFindTextResult &result) override; + void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + const QRect &bounds, bool autoselectFirstSuggestion) override; + void hideAutofillPopup() override; QtWebEngineCore::ProfileAdapter *profileAdapter() override; QtWebEngineCore::WebContentsAdapter *webContentsAdapter() override; diff --git a/src/core/autofill_client_qt.cpp b/src/core/autofill_client_qt.cpp new file mode 100644 index 000000000..b599560d1 --- /dev/null +++ b/src/core/autofill_client_qt.cpp @@ -0,0 +1,162 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "autofill_client_qt.h" + +#include "autofill_popup_controller.h" +#include "autofill_popup_controller_p.h" +#include "render_widget_host_view_qt.h" +#include "type_conversion.h" +#include "web_contents_adapter_client.h" +#include "web_contents_view_qt.h" + +#include "base/task/thread_pool.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/profiles/profile.h" +#include "components/autofill/core/common/autofill_prefs.h" +#include "content/browser/web_contents/web_contents_impl.h" + +namespace QtWebEngineCore { + +AutofillClientQt::AutofillClientQt(content::WebContents *webContents) + : content::WebContentsObserver(webContents) + , m_popupController(new AutofillPopupController(new AutofillPopupControllerPrivate)) +{ +} + +AutofillClientQt::~AutofillClientQt() { } + +autofill::PersonalDataManager *AutofillClientQt::GetPersonalDataManager() +{ + return nullptr; +} + +autofill::AutocompleteHistoryManager *AutofillClientQt::GetAutocompleteHistoryManager() +{ + return nullptr; +} + +PrefService *AutofillClientQt::GetPrefs() +{ + return const_cast<PrefService *>(base::as_const(*this).GetPrefs()); +} + +const PrefService *AutofillClientQt::GetPrefs() const +{ + Profile *profile = Profile::FromBrowserContext(web_contents()->GetBrowserContext()); + return profile->GetPrefs(); +} + +void AutofillClientQt::ShowAutofillPopup(const autofill::AutofillClient::PopupOpenArgs &open_args, + base::WeakPtr<autofill::AutofillPopupDelegate> delegate) +{ + // Specific popups (personal, address, credit card, password) are not supported. + DCHECK(open_args.popup_type == autofill::PopupType::kUnspecified); + + m_popupController->d->delegate = delegate; + m_popupController->d->suggestions = open_args.suggestions; + m_popupController->updateModel(); + + adapterClient()->showAutofillPopup(m_popupController.data(), + QRect(toQt(gfx::ToEnclosingRect(open_args.element_bounds))), + open_args.autoselect_first_suggestion.value()); +} + +void AutofillClientQt::UpdateAutofillPopupDataListValues(const std::vector<std::u16string> &values, + const std::vector<std::u16string> &labels) +{ + Q_UNUSED(labels); + + if (values.empty()) + HideAutofillPopup(autofill::PopupHidingReason::kNoSuggestions); +} + +void AutofillClientQt::PinPopupView() +{ + // Called by password_manager component only. + NOTIMPLEMENTED(); +} + +autofill::AutofillClient::PopupOpenArgs AutofillClientQt::GetReopenPopupArgs() const +{ + // Called by password_manager component only. + NOTIMPLEMENTED(); + return autofill::AutofillClient::PopupOpenArgs(); +} + +base::span<const autofill::Suggestion> AutofillClientQt::GetPopupSuggestions() const +{ + // Called by password_manager component only. + NOTIMPLEMENTED(); + return base::span<const autofill::Suggestion>(); +} + +void AutofillClientQt::UpdatePopup(const std::vector<autofill::Suggestion> &, autofill::PopupType) +{ + // Called by password_manager component only. + NOTIMPLEMENTED(); +} + +void AutofillClientQt::HideAutofillPopup(autofill::PopupHidingReason) +{ + adapterClient()->hideAutofillPopup(); +} + +bool AutofillClientQt::IsAutocompleteEnabled() +{ + return autofill::prefs::IsAutocompleteEnabled(GetPrefs()); +} + +void AutofillClientQt::PropagateAutofillPredictions(content::RenderFrameHost *, + const std::vector<autofill::FormStructure *> &) +{ + // For testing purposes only. + NOTIMPLEMENTED(); +} + +WebContentsAdapterClient *AutofillClientQt::adapterClient() +{ + return WebContentsViewQt::from( + static_cast<content::WebContentsImpl *>(web_contents())->GetView()) + ->client(); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(AutofillClientQt); + +} // namespace QtWebEngineCore diff --git a/src/core/autofill_client_qt.h b/src/core/autofill_client_qt.h new file mode 100644 index 000000000..762ff1109 --- /dev/null +++ b/src/core/autofill_client_qt.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef AUTOFILL_CLIENT_QT_H +#define AUTOFILL_CLIENT_QT_H + +#include <string> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "components/autofill/core/browser/autofill_client.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +#include <QScopedPointer> + +namespace QtWebEngineCore { + +class AutofillPopupController; +class WebContentsAdapterClient; + +class AutofillClientQt : public autofill::AutofillClient, + public content::WebContentsUserData<AutofillClientQt>, + public content::WebContentsObserver +{ +public: + ~AutofillClientQt() override; + + // autofill::AutofillClient overrides: + autofill::PersonalDataManager *GetPersonalDataManager() override; + autofill::AutocompleteHistoryManager *GetAutocompleteHistoryManager() override; + PrefService *GetPrefs() override; + const PrefService *GetPrefs() const override; + + void ShowAutofillPopup(const autofill::AutofillClient::PopupOpenArgs &open_args, + base::WeakPtr<autofill::AutofillPopupDelegate> delegate) override; + void UpdateAutofillPopupDataListValues(const std::vector<std::u16string> &values, + const std::vector<std::u16string> &labels) override; + void PinPopupView() override; + autofill::AutofillClient::PopupOpenArgs GetReopenPopupArgs() const override; + base::span<const autofill::Suggestion> GetPopupSuggestions() const override; + void UpdatePopup(const std::vector<autofill::Suggestion> &, autofill::PopupType) override; + void HideAutofillPopup(autofill::PopupHidingReason reason) override; + bool IsAutocompleteEnabled() override; + void PropagateAutofillPredictions(content::RenderFrameHost *, + const std::vector<autofill::FormStructure *> &) override; + +private: + explicit AutofillClientQt(content::WebContents *webContents); + + WebContentsAdapterClient *adapterClient(); + + QScopedPointer<AutofillPopupController> m_popupController; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); + friend class content::WebContentsUserData<AutofillClientQt>; +}; + +} // namespace QtWebEngineCore + +#endif // AUTOFILL_CLIENT_QT_H diff --git a/src/core/autofill_popup_controller.cpp b/src/core/autofill_popup_controller.cpp new file mode 100644 index 000000000..e42f5a4f6 --- /dev/null +++ b/src/core/autofill_popup_controller.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "autofill_popup_controller.h" +#include "autofill_popup_controller_p.h" + +#include "components/autofill/core/browser/ui/autofill_popup_delegate.h" +#include "components/autofill/core/browser/ui/suggestion.h" + +namespace QtWebEngineCore { + +AutofillPopupController::AutofillPopupController(AutofillPopupControllerPrivate *dd) +{ + Q_ASSERT(dd); + d.reset(dd); +} + +AutofillPopupController::~AutofillPopupController() { } + +void AutofillPopupController::setCurrentIndex(const QModelIndex &index) +{ + if (m_currentIndex == index) + return; + + m_currentIndex = index; + + if (m_currentIndex.isValid()) { + const autofill::Suggestion &suggestion = d->suggestions[m_currentIndex.row()]; + d->delegate->DidSelectSuggestion(suggestion.value, suggestion.frontend_id); + } + + Q_EMIT currentIndexChanged(index); +} + +void AutofillPopupController::selectPreviousSuggestion() +{ + if (!m_currentIndex.isValid()) { + setCurrentIndex(m_model.index(m_model.rowCount() - 1, 0)); + return; + } + + if (m_currentIndex.row() == 0) { + selectLastSuggestion(); + return; + } + + setCurrentIndex(m_model.index(m_currentIndex.row() - 1, 0)); +} + +void AutofillPopupController::selectNextSuggestion() +{ + if (!m_currentIndex.isValid()) { + setCurrentIndex(m_model.index(0, 0)); + return; + } + + if (m_currentIndex.row() == m_model.rowCount() - 1) { + selectFirstSuggestion(); + return; + } + + setCurrentIndex(m_model.index(m_currentIndex.row() + 1, 0)); +} + +void AutofillPopupController::selectFirstSuggestion() +{ + setCurrentIndex(m_model.index(0, 0)); +} + +void AutofillPopupController::selectLastSuggestion() +{ + setCurrentIndex(m_model.index(m_model.rowCount() - 1, 0)); +} + +void AutofillPopupController::acceptSuggestion() +{ + if (!m_currentIndex.isValid()) + return; + + const int index = m_currentIndex.row(); + const autofill::Suggestion &suggestion = d->suggestions[index]; + d->delegate->DidAcceptSuggestion(suggestion.value, suggestion.frontend_id, + suggestion.backend_id, index); +} + +void AutofillPopupController::notifyPopupShown() +{ + d->delegate->OnPopupShown(); +} + +void AutofillPopupController::notifyPopupHidden() +{ + d->delegate->OnPopupHidden(); +} + +void AutofillPopupController::selectSuggestion(int index) +{ + if (index < 0) + setCurrentIndex(QModelIndex()); + else + setCurrentIndex(m_model.index(index, 0)); +} + +void AutofillPopupController::updateModel() +{ + QStringList values; + for (size_t i = 0; i < d->suggestions.size(); ++i) { + values.append(QString::fromStdU16String(d->suggestions[i].value)); + } + m_model.setStringList(values); + setCurrentIndex(QModelIndex()); +} + +} // namespace QtWebEngineCore diff --git a/src/core/autofill_popup_controller.h b/src/core/autofill_popup_controller.h new file mode 100644 index 000000000..5432123f1 --- /dev/null +++ b/src/core/autofill_popup_controller.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef AUTOFILL_POPUP_CONTROLLER_H +#define AUTOFILL_POPUP_CONTROLLER_H + +#include <QtWebEngineCore/private/qtwebenginecoreglobal_p.h> + +#include <QModelIndex> +#include <QObject> +#include <QScopedPointer> +#include <QStringListModel> + +namespace QtWebEngineCore { + +class AutofillPopupControllerPrivate; + +class Q_WEBENGINECORE_PRIVATE_EXPORT AutofillPopupController : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringListModel *model READ model CONSTANT FINAL) + +public: + ~AutofillPopupController(); + + QStringListModel *model() { return &m_model; } + + void selectPreviousSuggestion(); + void selectNextSuggestion(); + void selectFirstSuggestion(); + void selectLastSuggestion(); + + void notifyPopupShown(); + void notifyPopupHidden(); + +public Q_SLOTS: + void selectSuggestion(int index); + void acceptSuggestion(); + +Q_SIGNALS: + void currentIndexChanged(const QModelIndex &index); + +private: + AutofillPopupController(AutofillPopupControllerPrivate *); + QScopedPointer<AutofillPopupControllerPrivate> d; + + void setCurrentIndex(const QModelIndex &index); + + // Only called by AutofillClientQt: + void updateModel(); + + QStringListModel m_model; + QModelIndex m_currentIndex; + + friend class AutofillClientQt; +}; + +} // namespace QtWebEngineCore + +Q_DECLARE_METATYPE(QtWebEngineCore::AutofillPopupController *) + +#endif // AUTOFILL_POPUP_CONTROLLER_H diff --git a/src/core/autofill_popup_controller_p.h b/src/core/autofill_popup_controller_p.h new file mode 100644 index 000000000..880eec86e --- /dev/null +++ b/src/core/autofill_popup_controller_p.h @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef AUTOFILL_POPUP_CONTROLLER_P_H +#define AUTOFILL_POPUP_CONTROLLER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "components/autofill/core/browser/ui/suggestion.h" + +namespace autofill { +class AutofillPopupDelegate; +} + +namespace QtWebEngineCore { + +class AutofillPopupControllerPrivate +{ +public: + AutofillPopupControllerPrivate() = default; + + base::WeakPtr<autofill::AutofillPopupDelegate> delegate = nullptr; + std::vector<autofill::Suggestion> suggestions; +}; + +} // namespace QtWebEngineCore + +#endif // AUTOFILL_POPUP_CONTROLLER_P_H diff --git a/src/core/configure/BUILD.root.gn.in b/src/core/configure/BUILD.root.gn.in index 35fe32476..f1fa1eaa3 100644 --- a/src/core/configure/BUILD.root.gn.in +++ b/src/core/configure/BUILD.root.gn.in @@ -104,6 +104,9 @@ shared_library("QtWebEngineCore") { defines = [ "CHROMIUM_VERSION=\"" + chromium_version[0] + "\"" ] deps = [ "//base", + "//components/autofill/content/browser", + "//components/autofill/content/renderer", + "//components/autofill/core/browser", "//components/cdm/renderer", "//components/error_page/common", "//components/favicon/content", diff --git a/src/core/content_browser_client_qt.cpp b/src/core/content_browser_client_qt.cpp index 1bbac4439..0fdf1f397 100644 --- a/src/core/content_browser_client_qt.cpp +++ b/src/core/content_browser_client_qt.cpp @@ -42,6 +42,7 @@ #include "base/files/file_util.h" #include "base/task/post_task.h" #include "chrome/browser/tab_contents/form_interaction_tab_helper.h" +#include "components/autofill/content/browser/content_autofill_driver_factory.h" #include "components/custom_handlers/protocol_handler_registry.h" #include "components/error_page/common/error.h" #include "components/error_page/common/localized_error.h" @@ -502,6 +503,8 @@ void ContentBrowserClientQt::ExposeInterfacesToRenderer(service_manager::BinderR #endif } +// TODO: BindAssociatedReceiverForFrame -> RegisterAssociatedInterfaceBindersForRenderFrameHost +// https://chromium-review.googlesource.com/c/chromium/src/+/3281481 bool ContentBrowserClientQt::BindAssociatedReceiverFromFrame(content::RenderFrameHost *rfh, const std::string &interface_name, mojo::ScopedInterfaceEndpointHandle *handle) @@ -529,6 +532,14 @@ bool ContentBrowserClientQt::BindAssociatedReceiverFromFrame(content::RenderFram return true; } #endif + + if (interface_name == autofill::mojom::AutofillDriver::Name_) { + mojo::PendingAssociatedReceiver<autofill::mojom::AutofillDriver> receiver( + std::move(*handle)); + autofill::ContentAutofillDriverFactory::BindAutofillDriver(std::move(receiver), rfh); + return true; + } + DCHECK(!ContentBrowserClient::BindAssociatedReceiverFromFrame(rfh, interface_name, handle)); return false; } diff --git a/src/core/pref_service_adapter.cpp b/src/core/pref_service_adapter.cpp index d7795c7e9..f3a99c3e2 100644 --- a/src/core/pref_service_adapter.cpp +++ b/src/core/pref_service_adapter.cpp @@ -46,6 +46,7 @@ #include "base/threading/thread_restrictions.h" #include "chrome/browser/prefs/chrome_command_line_pref_store.h" #include "content/public/browser/browser_thread.h" +#include "components/autofill/core/common/autofill_prefs.h" #include "components/language/core/browser/pref_names.h" #include "components/prefs/pref_member.h" #include "components/prefs/in_memory_pref_store.h" @@ -131,6 +132,14 @@ void PrefServiceAdapter::setup(const ProfileAdapter &profileAdapter) // default value will be different. We'll need to initialize it later. registry->RegisterStringPref(kPrefMediaDeviceIDSalt, std::string()); + registry->RegisterBooleanPref(autofill::prefs::kAutofillEnabledDeprecated, false); + registry->RegisterBooleanPref(autofill::prefs::kAutofillProfileEnabled, false); + registry->RegisterBooleanPref(autofill::prefs::kAutofillCreditCardEnabled, false); + registry->RegisterBooleanPref(autofill::prefs::kAutofillCreditCardFidoAuthEnabled, false); + registry->RegisterBooleanPref(autofill::prefs::kAutofillWalletImportEnabled, false); + registry->RegisterBooleanPref(autofill::prefs::kAutofillJapanCityFieldMigratedDeprecated, + false); + { base::ScopedAllowBlocking allowBlock; m_prefService = factory.Create(registry); diff --git a/src/core/renderer/content_renderer_client_qt.cpp b/src/core/renderer/content_renderer_client_qt.cpp index b410ad377..ffb5178e7 100644 --- a/src/core/renderer/content_renderer_client_qt.cpp +++ b/src/core/renderer/content_renderer_client_qt.cpp @@ -47,6 +47,10 @@ #include "components/spellcheck/renderer/spellcheck.h" #include "components/spellcheck/renderer/spellcheck_provider.h" #endif +#include "components/autofill/content/renderer/autofill_agent.h" +#include "components/autofill/content/renderer/autofill_assistant_agent.h" +#include "components/autofill/content/renderer/password_autofill_agent.h" +#include "components/autofill/content/renderer/password_generation_agent.h" #include "components/cdm/renderer/external_clear_key_key_system_properties.h" #include "components/cdm/renderer/widevine_key_system_properties.h" #include "components/error_page/common/error.h" @@ -210,8 +214,10 @@ void ContentRendererClientQt::RenderFrameCreated(content::RenderFrame *render_fr #if QT_CONFIG(webengine_printing_and_pdf) new printing::PrintRenderFrameHelper(render_frame, base::WrapUnique(new PrintWebViewHelperDelegateQt())); #endif // QT_CONFIG(webengine_printing_and_pdf) -#if BUILDFLAG(ENABLE_EXTENSIONS) + blink::AssociatedInterfaceRegistry *associated_interfaces = render_frame_observer->associatedInterfaces(); + +#if BUILDFLAG(ENABLE_EXTENSIONS) associated_interfaces->AddInterface(base::BindRepeating( &extensions::MimeHandlerViewContainerManager::BindReceiver, render_frame->GetRoutingID())); @@ -219,6 +225,17 @@ void ContentRendererClientQt::RenderFrameCreated(content::RenderFrame *render_fr auto registry = std::make_unique<service_manager::BinderRegistry>(); ExtensionsRendererClientQt::GetInstance()->RenderFrameCreated(render_frame, render_frame_observer->registry()); #endif + + autofill::AutofillAssistantAgent *autofill_assistant_agent = + new autofill::AutofillAssistantAgent(render_frame); + autofill::PasswordAutofillAgent *password_autofill_agent = + new autofill::PasswordAutofillAgent(render_frame, associated_interfaces); + autofill::PasswordGenerationAgent *password_generation_agent = + new autofill::PasswordGenerationAgent(render_frame, password_autofill_agent, + associated_interfaces); + + new autofill::AutofillAgent(render_frame, password_autofill_agent, password_generation_agent, + autofill_assistant_agent, associated_interfaces); } void ContentRendererClientQt::RunScriptsAtDocumentStart(content::RenderFrame *render_frame) diff --git a/src/core/web_contents_adapter.cpp b/src/core/web_contents_adapter.cpp index 45dfd1017..525ee728d 100644 --- a/src/core/web_contents_adapter.cpp +++ b/src/core/web_contents_adapter.cpp @@ -43,6 +43,7 @@ #include "web_contents_adapter.h" +#include "autofill_client_qt.h" #include "devtools_frontend_qt.h" #include "download_manager_delegate_qt.h" #include "favicon_driver_qt.h" @@ -66,6 +67,8 @@ #include "base/task/sequence_manager/thread_controller_with_message_pump_impl.h" #include "base/values.h" #include "chrome/browser/tab_contents/form_interaction_tab_helper.h" +#include "components/autofill/core/browser/autofill_manager.h" +#include "components/autofill/content/browser/content_autofill_driver_factory.h" #include "components/favicon/core/favicon_service.h" #include "content/browser/renderer_host/render_view_host_impl.h" #include "content/browser/renderer_host/text_input_manager.h" @@ -517,6 +520,11 @@ void WebContentsAdapter::initialize(content::SiteInstance *site) FaviconDriverQt::CreateForWebContents( webContents(), FaviconServiceFactoryQt::GetForBrowserContext(context), m_adapterClient); + AutofillClientQt::CreateForWebContents(webContents()); + autofill::ContentAutofillDriverFactory::CreateForWebContentsAndDelegate( + webContents(), AutofillClientQt::FromWebContents(webContents()), + /* app_locale = */ "", autofill::AutofillManager::DISABLE_AUTOFILL_DOWNLOAD_MANAGER); + // Create an instance of WebEngineVisitedLinksManager to catch the first // content::NOTIFICATION_RENDERER_PROCESS_CREATED event. This event will // force to initialize visited links in VisitedLinkSlave. diff --git a/src/core/web_contents_adapter_client.h b/src/core/web_contents_adapter_client.h index 2729e7beb..0ad91e92e 100644 --- a/src/core/web_contents_adapter_client.h +++ b/src/core/web_contents_adapter_client.h @@ -80,6 +80,7 @@ struct DropData; namespace QtWebEngineCore { +class AutofillPopupController; class CertificateErrorController; class ClientCertSelectController; class AuthenticationDialogController; @@ -245,6 +246,9 @@ public: virtual void showTouchSelectionMenu(TouchSelectionMenuController *menuController, const QRect &bounds, const QSize &handleSize) = 0; virtual void hideTouchSelectionMenu() = 0; virtual void findTextFinished(const QWebEngineFindTextResult &result) = 0; + virtual void showAutofillPopup(AutofillPopupController *controller, const QRect &bounds, + bool autoselectFirstSuggestion) = 0; + virtual void hideAutofillPopup() = 0; virtual ProfileAdapter *profileAdapter() = 0; virtual WebContentsAdapter* webContentsAdapter() = 0; diff --git a/src/webenginequick/api/qquickwebengineview.cpp b/src/webenginequick/api/qquickwebengineview.cpp index babd0ade7..015f916b3 100644 --- a/src/webenginequick/api/qquickwebengineview.cpp +++ b/src/webenginequick/api/qquickwebengineview.cpp @@ -55,6 +55,7 @@ #include "qquickwebengineview_p_p.h" #include "authentication_dialog_controller.h" +#include "autofill_popup_controller.h" #include "profile_adapter.h" #include "file_picker_controller.h" #include "find_text_helper.h" @@ -737,6 +738,19 @@ void QQuickWebEngineViewPrivate::findTextFinished(const QWebEngineFindTextResult Q_EMIT q->findTextFinished(result); } +void QQuickWebEngineViewPrivate::showAutofillPopup( + QtWebEngineCore::AutofillPopupController *controller, const QRect &bounds, + bool autoselectFirstSuggestion) +{ + ui()->showAutofillPopup(controller, bounds.bottomLeft(), bounds.width() + 2, + autoselectFirstSuggestion); +} + +void QQuickWebEngineViewPrivate::hideAutofillPopup() +{ + ui()->hideAutofillPopup(); +} + QWebEngineSettings *QQuickWebEngineViewPrivate::webEngineSettings() const { return m_settings->d_ptr.data(); diff --git a/src/webenginequick/api/qquickwebengineview_p_p.h b/src/webenginequick/api/qquickwebengineview_p_p.h index fe9f78322..9f066af90 100644 --- a/src/webenginequick/api/qquickwebengineview_p_p.h +++ b/src/webenginequick/api/qquickwebengineview_p_p.h @@ -167,6 +167,10 @@ public: QtWebEngineCore::WebContentsAdapter *webContentsAdapter() override; void printRequested() override; void findTextFinished(const QWebEngineFindTextResult &result) override; + void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + const QRect &bounds, bool autoselectFirstSuggestion) override; + void hideAutofillPopup() override; + void updateAction(QQuickWebEngineView::WebAction) const; bool adoptWebContents(QtWebEngineCore::WebContentsAdapter *webContents); void setProfile(QQuickWebEngineProfile *profile); diff --git a/src/webenginequick/ui/AutofillPopup.qml b/src/webenginequick/ui/AutofillPopup.qml new file mode 100644 index 000000000..28b274bb6 --- /dev/null +++ b/src/webenginequick/ui/AutofillPopup.qml @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls + +Popup { + id: root + // Let Chromium close the popup. + closePolicy: Popup.NoAutoClose + + property variant controller: null + property int itemHeight: 0 + + signal selected(int index) + signal accepted() + + function setCurrentIndex(index) + { + listView.currentIndex = index; + } + + ListView { + id: listView + anchors.fill: parent + clip: true + + model: controller.model + currentIndex: -1 + + delegate: ItemDelegate { + width: listView.width + height: root.itemHeight + text: model.display + highlighted: ListView.isCurrentItem + + onHoveredChanged: if (hovered) selected(index); + onClicked: accepted(); + } + } +} diff --git a/src/webenginequick/ui/CMakeLists.txt b/src/webenginequick/ui/CMakeLists.txt index aa5832ba6..c24d8da8d 100644 --- a/src/webenginequick/ui/CMakeLists.txt +++ b/src/webenginequick/ui/CMakeLists.txt @@ -1,6 +1,7 @@ set(qml_files "AlertDialog.qml" "AuthenticationDialog.qml" + "AutofillPopup.qml" "ColorDialog.qml" "ConfirmDialog.qml" "FilePicker.qml" diff --git a/src/webenginequick/ui_delegates_manager.cpp b/src/webenginequick/ui_delegates_manager.cpp index 5a01ea2f0..06b72348c 100644 --- a/src/webenginequick/ui_delegates_manager.cpp +++ b/src/webenginequick/ui_delegates_manager.cpp @@ -43,6 +43,7 @@ #include "api/qquickwebengineview_p_p.h" #include <authentication_dialog_controller.h> +#include <autofill_popup_controller.h> #include <color_chooser_controller.h> #include <file_picker_controller.h> #include <javascript_dialog_controller.h> @@ -58,6 +59,9 @@ #include <QtQml/qqmlcontext.h> #include <QtQml/qqmlengine.h> #include <QtQml/qqmlproperty.h> +#include <QtQuick/qquickwindow.h> + +#include <algorithm> // Uncomment for QML debugging //#define UI_DELEGATES_DEBUG @@ -129,7 +133,7 @@ UIDelegatesManager::UIDelegatesManager(QQuickWebEngineView *view) : m_view(view) , m_toolTip(nullptr) , m_touchSelectionMenu(nullptr) - FOR_EACH_COMPONENT_TYPE(COMPONENT_MEMBER_INIT, NO_SEPARATOR) + , m_autofillPopup(nullptr) FOR_EACH_COMPONENT_TYPE(COMPONENT_MEMBER_INIT, NO_SEPARATOR) { } @@ -567,6 +571,158 @@ void UIDelegatesManager::hideTouchSelectionMenu() QTimer::singleShot(0, m_view, [this] { m_touchSelectionMenu.reset(); }); } +bool AutofillPopupEventFilter::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); + if (keyEvent->key() == Qt::Key_Escape) { + m_manager->hideAutofillPopup(); + return true; + } + + // Ignore shortcuts while the popup is open. It may result unwanted + // edit commands sent to Chromium that blocks the key press. + event->ignore(); + return true; + } + + // AutofillPopupControllerImpl::HandleKeyPressEvent() + // chrome/browser/ui/autofill/autofill_popup_controller_impl.cc + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); + switch (keyEvent->key()) { + case Qt::Key_Up: + m_controller->selectPreviousSuggestion(); + return true; + case Qt::Key_Down: + m_controller->selectNextSuggestion(); + return true; + case Qt::Key_PageUp: + m_controller->selectFirstSuggestion(); + return true; + case Qt::Key_PageDown: + m_controller->selectLastSuggestion(); + return true; + case Qt::Key_Escape: + m_manager->hideAutofillPopup(); + return true; + case Qt::Key_Enter: + case Qt::Key_Return: + m_controller->acceptSuggestion(); + return true; + case Qt::Key_Delete: + // Remove suggestion is not supported for datalist. + // Forward delete to view to be able to remove selected text. + break; + case Qt::Key_Tab: + m_controller->acceptSuggestion(); + break; + default: + break; + } + } + + // Do not forward release events of the overridden key presses. + if (event->type() == QEvent::KeyRelease) { + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); + switch (keyEvent->key()) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Escape: + case Qt::Key_Enter: + case Qt::Key_Return: + return true; + default: + break; + } + } + + return QObject::eventFilter(object, event); +} + +void UIDelegatesManager::showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + QPointF pos, int width, bool autoselectFirstSuggestion) +{ + static const int padding = 1; + static const int itemHeight = 20; + const int proposedHeight = itemHeight * (controller->model()->rowCount()) + padding * 2; + + bool popupWasNull = false; + if (m_autofillPopup.isNull()) { + popupWasNull = true; + if (!ensureComponentLoaded(AutofillPopup)) + return; + + QQmlContext *context = qmlContext(m_view); + m_autofillPopup.reset(autofillPopupComponent->beginCreate(context)); + if (QQuickItem *item = qobject_cast<QQuickItem *>(m_autofillPopup.data())) + item->setParentItem(m_view); + m_autofillPopup->setParent(m_view); + } + + m_autofillPopup->setProperty("controller", QVariant::fromValue(controller)); + m_autofillPopup->setProperty("x", pos.x()); + m_autofillPopup->setProperty("y", pos.y()); + m_autofillPopup->setProperty("width", width); + m_autofillPopup->setProperty("height", + std::min(proposedHeight, qRound(m_view->height() - pos.y()))); + m_autofillPopup->setProperty("padding", padding); + m_autofillPopup->setProperty("itemHeight", itemHeight); + + if (popupWasNull) { + QQmlProperty selectedSignal(m_autofillPopup.data(), QStringLiteral("onSelected")); + CHECK_QML_SIGNAL_PROPERTY(selectedSignal, autofillPopupComponent->url()); + static int selectSuggestionIndex = + controller->metaObject()->indexOfSlot("selectSuggestion(int)"); + QObject::connect(m_autofillPopup.data(), selectedSignal.method(), controller, + controller->metaObject()->method(selectSuggestionIndex)); + + QQmlProperty acceptedSignal(m_autofillPopup.data(), QStringLiteral("onAccepted")); + CHECK_QML_SIGNAL_PROPERTY(acceptedSignal, autofillPopupComponent->url()); + static int acceptSuggestionIndex = + controller->metaObject()->indexOfSlot("acceptSuggestion()"); + QObject::connect(m_autofillPopup.data(), acceptedSignal.method(), controller, + controller->metaObject()->method(acceptSuggestionIndex)); + + QObject::connect(controller, &QtWebEngineCore::AutofillPopupController::currentIndexChanged, + [this](const QModelIndex &index) { + QMetaObject::invokeMethod(m_autofillPopup.data(), "setCurrentIndex", + Qt::DirectConnection, + Q_ARG(QVariant, index.row())); + }); + + autofillPopupComponent->completeCreate(); + + m_view->window()->installEventFilter( + new AutofillPopupEventFilter(controller, this, m_autofillPopup.data())); + + QMetaObject::invokeMethod(m_autofillPopup.data(), "open"); + controller->notifyPopupShown(); + } + + if (autoselectFirstSuggestion) + controller->selectFirstSuggestion(); +} + +void UIDelegatesManager::hideAutofillPopup() +{ + if (!m_autofillPopup) + return; + + QTimer::singleShot(0, m_view, [this] { + if (m_autofillPopup) { + QtWebEngineCore::AutofillPopupController *controller = + m_autofillPopup->property("controller") + .value<QtWebEngineCore::AutofillPopupController *>(); + m_autofillPopup.reset(); + controller->notifyPopupHidden(); + } + }); +} + bool UIDelegatesManager::initializeImportDirs(QStringList &dirs, QQmlEngine *engine) { const QStringList paths = engine->importPathList(); diff --git a/src/webenginequick/ui_delegates_manager.h b/src/webenginequick/ui_delegates_manager.h index 0f515d4be..218d403d4 100644 --- a/src/webenginequick/ui_delegates_manager.h +++ b/src/webenginequick/ui_delegates_manager.h @@ -47,19 +47,20 @@ #include <QtCore/qstring.h> #include <QtCore/qstringlist.h> -#define FOR_EACH_COMPONENT_TYPE(F, SEPARATOR) \ - F(Menu, menu) SEPARATOR \ - F(MenuItem, menuItem) SEPARATOR \ - F(MenuSeparator, menuSeparator) SEPARATOR \ - F(AlertDialog, alertDialog) SEPARATOR \ - F(ColorDialog, colorDialog) SEPARATOR \ - F(ConfirmDialog, confirmDialog) SEPARATOR \ - F(PromptDialog, promptDialog) SEPARATOR \ - F(FilePicker, filePicker) SEPARATOR \ - F(AuthenticationDialog, authenticationDialog) SEPARATOR \ - F(ToolTip, toolTip) SEPARATOR \ - F(TouchHandle, touchHandle) SEPARATOR \ - F(TouchSelectionMenu, touchSelectionMenu) SEPARATOR \ +#define FOR_EACH_COMPONENT_TYPE(F, SEPARATOR) \ + F(Menu, menu) SEPARATOR F(MenuItem, menuItem) \ + SEPARATOR \ + F(MenuSeparator, menuSeparator) SEPARATOR F(AlertDialog, alertDialog) \ + SEPARATOR \ + F(ColorDialog, colorDialog) SEPARATOR F(ConfirmDialog, confirmDialog) \ + SEPARATOR \ + F(PromptDialog, promptDialog) SEPARATOR F(FilePicker, filePicker) \ + SEPARATOR \ + F(AuthenticationDialog, authenticationDialog) SEPARATOR F(ToolTip, toolTip) \ + SEPARATOR \ + F(TouchHandle, touchHandle) SEPARATOR F(TouchSelectionMenu, touchSelectionMenu) \ + SEPARATOR \ + F(AutofillPopup, autofillPopup) SEPARATOR #define COMMA_SEPARATOR , #define SEMICOLON_SEPARATOR ; @@ -79,6 +80,7 @@ QT_END_NAMESPACE namespace QtWebEngineCore { class AuthenticationDialogController; +class AutofillPopupController; class ColorChooserController; class FilePickerController; class JavaScriptDialogController; @@ -115,14 +117,18 @@ public: QQuickItem *createTouchHandle(); void showTouchSelectionMenu(TouchSelectionMenuController *, const QRect &, const int spacing); void hideTouchSelectionMenu(); + void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, QPointF pos, + int width, bool autoselectFirstSuggestion); + void hideAutofillPopup(); private: bool ensureComponentLoaded(ComponentType); QQuickWebEngineView *m_view; - QScopedPointer<QObject> m_toolTip; QStringList m_importDirs; + QScopedPointer<QObject> m_toolTip; QScopedPointer<QObject> m_touchSelectionMenu; + QScopedPointer<QObject> m_autofillPopup; FOR_EACH_COMPONENT_TYPE(MEMBER_DECLARATION, SEMICOLON_SEPARATOR) @@ -130,6 +136,25 @@ private: }; +class AutofillPopupEventFilter : public QObject +{ + Q_OBJECT + +public: + AutofillPopupEventFilter(QtWebEngineCore::AutofillPopupController *controller, + UIDelegatesManager *manager, QObject *parent) + : QObject(parent), m_controller(controller), m_manager(manager) + { + } + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + QtWebEngineCore::AutofillPopupController *m_controller; + UIDelegatesManager *m_manager; +}; + } // namespace QtWebEngineCore #endif // UI_DELEGATES_MANAGER_H diff --git a/src/webenginewidgets/CMakeLists.txt b/src/webenginewidgets/CMakeLists.txt index 1aa4ae1e2..97bde6b56 100644 --- a/src/webenginewidgets/CMakeLists.txt +++ b/src/webenginewidgets/CMakeLists.txt @@ -9,6 +9,7 @@ qt_internal_add_module(WebEngineWidgets api/qwebenginenotificationpresenter.cpp api/qwebenginenotificationpresenter_p.h api/qwebengineview.cpp api/qwebengineview.h api/qwebengineview_p.h qwebengine_accessible.cpp qwebengine_accessible.h + ui/autofillpopupwidget.cpp ui/autofillpopupwidget_p.h DEFINES QT_BUILD_WEBENGINEWIDGETS_LIB INCLUDE_DIRECTORIES diff --git a/src/webenginewidgets/api/qwebengineview.cpp b/src/webenginewidgets/api/qwebengineview.cpp index 9fa5f11fc..5fdd995b8 100644 --- a/src/webenginewidgets/api/qwebengineview.cpp +++ b/src/webenginewidgets/api/qwebengineview.cpp @@ -43,12 +43,15 @@ #include "render_widget_host_view_qt_delegate_client.h" #include "render_widget_host_view_qt_delegate_item.h" #include "qwebengine_accessible.h" +#include "ui/autofillpopupwidget_p.h" #include <QtWebEngineCore/private/qwebenginepage_p.h> #include <QtWebEngineCore/qwebenginecontextmenurequest.h> #include <QtWebEngineCore/qwebenginehistory.h> #include <QtWebEngineCore/qwebenginehttprequest.h> #include <QtWebEngineCore/qwebengineprofile.h> + +#include "autofill_popup_controller.h" #include "color_chooser_controller.h" #include "web_contents_adapter.h" @@ -911,6 +914,34 @@ QWebEngineContextMenuRequest *QWebEngineViewPrivate::lastContextMenuRequest() co { return m_contextRequest; } + +void QWebEngineViewPrivate::showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + const QRect &bounds, bool autoselectFirstSuggestion) +{ + Q_Q(QWebEngineView); + if (!m_autofillPopupWidget) + m_autofillPopupWidget.reset(new QtWebEngineWidgetUI::AutofillPopupWidget(controller, q)); + m_autofillPopupWidget->showPopup(q->mapToGlobal(bounds.bottomLeft()), bounds.width() + 2, + autoselectFirstSuggestion); + controller->notifyPopupShown(); +} + +void QWebEngineViewPrivate::hideAutofillPopup() +{ + if (!m_autofillPopupWidget) + return; + + Q_Q(QWebEngineView); + QTimer::singleShot(0, q, [this] { + if (m_autofillPopupWidget) { + QtWebEngineCore::AutofillPopupController *controller = + m_autofillPopupWidget->m_controller; + m_autofillPopupWidget.reset(); + controller->notifyPopupHidden(); + } + }); +} + /*! \fn QWebEngineView::renderProcessTerminated(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode) \since 5.6 diff --git a/src/webenginewidgets/api/qwebengineview.h b/src/webenginewidgets/api/qwebengineview.h index cc1495d24..b3dd66f85 100644 --- a/src/webenginewidgets/api/qwebengineview.h +++ b/src/webenginewidgets/api/qwebengineview.h @@ -47,6 +47,10 @@ #include <QtWebEngineWidgets/qtwebenginewidgetsglobal.h> #include <QtWebEngineCore/qwebenginepage.h> +namespace QtWebEngineWidgetUI { +class AutofillPopupWidget; +} + QT_BEGIN_NAMESPACE class QContextMenuEvent; @@ -169,6 +173,7 @@ private: Q_DECLARE_PRIVATE(QWebEngineView) QScopedPointer<QWebEngineViewPrivate> d_ptr; + friend class QtWebEngineWidgetUI::AutofillPopupWidget; friend class QWebEnginePage; friend class QWebEnginePagePrivate; #if QT_CONFIG(accessibility) diff --git a/src/webenginewidgets/api/qwebengineview_p.h b/src/webenginewidgets/api/qwebengineview_p.h index e3c9468ce..44eb32203 100644 --- a/src/webenginewidgets/api/qwebengineview_p.h +++ b/src/webenginewidgets/api/qwebengineview_p.h @@ -56,12 +56,17 @@ #include "render_view_context_menu_qt.h" namespace QtWebEngineCore { +class AutofillPopupController; class QWebEngineContextMenuRequest; class WebEngineQuickWidget; class RenderWidgetHostViewQtDelegate; class RenderWidgetHostViewQtDelegateClient; } +namespace QtWebEngineWidgetUI { +class AutofillPopupWidget; +} + QT_BEGIN_NAMESPACE class QMenu; @@ -99,6 +104,9 @@ public: void didPrintPage(QPrinter *&printer, QSharedPointer<QByteArray> result) override; void didPrintPageToPdf(const QString &filePath, bool success) override; void printRequested() override; + void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, + const QRect &bounds, bool autoselectFirstSuggestion) override; + void hideAutofillPopup() override; QWebEngineViewPrivate(); virtual ~QWebEngineViewPrivate(); @@ -117,6 +125,7 @@ public: bool m_dragEntered; mutable bool m_ownsPage; QWebEngineContextMenuRequest *m_contextRequest; + QScopedPointer<QtWebEngineWidgetUI::AutofillPopupWidget> m_autofillPopupWidget; }; class QContextMenuBuilder : public QtWebEngineCore::RenderViewContextMenuQt diff --git a/src/webenginewidgets/ui/autofillpopupwidget.cpp b/src/webenginewidgets/ui/autofillpopupwidget.cpp new file mode 100644 index 000000000..579869d9e --- /dev/null +++ b/src/webenginewidgets/ui/autofillpopupwidget.cpp @@ -0,0 +1,237 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "autofillpopupwidget_p.h" +#include "qwebengineview.h" +#include "qwebengineview_p.h" + +#include "autofill_popup_controller.h" + +#include <QApplication> +#include <QBoxLayout> +#include <QEvent> +#include <QKeyEvent> +#include <QListView> +#include <QMouseEvent> + +namespace QtWebEngineWidgetUI { + +AutofillPopupWidget::AutofillPopupWidget(QtWebEngineCore::AutofillPopupController *controller, + QWebEngineView *parent) + : QFrame(parent, Qt::Popup), m_controller(controller), m_webEngineView(parent) +{ + setAttribute(Qt::WA_WindowPropagation); + setAttribute(Qt::WA_X11NetWmWindowTypeCombo); + + // we need a vertical layout + QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, this); + layout->setSpacing(0); + layout->setContentsMargins(QMargins()); + + m_listView = new QListView(m_webEngineView); + m_listView->setModel(m_controller->model()); + m_listView->setTextElideMode(Qt::ElideMiddle); + + // Based on QComboBoxPrivateContainer::setItemView + m_listView->setParent(this); + m_listView->setAttribute(Qt::WA_MacShowFocusRect, false); + layout->insertWidget(0, m_listView); + m_listView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + + m_listView->installEventFilter(this); + // Necessary for filtering QEvent::MouseMove: + m_listView->viewport()->installEventFilter(this); + + m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + // TODO: Implement vertical scrollbar. Chromium also has it. + m_listView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_listView->setMouseTracking(true); + + m_listView->setSelectionMode(QAbstractItemView::SingleSelection); + m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); + + // Some styles (Mac) have a margin at the top and bottom of the popup. + layout->insertSpacing(0, 0); + layout->addSpacing(0); + + connect(m_controller, &QtWebEngineCore::AutofillPopupController::currentIndexChanged, + m_listView, &QListView::setCurrentIndex); +} + +AutofillPopupWidget::~AutofillPopupWidget() { } + +// Based on QComboBox::showPopup() +void AutofillPopupWidget::showPopup(QPoint pos, int width, bool autoselectFirstSuggestion) +{ + QStyle *const style = m_webEngineView->style(); + QStyleOptionComboBox opt; + opt.initFrom(m_webEngineView); + + if (autoselectFirstSuggestion) + m_controller->selectFirstSuggestion(); + + QRect listRect(pos, QSize(width, 0)); + + // Calculate height + { + int listHeight = 0; + int rowCount = m_controller->model()->rowCount(); + for (int i = 0; i < rowCount; ++i) { + QModelIndex idx = m_controller->model()->index(i, 0); + listHeight += m_listView->visualRect(idx).height(); + } + if (rowCount > 1) + listHeight += (rowCount - 1) * m_listView->spacing() * 2; + + listRect.setHeight(listRect.height() + listHeight); + } + + // Calculate height margin + { + int heightMargin = m_listView->spacing() * 2; + + // Add the frame of the popup + const QMargins pm = contentsMargins(); + heightMargin += pm.top() + pm.bottom(); + + // Add the frame of the list view + const QMargins vm = m_listView->contentsMargins(); + heightMargin += vm.top() + vm.bottom(); + listRect.setHeight(listRect.height() + heightMargin); + } + + // Add space for margin at top and bottom if the style wants it + int styleMargin = style->pixelMetric(QStyle::PM_MenuVMargin, &opt, this) * 2; + listRect.setHeight(listRect.height() + styleMargin); + + // Takes account of the mimium/maximum size of the popup + layout()->activate(); + listRect.setSize(listRect.size().expandedTo(minimumSize()).boundedTo(maximumSize())); + + setGeometry(listRect); + QFrame::show(); +} + +bool AutofillPopupWidget::eventFilter(QObject *object, QEvent *event) +{ + switch (event->type()) { + case QEvent::MouseMove: + if (isVisible()) { + QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); + QModelIndex indexUnderMouse = m_listView->indexAt(mouseEvent->position().toPoint()); + if (indexUnderMouse.isValid() + && indexUnderMouse.data(Qt::AccessibleDescriptionRole).toString() + != QLatin1String("separator")) { + m_controller->selectSuggestion(indexUnderMouse.row()); + } + } + return true; + case QEvent::MouseButtonRelease: { + QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); + if (mouseEvent->button() == Qt::LeftButton) { + m_controller->acceptSuggestion(); + return true; + } + break; + } + default: + break; + } + + return QFrame::eventFilter(object, event); +} + +void AutofillPopupWidget::keyPressEvent(QKeyEvent *event) +{ + // AutofillPopupControllerImpl::HandleKeyPressEvent() + // chrome/browser/ui/autofill/autofill_popup_controller_impl.cc + switch (event->key()) { + case Qt::Key_Up: + m_controller->selectPreviousSuggestion(); + return; + case Qt::Key_Down: + m_controller->selectNextSuggestion(); + return; + case Qt::Key_PageUp: + m_controller->selectFirstSuggestion(); + return; + case Qt::Key_PageDown: + m_controller->selectLastSuggestion(); + return; + case Qt::Key_Escape: + m_webEngineView->d_ptr->hideAutofillPopup(); + return; + case Qt::Key_Enter: + case Qt::Key_Return: + m_controller->acceptSuggestion(); + return; + case Qt::Key_Delete: + // Remove suggestion is not supported for datalist. + // Forward delete to view to be able to remove selected text. + break; + case Qt::Key_Tab: + m_controller->acceptSuggestion(); + break; + default: + break; + } + + QCoreApplication::sendEvent(m_webEngineView->focusWidget(), event); +} + +void AutofillPopupWidget::keyReleaseEvent(QKeyEvent *event) +{ + // Do not forward release events of the overridden key presses. + switch (event->key()) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Escape: + case Qt::Key_Enter: + case Qt::Key_Return: + return; + default: + break; + } + + QCoreApplication::sendEvent(m_webEngineView->focusWidget(), event); +} + +} // namespace QtWebEngineWidgetUI diff --git a/src/webenginewidgets/ui/autofillpopupwidget_p.h b/src/webenginewidgets/ui/autofillpopupwidget_p.h new file mode 100644 index 000000000..064c4f824 --- /dev/null +++ b/src/webenginewidgets/ui/autofillpopupwidget_p.h @@ -0,0 +1,94 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef AUTOFILLPOPUPWIDGET_P_H +#define AUTOFILLPOPUPWIDGET_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QFrame> + +namespace QtWebEngineCore { +class AutofillPopupController; +} + +QT_BEGIN_NAMESPACE +class QListView; +class QWebEngineView; +class QWebEngineViewPrivate; +QT_END_NAMESPACE + +namespace QtWebEngineWidgetUI { + +// Based on QComboBoxPrivateContainer +class AutofillPopupWidget : public QFrame +{ + Q_OBJECT +public: + AutofillPopupWidget(QtWebEngineCore::AutofillPopupController *controller, + QWebEngineView *parent); + ~AutofillPopupWidget(); + + void showPopup(QPoint pos, int width, bool autoselectFirstSuggestion); + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + +private: + QtWebEngineCore::AutofillPopupController *m_controller; + QWebEngineView *m_webEngineView; + QListView *m_listView; + + friend class QT_PREPEND_NAMESPACE(QWebEngineViewPrivate); +}; + +} // namespace QtWebEngineWidgetUI + +#endif // AUTOFILLPOPUPWIDGET_P_H diff --git a/tests/auto/quick/qmltests/CMakeLists.txt b/tests/auto/quick/qmltests/CMakeLists.txt index d878bdfcf..53c8ed231 100644 --- a/tests/auto/quick/qmltests/CMakeLists.txt +++ b/tests/auto/quick/qmltests/CMakeLists.txt @@ -19,6 +19,7 @@ set(testList tst_audioMuted.qml tst_contextMenu.qml tst_basicProfiles.qml + tst_datalist.qml tst_desktopBehaviorLoadHtml.qml tst_download.qml tst_favicon.qml diff --git a/tests/auto/quick/qmltests/data/TestWebEngineView.qml b/tests/auto/quick/qmltests/data/TestWebEngineView.qml index 68417c6c5..869754c90 100644 --- a/tests/auto/quick/qmltests/data/TestWebEngineView.qml +++ b/tests/auto/quick/qmltests/data/TestWebEngineView.qml @@ -113,6 +113,21 @@ WebEngineView { return textSelection; } + function getElementValue(element) { + var elementValue; + runJavaScript("document.getElementById('" + element + "').value", function(result) { + elementValue = result; + }); + testCase.tryVerify(function() { return elementValue != undefined; }); + return elementValue; + } + + function compareElementValue(element, expected) { + testCase.tryVerify(function() { return expected == getElementValue(element); }, 5000, + "Value of element \"" + element + "\" is \"" + expected + "\""); + } + + TestResult { id: testResult } onLoadingChanged: function(load) { diff --git a/tests/auto/quick/qmltests/data/tst_datalist.qml b/tests/auto/quick/qmltests/data/tst_datalist.qml new file mode 100644 index 000000000..66cc49b0c --- /dev/null +++ b/tests/auto/quick/qmltests/data/tst_datalist.qml @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtTest +import QtWebEngine + +TestWebEngineView { + id: webEngineView + width: 200 + height: 400 + + property string html: "<html><body>" + + "<input id='browserInput' list='browserDatalist'>" + + "<datalist id='browserDatalist'>" + + " <option value='Internet Explorer'>" + + " <option value='Firefox'>" + + " <option value='Chrome'>" + + " <option value='Opera'>" + + " <option value='Safari'>" + + "</datalist>" + + "</body></html>" + + function listView() { + if (webEngineView.parent.visibleChildren.length == 1) { + // No popup case. + return null; + } + + let overlay = null; + for (let i = 0; i < webEngineView.parent.visibleChildren.length; ++i) { + let child = webEngineView.parent.visibleChildren[i]; + if (child instanceof Overlay) { + overlay = child; + break; + } + } + + if (!overlay) + return null; + + let popupItem = null; + for (let i = 0; i < overlay.visibleChildren[0].visibleChildren.length; ++i) { + let child = overlay.visibleChildren[0].visibleChildren[i]; + if (child.objectName == "QQuickPopupItem") { + popupItem = child; + } + } + + if (!popupItem) + return null; + + for (let i = 0; i < popupItem.visibleChildren.length; ++i) { + let child = popupItem.visibleChildren[i]; + if (child instanceof ListView) + return child; + } + + return null; + } + + TestCase { + id: testCase + name: "WebEngineDatalist" + when: windowShown + + function test_showAndHide() { + webEngineView.loadHtml(webEngineView.html); + verify(webEngineView.waitForLoadSucceeded()); + + var values = ""; + webEngineView.runJavaScript( + "(function() {" + + " var browserDatalist = document.getElementById('browserDatalist');" + + " var options = browserDatalist.options;" + + " var result = [];" + + " for (let i = 0; i < options.length; ++i) {" + + " result.push(options[i].value);" + + " }" + + " return result;" + + "})();", function(result) { values = result; }); + tryVerify(function() { return values.length != 0; }); + compare(values, ["Internet Explorer", "Firefox", "Chrome", "Opera", "Safari"]); + compareElementValue("browserInput", ""); + + // Make sure there is no open popup yet. + verify(!listView()); + // Click in the input field. + var browserInputCenter = getElementCenter("browserInput"); + mouseClick(webEngineView, browserInputCenter.x, browserInputCenter.y, Qt.LeftButton); + // Wait for the popup. + tryVerify(function() { return listView() != null; }); + + // No suggestion is selected. + verify(!listView().currentItem); + compare(listView().count, 5); + + // Accepting suggestion does nothing. + keyClick(Qt.Key_Enter); + tryVerify(function() { return listView() != null; }); + verify(!listView().currentItem); + + // Escape should close popup. + keyClick(Qt.Key_Escape); + tryVerify(function() { return listView() == null; }); + + // Key Down should open the popup and select the first suggestion. + keyClick(Qt.Key_Down); + tryVerify(function() { return listView() != null; }); + compare(listView().currentIndex, 0); + verify(listView().currentItem); + } + + function test_keyboardNavigationAndAccept() { + webEngineView.loadHtml(html); + verify(webEngineView.waitForLoadSucceeded()); + setFocusToElement("browserInput"); + + // Make sure there is no open popup yet. + verify(!listView()); + + // Key Down should open the popup and select the first suggestion. + keyClick(Qt.Key_Down); + tryVerify(function() { return listView() != null; }); + compare(listView().currentIndex, 0); + + // Test keyboard navigation in list. + keyClick(Qt.Key_Up); + compare(listView().currentIndex, 4); + keyClick(Qt.Key_Up); + compare(listView().currentIndex, 3); + keyClick(Qt.Key_PageDown); + compare(listView().currentIndex, 4); + keyClick(Qt.Key_PageUp); + compare(listView().currentIndex, 0); + keyClick(Qt.Key_Down); + compare(listView().currentIndex, 1); + keyClick(Qt.Key_Down); + compare(listView().currentIndex, 2); + + // Test accepting suggestion. + compare(listView().currentItem.text, "Chrome"); + keyClick(Qt.Key_Enter); + compareElementValue("browserInput", "Chrome"); + // Accept closes popup. + tryVerify(function() { return listView() == null; }); + + // Clear input field, should not trigger popup. + webEngineView.runJavaScript("document.getElementById('browserInput').value = ''"); + compareElementValue("browserInput", ""); + verify(listView() == null); + } + + function test_filterSuggestion() { + webEngineView.loadHtml(html); + verify(webEngineView.waitForLoadSucceeded()); + setFocusToElement("browserInput"); + + // Make sure there is no open popup yet. + verify(!listView()); + + // Filter suggestions. + keyClick(Qt.Key_F); + tryVerify(function() { return listView() != null; }); + compare(listView().count, 2); + verify(!listView().currentItem); + compare(listView().itemAtIndex(0).text, "Firefox"); + compare(listView().itemAtIndex(1).text, "Safari"); + keyClick(Qt.Key_I); + tryVerify(function() { return listView().count == 1; }); + verify(!listView().currentItem); + compare(listView().itemAtIndex(0).text, "Firefox"); + keyClick(Qt.Key_L); + // Mismatch should close popup. + tryVerify(function() { return listView() == null; }); + compareElementValue("browserInput", "fil"); + } + } +} diff --git a/tests/auto/quick/qmltests/data/tst_keyboardEvents.qml b/tests/auto/quick/qmltests/data/tst_keyboardEvents.qml index 7c594d921..170dde5e7 100644 --- a/tests/auto/quick/qmltests/data/tst_keyboardEvents.qml +++ b/tests/auto/quick/qmltests/data/tst_keyboardEvents.qml @@ -54,20 +54,6 @@ TestWebEngineView { "Element \"" + element + "\" is " + (expected ? "" : "not") + " checked"); } - function getElementValue(element) { - var elementValue; - runJavaScript("document.getElementById('" + element + "').value", function(result) { - elementValue = result; - }); - tryVerify(function() { return elementValue != undefined; }); - return elementValue; - } - - function compareElementValue(element, expected) { - tryVerify(function() { return expected == getElementValue(element); }, 5000, - "Value of element \"" + element + "\" is \"" + expected + "\""); - } - function test_keyboardEvents() { webEngineView.url = Qt.resolvedUrl("keyboardEvents.html"); verify(webEngineView.waitForLoadSucceeded()); diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp index 80da4664c..f54fdbbfc 100644 --- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp +++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp @@ -37,6 +37,7 @@ #include <QDropEvent> #include <QLabel> #include <QLineEdit> +#include <QListView> #include <QHBoxLayout> #include <QMenu> #include <QMimeData> @@ -44,6 +45,7 @@ #include <QQuickWidget> #include <QtWebEngineCore/qwebenginehttprequest.h> #include <QScopeGuard> +#include <QStringListModel> #include <QTcpServer> #include <QTcpSocket> #include <QStyle> @@ -179,6 +181,7 @@ private Q_SLOTS: void inspectElement(); void navigateOnDrop_data(); void navigateOnDrop(); + void datalist(); }; // This will be called before the first test function is executed. @@ -3597,5 +3600,154 @@ void tst_QWebEngineView::navigateOnDrop() } } +void tst_QWebEngineView::datalist() +{ + QString html("<html><body>" + "<input id='browserInput' list='browserDatalist'>" + "<datalist id='browserDatalist'>" + " <option value='Internet Explorer'>" + " <option value='Firefox'>" + " <option value='Chrome'>" + " <option value='Opera'>" + " <option value='Safari'>" + "</datalist>" + "</body></html>"); + + QWebEngineView view; + view.resize(200, 400); + view.show(); + + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + QSignalSpy loadSpy(&view, &QWebEngineView::loadFinished); + view.setHtml(html); + QTRY_COMPARE(loadSpy.count(), 1); + + QString listValuesJS("(function() {" + " var browserDatalist = document.getElementById('browserDatalist');" + " var options = browserDatalist.options;" + " var result = [];" + " for (let i = 0; i < options.length; ++i) {" + " result.push(options[i].value);" + " }" + " return result;" + "})();"); + QStringList values = evaluateJavaScriptSync(view.page(), listValuesJS).toStringList(); + QCOMPARE(values, QStringList({ "Internet Explorer", "Firefox", "Chrome", "Opera", "Safari" })); + QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('browserInput').value;") + .toString(), + QStringLiteral("")); + + auto listView = [&view]() -> QListView * { + if (QApplication::topLevelWidgets().size() == 1) { + // No popup case. + return nullptr; + } + + QWidget *autofillPopupWidget = nullptr; + for (QWidget *w : QApplication::topLevelWidgets()) { + if (w != &view) { + autofillPopupWidget = w; + break; + } + } + + if (!autofillPopupWidget) + return nullptr; + + for (QObject *o : autofillPopupWidget->children()) { + if (QListView *listView = qobject_cast<QListView *>(o)) + return listView; + } + + return nullptr; + }; + + // Make sure there is no open popup yet. + QVERIFY(!listView()); + // Click in the input field. + QPoint browserInputCenter = elementCenter(view.page(), "browserInput"); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, browserInputCenter); + // Wait for the popup. + QTRY_VERIFY(listView()); + + // No suggestion is selected. + QCOMPARE(listView()->currentIndex(), QModelIndex()); + QCOMPARE(listView()->model()->rowCount(), 5); + + // Accepting suggestion does nothing. + QTest::keyClick(view.windowHandle(), Qt::Key_Enter); + QVERIFY(listView()); + QCOMPARE(listView()->currentIndex(), QModelIndex()); + + // Escape should close popup. + QTest::keyClick(view.windowHandle(), Qt::Key_Escape); + QTRY_VERIFY(!listView()); + + // Key Down should open the popup and select the first suggestion. + QTest::keyClick(view.windowHandle(), Qt::Key_Down); + QTRY_VERIFY(listView()); + QCOMPARE(listView()->currentIndex().row(), 0); + + // Test keyboard navigation in list. + QTest::keyClick(view.windowHandle(), Qt::Key_Up); + QCOMPARE(listView()->currentIndex().row(), 4); + QTest::keyClick(view.windowHandle(), Qt::Key_Up); + QCOMPARE(listView()->currentIndex().row(), 3); + QTest::keyClick(view.windowHandle(), Qt::Key_PageDown); + QCOMPARE(listView()->currentIndex().row(), 4); + QTest::keyClick(view.windowHandle(), Qt::Key_PageUp); + QCOMPARE(listView()->currentIndex().row(), 0); + QTest::keyClick(view.windowHandle(), Qt::Key_Down); + QCOMPARE(listView()->currentIndex().row(), 1); + QTest::keyClick(view.windowHandle(), Qt::Key_Down); + QCOMPARE(listView()->currentIndex().row(), 2); + + // Test accepting suggestion. + QCOMPARE(static_cast<QStringListModel *>(listView()->model()) + ->data(listView()->currentIndex()) + .toString(), + QStringLiteral("Chrome")); + QTest::keyClick(view.windowHandle(), Qt::Key_Enter); + QTRY_COMPARE( + evaluateJavaScriptSync(view.page(), "document.getElementById('browserInput').value") + .toString(), + QStringLiteral("Chrome")); + // Accept closes popup. + QTRY_VERIFY(!listView()); + + // Clear input field, should not trigger popup. + evaluateJavaScriptSync(view.page(), "document.getElementById('browserInput').value = ''"); + QVERIFY(!listView()); + + // Filter suggestions. + QTest::keyClick(view.windowHandle(), Qt::Key_F); + QTRY_VERIFY(listView()); + QCOMPARE(listView()->model()->rowCount(), 2); + QCOMPARE(listView()->currentIndex(), QModelIndex()); + QCOMPARE(static_cast<QStringListModel *>(listView()->model()) + ->data(listView()->model()->index(0, 0)) + .toString(), + QStringLiteral("Firefox")); + QCOMPARE(static_cast<QStringListModel *>(listView()->model()) + ->data(listView()->model()->index(1, 0)) + .toString(), + QStringLiteral("Safari")); + QTest::keyClick(view.windowHandle(), Qt::Key_I); + QTRY_COMPARE(listView()->model()->rowCount(), 1); + QCOMPARE(listView()->currentIndex(), QModelIndex()); + QCOMPARE(static_cast<QStringListModel *>(listView()->model()) + ->data(listView()->model()->index(0, 0)) + .toString(), + QStringLiteral("Firefox")); + QTest::keyClick(view.windowHandle(), Qt::Key_L); + // Mismatch should close popup. + QTRY_VERIFY(!listView()); + QTRY_COMPARE( + evaluateJavaScriptSync(view.page(), "document.getElementById('browserInput').value") + .toString(), + QStringLiteral("fil")); +} + QTEST_MAIN(tst_QWebEngineView) #include "tst_qwebengineview.moc" |