blob: 91d2418b75b4ffd7733ec5b3a2852856d3e70818 [file] [log] [blame]
/*
* Copyright (C) 2018, 2019 Igalia S.L
* Copyright (C) 2018, 2019 Zodiac Inflight Innovations
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "WPEQtView.h"
#include "WPEQtViewBackend.h"
#include "WPEQtViewLoadRequest.h"
#include "WPEQtViewLoadRequestPrivate.h"
#include <QGuiApplication>
#include <QQuickWindow>
#include <QSGSimpleTextureNode>
#include <QScreen>
#include <QtPlatformHeaders/QEGLNativeContext>
#include <qpa/qplatformnativeinterface.h>
#include <wtf/glib/GUniquePtr.h>
/*!
\qmltype WPEView
\inqmlmodule org.wpewebkit.qtwpe
\brief A component for displaying web content.
WPEView is a component for displaying web content which is implemented using native
APIs on the platforms where this is available, thus it does not necessarily require
including a full web browser stack as part of the application.
WPEView provides an API compatible with Qt's QtWebView component. However
WPEView is limited to Linux platforms supporting EGL KHR extensions. WPEView
was successfully tested with the EGLFS and Wayland-EGL QPAs.
*/
WPEQtView::WPEQtView(QQuickItem* parent)
: QQuickItem(parent)
{
connect(this, &QQuickItem::windowChanged, this, &WPEQtView::configureWindow);
setFlag(ItemHasContents, true);
setAcceptedMouseButtons(Qt::AllButtons);
setAcceptHoverEvents(true);
setAcceptTouchEvents(true);
}
WPEQtView::~WPEQtView()
{
g_signal_handlers_disconnect_by_func(m_webView.get(), reinterpret_cast<gpointer>(notifyUrlChangedCallback), this);
g_signal_handlers_disconnect_by_func(m_webView.get(), reinterpret_cast<gpointer>(notifyTitleChangedCallback), this);
g_signal_handlers_disconnect_by_func(m_webView.get(), reinterpret_cast<gpointer>(notifyLoadChangedCallback), this);
g_signal_handlers_disconnect_by_func(m_webView.get(), reinterpret_cast<gpointer>(notifyLoadFailedCallback), this);
g_signal_handlers_disconnect_by_func(m_webView.get(), reinterpret_cast<gpointer>(notifyLoadProgressCallback), this);
}
void WPEQtView::geometryChanged(const QRectF& newGeometry, const QRectF&)
{
m_size = newGeometry.size();
if (m_backend)
m_backend->resize(newGeometry.size());
}
void WPEQtView::configureWindow()
{
auto* win = window();
if (!win)
return;
win->setSurfaceType(QWindow::OpenGLSurface);
connect(win, &QQuickWindow::sceneGraphInitialized, this, &WPEQtView::createWebView);
}
void WPEQtView::createWebView()
{
if (m_backend)
return;
auto display = static_cast<EGLDisplay>(QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("egldisplay"));
auto* context = window()->openglContext();
std::unique_ptr<WPEQtViewBackend> backend = WPEQtViewBackend::create(m_size, context, display, QPointer<WPEQtView>(this));
RELEASE_ASSERT_WITH_MESSAGE(backend, "EGL initialization failed");
if (!backend)
return;
m_backend = backend.get();
auto settings = adoptGRef(webkit_settings_new_with_settings("enable-developer-extras", TRUE,
"enable-webgl", TRUE, "enable-mediasource", TRUE, nullptr));
m_webView = adoptGRef(WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW,
"backend", webkit_web_view_backend_new(m_backend->backend(), [](gpointer data) {
delete static_cast<WPEQtViewBackend*>(data);
}, backend.release()),
"settings", settings.get(), nullptr)));
g_signal_connect_swapped(m_webView.get(), "notify::uri", G_CALLBACK(notifyUrlChangedCallback), this);
g_signal_connect_swapped(m_webView.get(), "notify::title", G_CALLBACK(notifyTitleChangedCallback), this);
g_signal_connect_swapped(m_webView.get(), "notify::estimated-load-progress", G_CALLBACK(notifyLoadProgressCallback), this);
g_signal_connect(m_webView.get(), "load-changed", G_CALLBACK(notifyLoadChangedCallback), this);
g_signal_connect(m_webView.get(), "load-failed", G_CALLBACK(notifyLoadFailedCallback), this);
if (!m_url.isEmpty())
webkit_web_view_load_uri(m_webView.get(), m_url.toString().toUtf8().constData());
else if (!m_html.isEmpty())
webkit_web_view_load_html(m_webView.get(), m_html.toUtf8().constData(), m_baseUrl.toString().toUtf8().constData());
Q_EMIT webViewCreated();
}
void WPEQtView::notifyUrlChangedCallback(WPEQtView* view)
{
Q_EMIT view->urlChanged();
}
void WPEQtView::notifyTitleChangedCallback(WPEQtView* view)
{
Q_EMIT view->titleChanged();
}
void WPEQtView::notifyLoadProgressCallback(WPEQtView* view)
{
Q_EMIT view->loadProgressChanged();
}
void WPEQtView::notifyLoadChangedCallback(WebKitWebView*, WebKitLoadEvent event, WPEQtView* view)
{
bool statusSet = false;
WPEQtView::LoadStatus loadStatus;
switch (event) {
case WEBKIT_LOAD_STARTED:
loadStatus = WPEQtView::LoadStatus::LoadStartedStatus;
statusSet = true;
break;
case WEBKIT_LOAD_FINISHED:
loadStatus = WPEQtView::LoadStatus::LoadSucceededStatus;
statusSet = !view->errorOccured();
view->setErrorOccured(false);
break;
default:
break;
}
if (statusSet) {
WPEQtViewLoadRequestPrivate loadRequestPrivate(view->url(), loadStatus, "");
std::unique_ptr<WPEQtViewLoadRequest> loadRequest = makeUnique<WPEQtViewLoadRequest>(loadRequestPrivate);
Q_EMIT view->loadingChanged(loadRequest.get());
}
}
void WPEQtView::notifyLoadFailedCallback(WebKitWebView*, WebKitLoadEvent, const gchar* failingURI, GError* error, WPEQtView* view)
{
view->setErrorOccured(true);
WPEQtView::LoadStatus loadStatus;
if (g_error_matches(error, WEBKIT_NETWORK_ERROR, WEBKIT_NETWORK_ERROR_CANCELLED))
loadStatus = WPEQtView::LoadStatus::LoadStoppedStatus;
else
loadStatus = WPEQtView::LoadStatus::LoadFailedStatus;
WPEQtViewLoadRequestPrivate loadRequestPrivate(QUrl(QString(failingURI)), loadStatus, error->message);
std::unique_ptr<WPEQtViewLoadRequest> loadRequest = makeUnique<WPEQtViewLoadRequest>(loadRequestPrivate);
Q_EMIT view->loadingChanged(loadRequest.get());
}
QSGNode* WPEQtView::updatePaintNode(QSGNode* node, UpdatePaintNodeData*)
{
if (!m_webView || !m_backend)
return node;
auto* textureNode = static_cast<QSGSimpleTextureNode*>(node);
if (!textureNode)
textureNode = new QSGSimpleTextureNode();
GLuint textureId = m_backend->texture(window()->openglContext());
if (!textureId)
return node;
textureNode->setTexture(window()->createTextureFromId(textureId, m_size.toSize(), QQuickWindow::TextureHasAlphaChannel));
textureNode->setRect(boundingRect());
return textureNode;
}
QUrl WPEQtView::url() const
{
if (!m_webView)
return m_url;
const gchar* uri = webkit_web_view_get_uri(m_webView.get());
return uri ? QUrl(QString(uri)) : m_url;
}
/*!
\qmlproperty url WPEView::url
The URL of currently loaded web page. Changing this will trigger
loading new content.
The URL is used as-is. URLs that originate from user input should
be parsed with QUrl::fromUserInput().
\note The WPEView does not support loading content through the Qt Resource system.
*/
void WPEQtView::setUrl(const QUrl& url)
{
if (url == m_url)
return;
m_errorOccured = false;
m_url = url;
if (m_webView)
webkit_web_view_load_uri(m_webView.get(), m_url.toString().toUtf8().constData());
}
/*!
\qmlproperty int WPEView::loadProgress
\readonly
The current load progress of the web content, represented as
an integer between 0 and 100.
*/
int WPEQtView::loadProgress() const
{
if (!m_webView)
return 0;
return webkit_web_view_get_estimated_load_progress(m_webView.get()) * 100;
}
/*!
\qmlproperty string WPEView::title
\readonly
The title of the currently loaded web page.
*/
QString WPEQtView::title() const
{
if (!m_webView)
return "";
return webkit_web_view_get_title(m_webView.get());
}
/*!
\qmlproperty bool WPEView::canGoBack
\readonly
Holds \c true if it's currently possible to navigate back in the web history.
*/
bool WPEQtView::canGoBack() const
{
if (!m_webView)
return false;
return webkit_web_view_can_go_back(m_webView.get());
}
/*!
\qmlproperty bool WPEView::loading
\readonly
Holds \c true if the WPEView is currently in the process of loading
new content, \c false otherwise.
\sa loadingChanged()
*/
/*!
\qmlsignal WPEView::loadingChanged(WPEViewLoadRequest loadRequest)
This signal is emitted when the state of loading the web content changes.
By handling this signal it's possible, for example, to react to page load
errors.
The \a loadRequest parameter holds the \e url and \e status of the request,
as well as an \e errorString containing an error message for a failed
request.
\sa WPEViewLoadRequest
*/
bool WPEQtView::isLoading() const
{
if (!m_webView)
return false;
return webkit_web_view_is_loading(m_webView.get());
}
/*!
\qmlproperty bool WPEView::canGoForward
\readonly
Holds \c true if it's currently possible to navigate forward in the web history.
*/
bool WPEQtView::canGoForward() const
{
if (!m_webView)
return false;
return webkit_web_view_can_go_forward(m_webView.get());
}
/*!
\qmlmethod void WPEView::goBack()
Navigates back in the web history.
*/
void WPEQtView::goBack()
{
if (m_webView)
webkit_web_view_go_back(m_webView.get());
}
/*!
\qmlmethod void WPEView::goForward()
Navigates forward in the web history.
*/
void WPEQtView::goForward()
{
if (m_webView)
webkit_web_view_go_forward(m_webView.get());
}
/*!
\qmlmethod void WPEView::reload()
Reloads the current \l url.
*/
void WPEQtView::reload()
{
if (m_webView)
webkit_web_view_reload(m_webView.get());
}
/*!
\qmlmethod void WPEView::stop()
Stops loading the current \l url.
*/
void WPEQtView::stop()
{
if (m_webView)
webkit_web_view_stop_loading(m_webView.get());
}
/*!
\qmlmethod void WPEView::loadHtml(string html, url baseUrl)
Loads the specified \a html content to the web view.
This method offers a lower-level alternative to the \l url property,
which references HTML pages via URL.
External objects such as stylesheets or images referenced in the HTML
document should be located relative to \a baseUrl. For example, if \a html
is retrieved from \c http://www.example.com/documents/overview.html, which
is the base URL, then an image referenced with the relative url, \c diagram.png,
should be at \c{http://www.example.com/documents/diagram.png}.
\note The WPEView does not support loading content through the Qt Resource system.
\sa url
*/
void WPEQtView::loadHtml(const QString& html, const QUrl& baseUrl)
{
m_html = html;
m_baseUrl = baseUrl;
m_errorOccured = false;
if (m_webView)
webkit_web_view_load_html(m_webView.get(), html.toUtf8().constData(), baseUrl.toString().toUtf8().constData());
}
struct JavascriptCallbackData {
WTF_MAKE_STRUCT_FAST_ALLOCATED;
JavascriptCallbackData(QJSValue cb, QPointer<WPEQtView> obj)
: callback(cb)
, object(obj) { }
QJSValue callback;
QPointer<WPEQtView> object;
};
static void jsAsyncReadyCallback(GObject* object, GAsyncResult* result, gpointer userData)
{
GUniqueOutPtr<GError> error;
std::unique_ptr<JavascriptCallbackData> data(reinterpret_cast<JavascriptCallbackData*>(userData));
WebKitJavascriptResult* jsResult = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error.outPtr());
if (!jsResult) {
qWarning("Error running javascript: %s", error->message);
return;
}
if (data->object.data()) {
QQmlEngine* engine = qmlEngine(data->object.data());
if (!engine) {
qWarning("No JavaScript engine, unable to handle JavaScript callback!");
webkit_javascript_result_unref(jsResult);
return;
}
QJSValueList args;
JSCValue* value = webkit_javascript_result_get_js_value(jsResult);
QVariant variant;
// FIXME: Handle more value types?
if (jsc_value_is_string(value)) {
GUniquePtr<gchar> strValue(jsc_value_to_string(value));
JSCContext* context = jsc_value_get_context(value);
JSCException* exception = jsc_context_get_exception(context);
if (exception) {
qWarning("Error running javascript: %s", jsc_exception_get_message(exception));
jsc_context_clear_exception(context);
} else
variant.setValue(QString(g_strdup(strValue.get())));
}
args.append(engine->toScriptValue(variant));
data->callback.call(args);
}
webkit_javascript_result_unref(jsResult);
}
/*!
\qmlmethod void WPEView::runJavaScript(string script, variant callback)
Runs the specified JavaScript.
In case a \a callback function is provided, it will be invoked after the \a script
finished running.
\badcode
runJavaScript("document.title", function(result) { console.log(result); });
\endcode
*/
void WPEQtView::runJavaScript(const QString& script, const QJSValue& callback)
{
std::unique_ptr<JavascriptCallbackData> data = makeUnique<JavascriptCallbackData>(callback, QPointer<WPEQtView>(this));
webkit_web_view_run_javascript(m_webView.get(), script.toUtf8().constData(), nullptr, jsAsyncReadyCallback, data.release());
}
void WPEQtView::mousePressEvent(QMouseEvent* event)
{
forceActiveFocus();
if (m_backend)
m_backend->dispatchMousePressEvent(event);
}
void WPEQtView::mouseReleaseEvent(QMouseEvent* event)
{
if (m_backend)
m_backend->dispatchMouseReleaseEvent(event);
}
void WPEQtView::hoverEnterEvent(QHoverEvent* event)
{
if (m_backend)
m_backend->dispatchHoverEnterEvent(event);
}
void WPEQtView::hoverLeaveEvent(QHoverEvent* event)
{
if (m_backend)
m_backend->dispatchHoverLeaveEvent(event);
}
void WPEQtView::hoverMoveEvent(QHoverEvent* event)
{
if (m_backend)
m_backend->dispatchHoverMoveEvent(event);
}
void WPEQtView::wheelEvent(QWheelEvent* event)
{
if (m_backend)
m_backend->dispatchWheelEvent(event);
}
void WPEQtView::keyPressEvent(QKeyEvent* event)
{
if (m_backend)
m_backend->dispatchKeyEvent(event, true);
}
void WPEQtView::keyReleaseEvent(QKeyEvent* event)
{
if (m_backend)
m_backend->dispatchKeyEvent(event, false);
}
void WPEQtView::touchEvent(QTouchEvent* event)
{
if (m_backend)
m_backend->dispatchTouchEvent(event);
}