[Qtwebengine] new QML window API

Hausmann Simon Simon.Hausmann at digia.com
Fri Dec 27 19:01:46 CET 2013


Hi,

I keep on thinking about the new window QML API and how much I'd like it to be
convenient, i.e. hide the process of stitching the newly created window
component to engine-given web contents object entirey from the developer. I
think I have found a solution:

Generally speaking the webview can be created in two different contexts:

1) A new window is created by the user, or for example on start-up. In this
context there's nothing for the implementation to adopt or do.

2) A new window is created in the context of an engine requesting/informing us
to do so.

Now if we want to allow the user to specify the environment the QML webview
lives in as a QML component, then we are presented with an arbitrary QML type.
It could be simple like this:

Component {
    id: viewComponent

    WebView {
        newWindowComponent: viewComponent
    }
}

Or it could be deeply nested:

Component {
    id: viewComponent

    SomeOtherType {
        ...
    }
}

SomeOtherType.qml:
import QtQuick 2.0
import QtWebEngine 1.0

SomeCrazyItem {
    WebView {
        newWindowComponent: viewComponent // this works, but it's better to use an intermediate property
    }
}

In these two cases the user supplied component will eventually create one
WebView. It is also possible that multiple WebViews are created, maybe each
WebView also has a WebView for the web inspector or some meta-data.  I'd like
to classify the first two cases as very common and the latter as rare.

I think the solution for the common case is simply to specify and determine the
the context of creation, in the literal as well as in the QML sense. When a new
instance is created with 'viewComponent", the QML context for the new type will
be a child context of viewComponent's context.

We can utilize this concept and whenever the WebView itself wants to create a
new window with a given WebContents, we take the creation context of the view
component, but inject an intermediate QQmlContext - the NewWindowContext. In
this context we can put anything we want. We could do it on a QML property
level, but I actually think it's easier to simply do that in C++.

So when a WebView is created, in its componentComplete() implementation we can
fetch the QML context that belongs do this instance and walk up the parent
context chain until we either find the root context or a NewWindowContext. If
the latter, then we can extract all the data we need for a successful
WebContents adoption. That's entirely transparent to the user of the API.

In the rare case where somebody specifies multiple WebView instances, we need
an API for the developer to specify which WebView in the component shall be the
primary one for new window content, for example through a property that
indicates whether the WebView should adopt web contents or not.

The resulting API makes the common case intuitive and trivial: The component
specified must have a WebView _somewhere_ in its object hierarchy and it'll
automatically be picked up and used for the context of newly created windows.
The rare case we can make possible by requiring a little bit of help from the
user.

In addition to all this we can of course still have informational signals about
newly created windows, etc. - but they don't come with any contractual
obligations :)

I've hacked up a prototype below that demonstrates the concept. Right now it
uses a dedicated QQmlContext for each new-window created WebView, but using an
incubator we could also avoid the "overhead" and get rid of the dummy context I
think.

Let me know what you think :)


Simon

#include <QtQml>

class NewWindowContext : public QQmlContext
{
    Q_OBJECT
public:
    NewWindowContext(QQmlContext *parentContext)
        : QQmlContext(parentContext)
        , handle(0)
    {}

    // Use QExplicitlySharedDataPointer<WebContentsAdapter> in real version :)
    void *handle;
};

class WebView : public QObject, public QQmlParserStatus
{
    Q_OBJECT
    Q_INTERFACES(QQmlParserStatus)
    Q_PROPERTY(QQmlComponent* newWindowComponent READ newWindowComponent WRITE setNewWindowComponent)
public:
    WebView();

    void setNewWindowComponent(QQmlComponent *comp) { m_newWindowComponent = comp; }
    QQmlComponent *newWindowComponent() const { return m_newWindowComponent; }

    Q_INVOKABLE void createNewWindow();

    virtual void classBegin() {}
    virtual void componentComplete();

private:
    QQmlComponent *m_newWindowComponent;
};

WebView::WebView()
    : m_newWindowComponent(0)
{
    qDebug() << "WebView::WebView()";
}

void WebView::createNewWindow()
{
    Q_ASSERT(m_newWindowComponent);

    QQmlContext *parentContext = m_newWindowComponent->creationContext();
    if (!parentContext)
        parentContext = qmlContext(this);
    // ### can this happen??
    if (!parentContext) {
        QQmlEngine *engine = qmlEngine(this);
        Q_ASSERT(engine);
        parentContext = engine->rootContext();
    }
    NewWindowContext *newContext = new NewWindowContext(parentContext);
    newContext->handle = this; // Or anything, really :)
    m_newWindowComponent->create(newContext);
}

void WebView::componentComplete()
{
    QQmlContext *context = qmlContext(this);

    void *handle = 0;
    while (context) {
        if (NewWindowContext *newWindowContext = qobject_cast<NewWindowContext*>(context)) {
            qSwap(newWindowContext->handle, handle);
            break;
        }
        context = context->parentContext();
    }
    if (handle)
        qDebug() << "WebView created from another WebView!" << handle;
    else
        qDebug() << "Standalone WebView complete";
}

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);

    QQmlEngine engine;

    qmlRegisterType<WebView>("TestWebEngine", 1, 0, "WebView");

    QQmlComponent doc(&engine);
    doc.setData(""
            "import QtQml 2.0\n"
            "import TestWebEngine 1.0\n"

            "QtObject {\n"
            "    id: root\n"
            "    property string test: 'Hello World'\n"
            "\n"
            "    property Component factory: Component {\n"
            "        id: viewComponent\n"
            "\n"
            "        WebView {\n"
            "            newWindowComponent: viewComponent\n"
            "        }\n"
            "    }\n"
            "\n"
            "    property QtObject initialWebView: factory.createObject();\n"
            "}", QUrl());

    QScopedPointer<QObject> root(doc.create());
    if (!root) {
        qDebug() << doc.errors().first().toString();
        return 1;
    }

    QObject *initialView = qvariant_cast<QObject*>(root->property("initialWebView"));
    QMetaObject::invokeMethod(initialView, "createNewWindow");

    return 0;
}

#include "main.moc"



More information about the QtWebEngine mailing list