Deploying HTML5 Applications with GNOME

Exclusive offer: get 50% off this eBook here
GNOME 3 Application Development Beginner's Guide

GNOME 3 Application Development Beginner's Guide — Save 50%

Step-by-step practical guide to get to grips with GNOME application development book and ebook.

$26.99    $13.50
by Mohammad Anwari | May 2013 | Beginner's Guides Open Source

The concept of running HTML5 applications is like running the application in a stripped-down web browser as a wrapper. The UI wrapper for our HTML5 application will be written in Vala, using the GTK+ flavor of the famous WebKit layout engine, which is called WebKitGTK+.

In this article, by Mohammad Anwari author of GNOME 3 Application Development Beginner's Guide, we will not only learn how to run our HTML5 applications inside a UI wrapper,but will also learn to use GNOME platform as the middleware. Specifically, our topics for this article are as follows:

  • Embedding WebKit inside our GTK+ application

  • Introducing JavaScriptCore

  • Interfacing with JavaScriptCore

(For more resources related to this topic, see here.)

Before we start

Most of the discussions in this article require a moderate knowledge of HTML5, JSON, and common client-side JavaScript programming. One particular exercise uses JQuery and JQuery Mobile to show how a real HTML5 application will be implemented.

Embedding WebKit

What we need to learn first is how to embed a WebKit layout engine inside our GTK+ application. Embedding WebKit means we can use HTML and CSS as our user interface instead of GTK+ or Clutter.

Time for action – embedding WebKit

With WebKitGTK+, this is a very easy task to do; just follow these steps:

  1. Create an empty Vala project without GtkBuilder and no license. Name it hello-webkit.

  2. Modify configure.ac to include WebKitGTK+ into the project. Find the following line of code in the file:

    PKG_CHECK_MODULES(HELLO_WEBKIT, [gtk+-3.0])

  3. Remove the previous line and replace it with the following one:

    PKG_CHECK_MODULES(HELLO_WEBKIT, [gtk+-3.0 webkitgtk-3.0])

  4. Modify Makefile.am inside the src folder to include WebKitGTK into the Vala compilation pipeline. Find the following lines of code in the file:

    hello_webkit_VALAFLAGS = \
    --pkg gtk+-3.0

  5. Remove it and replace it completely with the following lines:

    hello_webkit_VALAFLAGS = \
    --vapidir . --pkg gtk+-3.0 --pkg webkit-1.0 --pkg
    libsoup-2.4

  6. Fill the hello_webkit.vala file inside the src folder with the following lines:

    using GLib;
    using Gtk;
    using WebKit;
    public class Main : WebView
    {
    public Main ()
    {
    load_html_string("<h1>Hello</h1>","/");
    }
    static int main (string[] args)
    {
    Gtk.init (ref args);
    var webView = new Main ();
    var window = new Gtk.Window();
    window.add(webView);
    window.show_all ();
    Gtk.main ();
    return 0;
    }
    }

  7. Copy the accompanying webkit-1.0.vapi file into the src folder. We need to do this, unfortunately, because the webkit-1.0.vapi file distributed with many distributions is still using GTK+ Version 2.

  8. Run it, you will see a window with the message Hello, as shown in the following screenshot:

What just happened?

What we need to do first is to include WebKit into our namespace, so we can use all the functions and classes from it.

using WebKit;

Our class is derived from the WebView widget. It is an important widget in WebKit, which is capable of showing a web page. Showing it means not only parsing and displaying the DOM properly, but that it's capable to run the scripts and handle the styles referred to by the document. The derivation declaration is put in the class declaration as shown next:

public class Main : WebView

In our constructor, we only load a string and parse it as an HTML document. The string is Hello, styled with level 1 heading. After the execution of the following line, WebKit will parse and display the presentation of the HTML5 code inside its body:

public Main ()
{
load_html_string("<h1>Hello</h1>","/");
}

In our main function, what we need to do is create a window to put our WebView widget into. After adding the widget, we need to call the show_all() function in order to display both the window and the widget.

