Using the Ubuntu WebView

Advanced usages of the Ubuntu WebView in your QML application

Dan Chapman

5 minute read

NOTE: I WROTE THIS POST SOME TIME AGO AND NEVER GOT AROUND TO FINISHING IT. I’M PUBLISHING IT ANYWAY AS IT MIGHT BE OF SOME USE TO SOME FOLKS.

Ubuntu’s own qml WebView component is a largely undocumented beast, with no guidance or examples beyond the basic “that’s how you declare it and stick the url there!”. Which i suppose is fine if you just want to create a simple webapp, but what if you want to do something more advanced?

You might be wanting to send custom events in and out of the webview to provide some user interaction between qml and the webview content; or you might want to add some simple logic to handle navigation requests; insert userscripts to apply custom syling; or add support for an apps internal url schemes, or possibly even modify or block a specific url on the fly before it even get’s requested. My point is most of this stuff is hidden and it’s difficult to know how this is supposed to work without spending alot of time sifting through the Oxide and Web browser app source.

So to save you some time i’m going to write about my findings while trying to figuring out the more advanced usages of the WebView

WHICH WEBVIEW SHOULD I USE?

As you may have seen there are 2 ways to add a WebView to your application, by either importing the Ubuntu.Web module or importing com.canonical.Oxide. They both provide access to the same underlying WebView component, but they both have their differences.

Let’s start with com.canonical.Oxide. So this WebView along with the WebContext and other associated delegates are the raw Qt5/QML bindings which exposes the chromium content api to QML applications. No UI features such as popups and context menu’s are provided by these components, but the signals/slots/properties exist for developers to add their own using a toolkit of their choice.

This is where the Ubuntu.Web module comes in. Ubuntu.Web provides the UI components and logic on top of a raw Oxide webview. So you get copy/paste (with the grab handles on touch devices), context menu’s, alert popups, file pickers and all the other stuff you would otherwise have to implement yourself. So in my opinion for Ubuntu apps this is the WebView you should always use as you get alot for free, and you can always override any of the supplied components with custom ones to suite your needs.

HOW CAN I ADD USERSCRIPTS?

The default WebContext actually comes with some preset userscripts for services like twitter. But to add your own i’ve found it easiest to use a raw Oxide WebContext.

// CustomWebContext.qml
import QtQuick 2.4
import com.canonical.Oxide 1.12 as Oxide

Oxide.WebContext {
  userScripts: [
    Oxide.UserScript {
      context: "myapp://zoom/"
      url: Qt.resolvedUrl("zoom.js")
      matchAllFrames: true
    }
  ]
}
// Main.qml
import QtQuick 2.4
import Ubuntu.Components 1.3
import Ubuntu.Web 0.2

MainView {

  [...]
  
  Page {
    
    WebView {
      id: webview
      
      context: CustomWebContext {
        id: ctxt
        userAgent: "Set a custom UA if you want"
      }
      
      property int currentZoom: 100
      
      function zoomIn() {
        if (currentZoom < 150) {
          currentZoom = currentZoom + 10
          _setZoom(currentZoom)
        }
      }
      
      function zoomOut() {
        if (currentZoom > 50) {
          currentZoom = currentZoom - 10
          _setZoom(currentZoom)
        }
      }
      
      function _setZoom(val) {
        var level = {}
        level["level"] = "%1%".arg(val)
        webview.rootFrame.sendMessageNoReply("myapp://zoom/", "ZOOM_LEVEL", level);
      }
    }    
  [...]
  }
}
// ==UserScript==
// @name           Zoooooooooooom!
// @namespace      http://example.org
// @description    Oxide UserScript to adjust the zoom level of the message body
// ==/UserScript==

oxide.addMessageHandler("ZOOM_LEVEL", function(zoom) {
    document.body.style.zoom = zoom.args["level"];
});

HOW CAN I USE MY OWN CUSTOM URL SCHEMES?

Oxide provides a way to use custom schemes by utilizing the qml engines QQmlNetworkAccessManagerFactory. Applications need to define their custom url-schemes in the WebContext

// CustomWebContext.qml
import QtQuick 2.4
import com.canonical.Oxide 1.12 as Oxide

Oxide.WebContext {

  allowedExtraUrlSchemes: ["my-scheme", "my-other-scheme"]
  
}

With these set whenever Oxide encounters a url with any of the declared schemes it will skip using chromiums internal network stack, and instead use an QNetworkAccessManager instance created via the QQmlNetworkAccessManagerFactory.

Applications need to provide there own custom QNetworkAccessManager which can handle these schemes along with a factory for the qml engine.

// AppQNAM.h
class AppQNAM : public QNetworkAccessManager
{
    Q_OBJECT
public:
    explicit AppQNAM(QObject *parent = 0) : QNetworkAccessManager(parent){}

protected:
    virtual QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData){
        if (request.url().scheme() == QStringLiteral("my-scheme")) {
            // Handle the custom scheme.
            return new CustomReply(this, request.url());
        } else {
            // This is most likely a request from another qml component like an Image{}
            // so we don't stand in the way of these
            QNetworkRequest req(request);
            req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
            return QNetworkAccessManager::createRequest(op, req, outgoingData);
        }
    }
};

Create a custom network access manager factory

// AppQNAMFactory.h
class MsgPartQNAMFactory : public QObject, public QQmlNetworkAccessManagerFactory
{
    Q_OBJECT
public:
    virtual QNetworkAccessManager *create(QObject *parent) {
      return new AppQNAM(parent);
    }
};

Create a custom reply

// CustomReply.h
class CustomReply : public QNetworkReply
{
    Q_OBJECT

public:
    CustomReply(AppQNAM *parent, const QUrl &url) : QNetworkReply(parent),
    m_url(url), m_response(0)
    {
        m_buffer.setBuffer(m_response);
        m_buffer.open(QBuffer::ReadOnly);
        buildResponse();
    }

    void close()
    {
        [...]
    }

    qint64 bytesAvailable() const
    {
        return m_buffer.bytesAvailable() + QNetworkReply::bytesAvailable();
    }

    qint64 readData(char *data, qint64 maxlen)
    {
        return m_buffer.read(data, maxlen);
    }

private:

    void buildResponse() {
        // Fetch local data and update the buffer
        [...]
        QTimer::singleShot( 0, this, SIGNAL(readyRead()) );
        QTimer::singleShot( 0, this, SIGNAL(finished()) );
    }

    QUrl m_url;
    mutable QBuffer m_buffer;
    QByteArray *m_response;
};

Set the factory on the qmlengine

// main.cpp
int main(int argc, char **argv)
{
  [...]
  
  AppQNAMFactory factory;
  m_view = new QQuickView();
  m_view->engine()->setNetworkAccessManagerFactory(&factory);
  
  [...]
}
comments powered by Disqus