static int main (string[] args)
{
Gtk.init (ref args);
var webView = new Main ();
var window = new Gtk.Window();
window.add(webView);

The window content now only has a WebView widget as its sole displaying widget. At this point, we no longer use GTK+ to show our UI, but it is all written in HTML5.

Runtime with JavaScriptCore

An HTML5 application is, most of the time, accompanied by client-side scripts that are written in JavaScript and a set of styling definition written in CSS3. WebKit already provides the feature of running client-side JavaScript (running the script inside the web page) with a component called JavaScriptCore, so we don't need to worry about it.

But how about the connection with the GNOME platform? How to make the client-side script access the GNOME objects? One approach is that we can expose our objects, which are written in Vala so that they can be used by the client-side JavaScript. This is where we will utilize JavaScriptCore.

We can think of this as a frontend and backend architecture pattern. All of the code of business process which touch GNOME will reside in the backend. They are all written in Vala and run by the main process. On the opposite side, the frontend, the code is written in JavaScript and HTML5, and is run by WebKit internally. The frontend is what the user sees while the backend is what is going on behind the scene.

Consider the following diagram of our application. The backend part is grouped inside a grey bordered box and run in the main process. The frontend is outside the box and run and displayed by WebKit. From the diagram, we can see that the frontend creates an object and calls a function in the created object. The object we create is not defined in the client side, but is actually created at the backend. We ask JavaScriptCore to act as a bridge to connect the object created at the backend to be made accessible by the frontend code.

To do this, we wrap the backend objects with JavaScriptCore class and function definitions. For each object we want to make available to frontend, we need to create a mapping in the JavaScriptCore side. In the following diagram, we first map the MyClass object, then the helloFromVala function, then the intFromVala, and so on:

Time for action – calling the Vala object from the frontend

Now let's try and create a simple client-side JavaScript code and call an object defined at the backend:

  1. Create an empty Vala project, without GtkBuilder and no license. Name it hello-jscore.

  2. Modify configure.ac to include WebKitGTK+ exactly like our previous experiment.

  3. Modify Makefile.am inside the src folder to include WebKitGTK+ and JSCore into the Vala compilation pipeline. Find the following lines of code in the file:

    hello_jscore_VALAFLAGS = \
    --pkg gtk+-3.0

  4. Remove it and replace it completely with the following lines:

    hello_jscore_VALAFLAGS = \
    --vapidir . --pkg gtk+-3.0 --pkg webkit-1.0 --pkg
    libsoup-2.4 --pkg javascriptcore

  5. Fill the hello_jscore.vala file inside the src folder with the following lines of code:

    using GLib;
    using Gtk;
    using WebKit;
    using JSCore;
    public class Main : WebView
    {
    public Main ()
    {
    load_html_string("<h1>Hello</h1>" +
    "<script>alert(HelloJSCore.hello())</
    script>","/");
    window_object_cleared.connect ((frame, context) => {
    setup_js_class ((JSCore.GlobalContext) context);
    });
    }
    public static JSCore.Value helloFromVala (Context ctx,
    JSCore.Object function,
    JSCore.Object thisObject,
    JSCore.Value[] arguments,
    out JSCore.Value exception) {
    exception = null;
    var text = new String.with_utf8_c_string ("Hello from
    JSCore");
    return new JSCore.Value.string (ctx, text);
    }
    static const JSCore.StaticFunction[] js_funcs = {
    { "hello", helloFromVala, PropertyAttribute.ReadOnly },
    { null, null, 0 }
    };
    static const ClassDefinition js_class = {
    0, // version
    ClassAttribute.None, // attribute
    "HelloJSCore", // className
    null, // parentClass
    null, // static values
    js_funcs, // static functions
    null, // initialize
    null, // finalize
    null, // hasProperty
    null, // getProperty
    null, // setProperty
    null, // deleteProperty
    null, // getPropertyNames
    null, // callAsFunction
    null, // callAsConstructor
    null, // hasInstance
    null // convertToType
    };
    void setup_js_class (GlobalContext context) {
    var theClass = new Class (js_class);
    var theObject = new JSCore.Object (context, theClass,
    context);
    var theGlobal = context.get_global_object ();
    var id = new String.with_utf8_c_string ("HelloJSCore");
    theGlobal.set_property (context, id, theObject,
    PropertyAttribute.None, null);
    }
    static int main (string[] args)
    {
    Gtk.init (ref args);
    var webView = new Main ();
    var window = new Gtk.Window();
    window.add(webView);
    window.show_all ();
    Gtk.main ();
    return 0;
    }
    }

  6. Copy the accompanied webkit-1.0.vapi and javascriptcore.vapi files into the src folder. The javascriptcore.vapi file is needed because some distributions do not have this .vapi file in their repositories.

  7. Run the application. The following output will be displayed:

What just happened?

The first thing we do is include the WebKit and JavaScriptCore namespaces. Note, in the following code snippet, that the JavaScriptCore namespace is abbreviated as JSCore:

using WebKit;
using JSCore;

In the Main function, we load HTML content into the WebView widget. We display a level 1 heading and then call the alert function. The alert function displays a string returned by the hello function inside the HelloJSCore class, as shown in the following code:

public Main ()
{
load_html_string("<h1>Hello</h1>" +
"<script>alert(HelloJSCore.hello())</script>","/");

In the preceding code snippet, we can see that the client-side JavaScript code is as follows:

alert(HelloJSCore.hello())

And we can also see that we call the hello function from the HelloJSCore class as a static function. It means that we don't instantiate the HelloJSCore object before calling the hello function.

In WebView, we initialize the class defined in the Vala class when we get the window_object_cleared signal. This signal is emitted whenever a page is cleared. The initialization is done in setup_js_class and this is also where we pass the JSCore global context into. The global context is where JSCore keeps the global variables and functions. It is accessible by every code.

window_object_cleared.connect ((frame, context) => {
setup_js_class ((JSCore.GlobalContext)
context);
});

The following snippet of code contains the function, which we want to expose to the clientside JavaScript. The function just returns a Hello from JSCore string message:

public static JSCore.Value helloFromVala (Context ctx,
JSCore.Object function,
JSCore.Object thisObject,
JSCore.Value[] arguments,
out JSCore.Value exception) {
exception = null;
var text = new String.with_utf8_c_string ("Hello from JSCore");
return new JSCore.Value.string (ctx, text);
}

Then we need to put a boilerplate code that is needed to expose the function and other members of the class. The first part of the code is the static function index. This is the mapping between the exposed function and the name of the function defined in the wrapper. In the following example, we map the hello function, which can be used in the client side, with the helloFromVala function defined in the code. The index is then ended with null to mark the end of the array:

static const JSCore.StaticFunction[] js_funcs = {
{ "hello", helloFromVala, PropertyAttribute.ReadOnly },
{ null, null, 0 }
};

The next part of the code is the class definition. It is about the structure that we have to fill, so that JSCore would know about the class. All of the fields are filled with null, except for those we want to make use of. In this example, we use the static function for the hello function. So we fill the static function field with js_funcs, which we defined in the preceding code snippet:

static const ClassDefinition js_class = {
0, // version
ClassAttribute.None, // attribute
"HelloJSCore", // className
null, // parentClass
null, // static values
js_funcs, // static functions
null, // initialize
null, // finalize
null, // hasProperty
null, // getProperty
null, // setProperty
null, // deleteProperty
null, // getPropertyNames
null, // callAsFunction
null, // callAsConstructor
null, // hasInstance
null // convertToType
};

After that, in the the setup_js_class function, we set up the class to be made available in the JSCore global context. First, we create JSCore.Class with the class definition structure we filled previously. Then, we create an object of the class, which is created in the global context. Last but not least, we assign the object with a string identifier, which is HelloJSCore. After executing the following code, we will be able to refer HelloJSCore on the client side:

void setup_js_class (GlobalContext context) {
var theClass = new Class (js_class);
var theObject = new JSCore.Object (context, theClass,
context);
var theGlobal = context.get_global_object ();
var id = new String.with_utf8_c_string ("HelloJSCore");
theGlobal.set_property (context, id, theObject,
PropertyAttribute.None, null);
}

GNOME 3 Application Development Beginner's Guide Step-by-step practical guide to get to grips with GNOME application development book and ebook.
Published: February 2013
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

Have a go hero – using a separate HTML file

In our previous section, we put the HTML5 code inside our Vala code. Whenever the code gets lengthier, it will get messier and will be impossible to maintain anymore. How about putting it outside the Vala code, say, in a dedicated file?

Don't continue to our next section just yet before finishing this task!

Time for action – connecting GNOME with client-side JavaScript

Imagine that we want to create a GNOME launcher. We display the available programs in our system with HTML5 and then will be able to launch them. Let us see how this can be done:

  1. Create an empty Vala project, without GtkBuilder and no license. Name it html5-launcher.

  2. Modify configure.ac to include WebKitGTK+ and Gee into the project. Find the following line in the file:

    PKG_CHECK_MODULES(HTML5_LAUNCHER, [gtk+-3.0 ])

  3. Remove it and replace it completely with the following line:

    PKG_CHECK_MODULES(HTML5_LAUNCHER, [gtk+-3.0 gee-1.0
    webkitgtk-3.0])

  4. Modify Makefile.am inside the src folder to include WebKitGKT+, Gio, and JavaScriptCore into the Vala compilation pipeline. Find the following lines of code in the file:

    html5_launcher_VALAFLAGS = \
    --pkg gtk+-3.0

  5. Remove it and replace it completely with the following lines:

    html5_launcher_VALAFLAGS = \
    --vapidir . --pkg gee-1.0 --pkg gio-unix-2.0 --pkg gtk+-
    3.0 --pkg webkit-1.0 --pkg libsoup-2.4 --pkg javascriptcore

  6. Fill the html5_launcher.vala file inside the src folder. For brevity, the shortened content of this file is shown in the following code just to show the important parts of the entire code:

    public class Main : WebView
    {
    public Main ()
    {
    load_uri("file:///%s/index.html".printf(Environment.get_
    current_dir()));
    window_object_cleared.connect ((frame, context) => {
    LauncherJSCore.setup_js_class ((JSCore.
    GlobalContext) context);
    });
    }
    }
    // our Vala class
    public class Launcher
    {
    IconTheme icon = null;
    int ICON_SIZE = 80;
    public HashMap<string,DesktopAppInfo> applications {
    get;
    private set;
    }
    public void launch(string name) {
    var app = applications.get(name);
    if (app != null) {
    app.launch(null, new AppLaunchContext());
    }
    }
    public Launcher ()
    {
    icon = IconTheme.get_default ();
    applications = new HashMap<string,DesktopAppInfo>();
    var dir = Dir.open("/usr/share/applications");
    if (dir != null) {
    string entry;
    while (true) {
    entry = dir.read_name();
    if (entry == null) {
    break;
    }
    var appInfo = new DesktopAppInfo.from_filename("/usr/
    share/applications/" + entry);
    if (appInfo != null) {
    applications.set(entry, appInfo);
    }
    }
    }
    }
    }
    // Our JSCore wrapper
    public class LauncherJSCore
    {
    public static JSCore.Object js_constructor (Context ctx,
    JSCore.Object constructor,
    JSCore.Value[] arguments,
    out JSCore.Value exception) {
    exception = null;
    var c = new Class (js_class);
    var newObject = new JSCore.Object (ctx, c, null);
    // register function launch
    var functionName = new String.with_utf8_c_string ("launch");
    var newFunction = new JSCore.Object.function_with_callback
    (ctx, functionName, js_launch);
    newObject.set_property (ctx, functionName, newFunction, 0,
    null);
    // register function getApplications
    functionName = new String.with_utf8_c_string
    ("getApplications");
    newFunction = new JSCore.Object.function_with_callback
    (ctx, functionName, js_getApplications);
    newObject.set_property (ctx, functionName, newFunction, 0,
    null);
    Launcher* launcher = new Launcher ();
    newObject.set_private (launcher);
    return newObject;
    }
    public static JSCore.Value js_getApplications(Context ctx,
    JSCore.Object function,
    JSCore.Object thisObject,
    JSCore.Value[] arguments,
    out JSCore.Value exception) {
    exception = null;
    var launcher = thisObject.get_private() as Launcher;
    StringBuilder json = new StringBuilder("[");
    if (launcher.applications != null) {
    foreach (var key in launcher.applications.keys) {
    var entry = launcher.applications.get(key) as
    DesktopAppInfo;
    var name = entry.get_display_name();
    if (entry.get_icon() != null) {
    var icon = launcher.getIconPath(entry.get_icon().to_
    string());
    json.append(("{desktop: '%s', name: '%s', icon: '%s'},").
    printf (key, name, icon));
    } else {
    json.append(("{desktop: '%s', name: '%s'},").printf (key,
    name));
    }
    }
    }
    if (json.str [json.len - 1] == ',') {
    json.erase (json.len - 1, 1); // Remove trailing comma
    }
    json.append("]");
    var text = new String.with_utf8_c_string (json.str);
    var obj = ctx.evaluate_script (text, null, null, 0, null);
    return obj;
    }
    public static JSCore.Value js_launch (Context ctx,
    JSCore.Object function,
    JSCore.Object thisObject,
    JSCore.Value[] arguments,
    out JSCore.Value exception) {
    exception = null;
    var launcher = thisObject.get_private() as Launcher;
    if (arguments.length == 1 && arguments[0].is_string(ctx)) {
    var parameter = arguments[0].to_string_copy (ctx, null);
    char buffer[1024];
    parameter.get_utf8_c_string (buffer, buffer.length - 1);
    launcher.launch((string) buffer);
    }
    return new JSCore.Value.undefined (ctx);
    }
    static const ClassDefinition js_class = {
    0, // version
    ClassAttribute.None, // attribute
    "Launcher", // className
    null, // parentClass
    null, // static values
    null, // static functions
    null, // initialize
    null, // finalize
    null, // hasProperty
    null, // getProperty
    null, // setProperty
    null, // deleteProperty
    null, // getPropertyNames
    null, // callAsFunction
    null, // callAsConstructor
    null, // hasInstance
    null // convertToType
    };
    public static void setup_js_class (GlobalContext context) {
    var theClass = new Class (js_class);
    var theConstructor = new JSCore.Object.constructor
    (context, theClass, js_constructor);
    var theGlobal = context.get_global_object ();
    var id = new String.with_utf8_c_string ("Launcher");
    theGlobal.set_property (context, id, theConstructor,
    PropertyAttribute.None, null);
    }

  7. Create an HTML5 file with the name of index.html and put it in the src folder. Fill it with the following lines:

    <!DOCTYPE HTML>
    <html>
    <head>
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.1/
    jquery.mobile-1.1.1.min.css" />
    <script src = "http://code.jquery.com/jquery-1.7.1.min.js"></script>
    <script src = "http://code.jquery.com/mobile/1.1.1/jquery.mobile-
    1.1.1.min.js"></script>
    </head>
    <body>
    <div data-role="page">
    <div data-role="header">
    <h1>My Launcher</h1>
    </div>
    <div data-role="content">
    <ul data-role="listview" data-theme="a">
    </ul>
    </div>
    </div>
    <script>
    $(document).ready(function() {
    var launcher = new Launcher();
    var apps = launcher.getApplications();
    if (apps != null) {
    for (var i = 0; i < apps.length; i ++) {
    var image = $("<img/>").addClass("ui-li-icon").attr("src",
    apps[i].icon);
    var link = $("<a/>").attr("href", "#")
    .text(apps[i].name)
    .attr("data-desktop", apps[i].desktop)
    .addClass("desktop-launcher")
    .append(image)
    var entry = $("<li/>")
    .append(link)
    $("[data-role=listview]").append(entry);
    }
    $("[data-role=listview]").listview('refresh')
    }
    $(".desktop-launcher").click(function() {
    var desktopFile = $(this).attr("data-desktop");
    if (desktopFile) {
    launcher.launch(desktopFile);
    }
    });
    });
    </script>
    </body>
    </html>

  8. Copy the accompanying webkit-1.0.vapi and javascriptcore.vapi files into the src folder.

  9. Build and run the application. We should see a launcher as shown in the following screenshot:

What just happened?

GNOME uses a freedesktop.org desktop system. In this system, an application is accompanied with at least one desktop file. This file has a .desktop extension and contains information about the application title, icon name, the command which the system must call in order to launch this application, and a set of translations of the application title in many languages when available. What we do in this example is get all the desktop files from the system and display the information contained in them in a menu. And when we click on one of the menu items, our application launches it. The desktop files usually reside in /usr/ share/applications. So in our simple example, we just get all the files from this folder. Let's see how we do this in detail now.

In this example, we use Gee to make use of the HashMap data structure. This means that we need to include the Gee namespace.

using Gee;

In order to make our Vala code cleaner, we load the HTML5 file from an external file. We also put the object, which needs to be used at the client side, in a separate class, and finally, we put the code needed to set up JSCore in another class. With this separation, our code is better and easy to maintain.

In the constructor of the Main class, we load the index.html file from the current folder. In real world implementations, the location of the HTML5 file must not be hardcoded and should be flexible. Using a hardcoded location of the file would result in deployment difficulties.

public Main ()
{
load_uri("file:///%s/index.html".printf(Environment.get_current_
dir()));

Then, we connect the LauncherJSCore.setup_js_class function in the window_object_cleared signal.

window_object_cleared.connect ((frame, context) => {
LauncherJSCore.setup_js_class ((JSCore.GlobalContext)
context);
});

Next, we define our Vala class. This class should not use any JSCore type system. We call our class Launcher.

public class Launcher

We have a data structure created with a HashMap type. It contains the mapping between the desktop filename and the object containing the desktop data structure. The desktop file is represented with the DesktopAppInfo object, which is provided by the Gio namespace.

public HashMap<string,DesktopAppInfo> applications {
get;
private set;
}

In the Launcher constructor, we initialize the icon object from IconTheme in order to translate the icon name obtained from the DesktopAppInfo object into an actual path in the filesystem. We need this because HTML can't display an icon just by its name, but we need to specify the full URI. After this, we initialize the HashMap object.

public Launcher ()
{
icon = IconTheme.get_default ();
applications = new HashMap<string,DesktopAppInfo>();

Then we populate the object with all the desktop files found in /usr/share/ applications. We first create a Dir object pointing to the folder mentioned previously, then iterate its read_name() function. When the function returns null, it means it no longer finds a file and we must exit the while loop. If we don't do this, we will end up in an infinite loop and our application would be not responsive. There is no other cure for this than killing the frozen application.

var dir = Dir.open("/usr/share/applications");
if (dir != null) {
string entry;
while (true) {
entry = dir.read_name();
if (entry == null) {
break;
}

For each filename we get from the read_name() function, we must create a DesktopAppInfo object of that file. We just instantiate the object by using the from_filename()constructor and pass the full path of the desktop file. If the file is not a desktop file, then the instantiation will fail and the value will be null. If the value is not null, we immediately put the object into the HashMap data structure.

var appInfo = new DesktopAppInfo.from_filename("/usr/share/
applications/" + entry);
if (appInfo != null) {
applications.set(entry, appInfo);
}

Then we prepare a function that does the actual translation of the icon name into the full path of the icon in the filesystem. We use the lookup_icon() function and try to find the icon in the current active theme. When we find it, we get the full path of the icon, otherwise we just use the original icon name.

public string getIconPath(string name) {
var i = icon.lookup_icon (name, ICON_SIZE, IconLookupFlags.
GENERIC_FALLBACK);
if (i != null) {
return i.get_filename();
} else {
return name;
}
}

At last, we have a function to launch an application. What we do first is to get the DesktopAppInfo object from the HashMap type and call the launch() function, which is supplied by DesktopAppInfo. For each launched application, we create a new AppLaunchContext object, which defines the environment setting of the application.

public void launch(string name) {
var app = applications.get(name);
if (app != null) {
app.launch(null, new AppLaunchContext());
}
}

Now, let's shift to our JSCore wrapper. The wrapper only contains static functions and by itself is not an object.

// Our JSCore wrapper
public class LauncherJSCore

The first part of the wrapper is the constructor. This is the constructor of our object whenever we call a new Launcher object in our client-side JavaScript. In our constructor, we first create a JSCore class based on the class definition we will see later:

public static JSCore.Object js_constructor (Context ctx,
JSCore.Object constructor,
JSCore.Value[] arguments,
out JSCore.Value exception) {
exception = null;
var c = new Class (js_class);
var newObject = new JSCore.Object (ctx, c, null);

Then, we register the functions that we want to expose to the client side. Here, we register the launch and getApplications functions. What we do here is to create a JSCore string containing the name of the function, then create a JSCore object mapped to the function, which we will wrap the Vala function into. In this case, we create a newFunction object from the js_launch function. Then we use the set_property() function to assign the newFunction function object with the functionName parameter. After this, our function called launch is recognized in the Launcher object. We need to do this for each function that we want to expose to the client-side JavaScript.

// register function helloFromVala
var functionName = new String.with_utf8_c_string ("launch");
var newFunction = new JSCore.Object.function_with_callback
(ctx, functionName, js_launch);
newObject.set_property (ctx, functionName, newFunction, 0,
null);

Finally, we create and keep the reference of our Launcher object and set it as private in the JSCore object. So, whenever we want to call the Vala function, we just get this object back from the private area and call it from there.

Launcher* launcher = new Launcher ();
newObject.set_private (launcher);
return newObject;
}

Let's see each of the wrapping functions we expose to the client-side script. First is the getApplications() function.

public static JSCore.Value js_getApplications(Context ctx,
JSCore.Object function,
JSCore.Object thisObject,
JSCore.Value[] arguments,
out JSCore.Value exception) {

We set the exception value to null stating that we don't generate a JavaScript exception.

exception = null;

As discussed earlier, we can get the Vala object by getting it from the private area with the get_private() function and immediately cast it into Launcher. Also, we prepare a StringBuilder object to keep a JSON representation of the application list. The string is initialized with [ because we will use JSON as the array of applications. We will concatenate all JSON representations of the desktop files inside this string.

var launcher = thisObject.get_private() as Launcher;
StringBuilder json = new StringBuilder("[");

At this point, we must check whether the list's value is null or not. If not, we iterate by getting all the keys from the HashMap data structure. As we already know, the key contains the name of the desktop file and the value contains the DesktopAppInfo object. So, by using the get() function in the HashMap data structure, we can get the DesktopAppInfo object.

if (launcher.applications != null) {
foreach (var key in launcher.applications.keys) {
var entry = launcher.applications.get(key) as DesktopAppInfo;

After we get the DesktopAppInfo object, which we put into the entry variable, we get the name of the application with the get_display_name() function, and get the icon with the get_icon() function. Whenever we can get the actual path of the icon in the filesystem, we create a JSON similar to the following:

{desktop: '%s', name: '%s', icon: '%s'}

And if we cannot get the actual path of the icon in the filesystem, we create the JSON as shown here:

{desktop: '%s', name: '%s'}

We do this until all the desktop files are handled and put into the JSON string.

var name = entry.get_display_name();
if (entry.get_icon() != null) {
var icon = launcher.getIconPath(entry.get_icon().to_string());
json.append(("{desktop: '%s', name: '%s', icon: '%s'},").
printf (key, name, icon));
} else {
json.append(("{desktop: '%s', name: '%s'},").printf (key,
name));

At the end of the loop, we must remove any trailing commas, otherwise the JSCore will fail during the conversion of our JSON string into a JSCore object, which we will return in this JSCore function.

if (json.str [json.len - 1] == ',') {
json.erase (json.len - 1, 1); // Remove trailing comma
}

We terminate the string with ], which marks the end of the array.

json.append("]");

Afterwards, we convert the JSON string into a JSCore string. Then convert the JSON string into a JSCore object with the evaluate_script() function. Finally, we return that object.

var text = new String.with_utf8_c_string (json.str);
var obj = ctx.evaluate_script (text, null, null, 0, null);
return obj;

The next one is the launch() function. In this function, we have an arguments parameter in our JavaScript code, which can simply be obtained from the arguments parameter in this JSCore function. If the arguments parameter's length property is equal to 1 and the content of the arguments variable is a string, then we can say that this is a valid call to this function. In this case, we just call the launch() function in the launcher object and pass the contents of the arguments variable to the function. But before doing so, we need to convert the string coming from the arguments variable, that is, the JSCore string, into the C string, which is recognized by the Vala function by using the get_utf8_c_string() function.

public static JSCore.Value js_launch (Context ctx,
JSCore.Object function,
JSCore.Object thisObject,
JSCore.Value[] arguments,
out JSCore.Value exception) {
exception = null;
var launcher = thisObject.get_private() as Launcher;
if (arguments.length == 1 && arguments[0].is_string(ctx)) {
var parameter = arguments[0].to_string_copy (ctx, null);
char buffer[1024];
parameter.get_utf8_c_string (buffer, buffer.length - 1);
launcher.launch((string) buffer);
}

In case the call is invalid, then we just return an undefined value to the caller.

return new JSCore.Value.undefined (ctx);

And the following code contains our JSCore class definition structure. We fill everything with null, except the name of the class with the Launcher value and the constructor field pointing to the js_constructor function, which we discussed earlier.

static const ClassDefinition js_class = {
0, // version
ClassAttribute.None, // attribute
"Launcher", // className
null, // parentClass
null, // static values
null, // static functions
null, // initialize
null, // finalize
null, // hasProperty
null, // getProperty
null, // setProperty
null, // deleteProperty
null, // getPropertyNames
null, // callAsFunction
null, // callAsConstructor
null, // hasInstance
null // convertToType
};

At last, the following code snippet shows our setup_js_class function, which is called from the WebView widget explained earlier. Here, we create an JSCore object constructor that points to our js_constructor function. Then, we connect the constructor object with the Launcher name in the global context. So, whenever the code at the client side mentions new Launcher(), the constructor will be called.

public static void setup_js_class (GlobalContext context) {
var theClass = new Class (js_class);
var theConstructor = new JSCore.Object.constructor (context,
theClass, js_constructor);
var theGlobal = context.get_global_object ();
var id = new String.with_utf8_c_string ("Launcher");
theGlobal.set_property (context, id, theConstructor,
PropertyAttribute.None, null);
}

We will not go too deep into the index.html file, but just look at the following lines of code:

var launcher = new Launcher();
var apps = launcher.getApplications();

These are the lines that actually call our wrapped Vala code. Nice, isn't it?

Actually, this HTML-based launcher was really implemented for GNOME. Manokwari. This is a desktop shell for GNOME 3 and is developed with the techniques that we just discussed. Check it out at http:// manokwari.blankonlinux.or.id and study the source code.

Have a go hero – where to put index.html

It would be nice if we can put the index.html file in a different folder that does not contain the html5-launcher executable file, for example, if you not only have HTML files, but also CSS, image files, and other files as well. What we can do here is to put a string constant where we can put these assets into and deploy them nicely in the real environment. During development, we can use src/ or some other folder, but while deploying them, we can use a different folder, for example, /usr/share/html5-launcher/.

Summary

In this article, we learned how to create an application written in HTML5 that can also communicate with the GNOME platform underneath by utilizing JSCore. To display an HTML5 page, we use the WebKit's WebView widget. When there is no communication with the main process, we can display any HTML5 page only by using this widget.

Then we added a business process in our Vala code. We connected the objects created in the Vala code with the client-side JavaScript code, which is defined in the HTML5 page. Unfortunately, we need to use Boilerplate code to start with, and fill the code with our implementation. We need to wrap every function that we want to expose to the client-side code with the JSCore function.

With this approach, we can create a full HTML5 application without a web server and do the business process inside our Vala code. This does not stop here as it is only limited by your imagination. For example, if you find that a certain HTML5 feature is not supported by GtkWebKit, you can create your own extension with the same API using Vala.

Resources for Article :


Further resources on this subject:


GNOME 3 Application Development Beginner's Guide Step-by-step practical guide to get to grips with GNOME application development book and ebook.
Published: February 2013
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

About the Author :


Mohammad Anwari

Mohammad Anwari is a software hacker from Indonesia with more than 13 years of experience in software development. He has been working with Linux-based systems, applications in GNOME, and Qt platforms. The projects he has worked on range from the development of constrained devices and desktop applications, to high traffic server systems and applications.

He worked for his own startup company during the dotcom era before moving to Finland to work for Nokia/MeeGo. Now he's back in Indonesia, regaining his entrepreneurship by establishing a new startup company that focuses on Node.js and Linux-based projects. In his free time, he serves as an executive director for BlankOn, one of the biggest open source projects in Indonesia.

In the past, he has published a couple of books on Linux in the Indonesian language.

Books From Packt


Getting Started with Talend Open Studio for Data Integration
Getting Started with Talend Open Studio for Data Integration

LiveCode Mobile Development Beginner's Guide
LiveCode Mobile Development Beginner's Guide

 Python Multimediae
JBoss Tools 3 Developers Guide

VMware View 5 Desktop Virtualization Solutions
VMware View 5 Desktop Virtualization Solutions

Instant HTML5 Geolocation How-to [Instant]
Instant HTML5 Geolocation How-to [Instant]

HTML5 Mobile Development Cookbook
HTML5 Mobile Development Cookbook

WebGL Beginner's Guide
WebGL Beginner's Guide

PhoneGap Beginner’s Guide - Second Edition
PhoneGap Beginner’s Guide - Second Edition


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
k
x
V
V
f
z
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software