Clutter Wiki

Views
From ClutterProject
Jump to: navigation, search

Note: this tutorial was originally intended for MeeGo application developers. However, since JavaScript/Clutter/Mx are no longer the main focus for 3rd party developers, I moved the tutorial here instead. Some of the screenshots may not match recent MeeGo themes, but the code worked last time I tried it (2010-06-22).

Contents

Introduction

This document describes how to build Clutter/Mx desktop applications for MeeGo using JavaScript. Not applications running inside a browser, using JavaScript; rather, applications running natively on the desktop, written in JavaScript and calling into C libraries.

The things I wanted to find out generally about desktop JavaScript:

  • Which JavaScript engine provides the best developer experience?
  • How do you get a JavaScript engine working with bleeding edge Clutter/Mx?

And more specifically, how to go about writing a simple calculator application using it:

  • What does a MeeGo JavaScript project look like?
  • How do I load C libraries/my own JavaScript libraries?
  • How do callbacks work?
  • Can I do subclasses of C (GObject) types?
  • Can I run the application on a real MeeGo netbook?

Note that this document is going to evolve to be more thorough, but for now the intention is to give a broad feel for how suitable JavaScript is for MeeGo development, in its current state. I know there are massive holes (like how to submit the app to AppUp or Ovi stores, how to internationalise, how to package) but thought it's better to show something in progress than nothing at all.

It's also not going to be a full Clutter or Mx tutorial, so if you're not familiar with using those libraries, you might want to look at this Clutter tutorial too.

File:Docbook-Warning.pngTechnology preview alert!

The stuff I'm writing about here is highly experimental. Stuff might blow up or something. Use with care.

History

In 2009, I wrote a blog entry about experiments with JavaScript. At the time, I was using gjs, patched manually to include a print function. I wrote a tic-tac-toe game using Clutter/Mx as the testbed for these experiments: here are some files with the code in.

Since then, I've done some more experiments with JavaScript. You can see some of them in the moblin-sdk-examples part of the old Moblin git repository: http://git.moblin.org/cgit.cgi/moblin-sdk-examples/ (particularly in the examples/mx directory).

This work has culminated with this document, where I consolidate this work. At the same time, while writing this document, I've confirmed as far as possible that everything in place in the MeeGo stack and surrounding "ecosystem" to support JavaScript development.

Which JavaScript engine provides the best developer experience?

Assuming you want to write desktop JavaScript applications (not web browser applications), it's good to be able to bind to C libraries. This is why Gjs and Seed exist: to enable developers to use the huge pile of GNOME libraries from JavaScript. While V8 might be great, running desktop applications isn't really it's raison d'être. For the moment, then, your choices (should you want to bind to GObject libraries) boil down to:

Implementation Used in... Pros Cons

Gjs (http://live.gnome.org/Gjs/)

Litl webbook (i.e. production-ready)

GnomeShell (GNOME 3 user interface)

Binds to large set of GNOME/MeeGo libraries

Packaged for Fedora (though an old version of Gjs when I tried it)

Supports the let statement, which makes variable scoping sane

Practically zero documentation

Seed (http://live.gnome.org/Seed/)

MeeGo :) (well, the runtime is available there)

Tiny amount of documentation (better than nothing)

More developer-friendly: for example, bindings for C libraries which don't use GObject (SQLite, readline)

More examples in an easy-to-digest format (http://git.gnome.org/browse/seed-examples)

Packaged for MeeGo

Supports subclassing of GObject classes

Binds to large set of GNOME/MeeGo libraries - hang on, that sounds familiar...

Not packaged for Fedora

Note that I've expended zero effort working out if distros other than Fedora package these implementations. It's almost irrelevant in some respects, as they are so bleeding edge you may well need to build them yourself (more later...).

The important thing to take away from this is that both Gjs and Seed bind to the underlying C libraries through GObject introspection metadata. (Seed perhaps has the edge, because it also binds to other libraries which don't use GObject.)

A caveat: I don't (currently) contribute to any of the JavaScript engines: I'm just trying to work out how to use them. So how they do their job is not my primary concern, so long as they work.

Another caveat: comparative performance may be an issue I'm blithely ignoring here. I've not been able to find much comparing JavaScript engines running outside the browser, but in-browser testing indicates there's not a huge gap between Spidermonkey (the basis for Gjs) and WebKit (the base for Seed): e.g. see John Resig's performance rundown. But given these technologies are both still in the "experimental" phase anyway, I'd say we could worry about that later.

Summary: I decided to go with Seed, mainly because it's better documented and has more code examples.

How do you get Seed?

If Seed isn't packaged for your distribution, one solution is to build your own. You only need to do this if you can't install Seed, GObject introspection, and GIR from your package manager, or if you want really bleeding edge versions of everything.

File:Docbook-Tip.pngA netbook or chroot might be enough...

It's worth mentioning that you might not need to set up a separate dev machine. Dependening on your needs, JavaScript makes it much more feasible to develop on your netbook, as you may not need to compile anything. You only need a text editor. Given that Seed is part of the MeeGo distribution, along with bindings to the core libraries, you may be able to do your development work with just a netbook, in which case you won't need to compile Seed at all.

Another alternative would be to use the MeeGo SDK chroot and install seed into it: see http://wiki.meego.com/Getting_started_with_the_MeeGo_SDK_for_Linux for details.

As I typically have multiple versions of Clutter/Mx installed, and Seed depends on a fairly new version of GLib which I don't have, I decided to use jhbuild to build my Seed environment. This lets you compile your target applications/libraries in an isolated environment, without affecting your operating system's core libraries (like GLib).

Installing requirements on Ubuntu 9.10

I used Ubuntu 9.10 to do a fresh build of a whole Seed environment, to make sure I captured all the steps required.

ell@ub-vm:~$ sudo apt-get install git-core

Next, install all the dependencies required to build GNOME (as suggested in the jhbuild install instructions); I've also included a few others required by GNOME and Clutter, which are difficult to build (like xulrunner) or very low-level libraries:

ell@ub-vm:~/jhbuild/jhbuild$ sudo apt-get install gnome-common build-essential doxygen subversion \
automake1.4 automake1.7 cvs git-core docbook docbook-utils docbook-xsl flex bison texinfo python2.5-dev \
lynx mono-gmcs libtiff4-dev libxtst-dev libgdbm-dev libxml-simple-perl libelfg0-dev libcups2-dev \
libldap2-dev libexchange-storage1.2-dev libxmu-dev libpam0g-dev libgpgme11-dev libfreetype6-dev \
libpng12-dev libxrender-dev libxi-dev libexpat1-dev libbz2-dev libxcursor-dev guile-1.8-dev libxdamage-dev \
libxcomposite-dev libmono-cairo2.0-cil xnest libxft-dev libloudmouth1-0 libloudmouth1-dev libxss-dev \
libxkbfile-dev gtk-doc-tools libjasper-dev libnl-dev ppp-dev libdv4-dev uuid-dev libpcre3-dev \
libpurple-dev libcurl4-gnutls-dev libffi-dev liboil0.3-dev python-dev mesa-common-dev \
libgl1-mesa-dev gperf libicu-dev libenchant-dev libmpfr-dev libsoup-gnome2.4-dev \
libnm-glib xulrunner-1.9.1-dev

This is not an exact science, so it may be that in your case you may need other libraries for this to work. You can either install them to your base system, or add them to the jhbuild-meego moduleset (and contribute them back to the project).

Installing requirements on Fedora 12

To install all the requirements for a Fedora system, these are the packages:

ell@ub-vm:~$ sudo yum groupinstall "Development Tools"
ell@ub-vm:~$ sudo yum gnome-common doxygen subversion automake14 automake17 cvs git-core  \
publican docbook-utils docbook-xsl flex bison texinfo python-devel lynx mono-devel libtiff-devel \
libXtst-devel gdbm-devel perl-XML-LibXML elfutils-libelf-devel cups-devel openldap-devel \
evolution-data-server libXmu-devel pam-devel gpgme-devel freetype-devel libpng-devel libXrender-devel \
libXi-devel expat-devel bzip2-devel libXcursor-devel guile-devel libXdamage-devel libXcomposite-devel \ 
mono-core xorg-x11-server-Xnest libXft-devel loudmouth loudmouth-devel libXScrnSaver-devel libxkbfile-devel \
gtk-doc jasper-devel libnl-devel ppp-devel libdv-devel uuid-devel pcre-devel libpurple-devel libcurl-devel \
libffi-devel liboil-devel python-devel mesa-libGL-devel mesa-libGLw-devel gperf libicu-devel enchant-devel \
mpfr-devel libsoup-devel NetworkManager-glib xulrunner-devel

Again, this is not an exact science, so it may be that in your case you need more libraries on top of these. You can either install them to your base system, or add them to the jhbuild-meego moduleset (and contribute them back to the project).

Installing jhbuild

Install jhbuild from git:

ell@ub-vm:~$ mkdir jhbuild
ell@ub-vm:~$ cd jhbuild/
ell@ub-vm:~/jhbuild$ git clone git://git.gnome.org/jhbuild
ell@ub-vm:~/jhbuild$ cd jhbuild/

To build and install jhbuild itself:

ell@ub-vm:~/jhbuild/jhbuild$ ./autogen
ell@ub-vm:~/jhbuild/jhbuild$ make install

This installs jhbuild to the ~/.local directory. Add that to your path:

ell@ub-vm:~/jhbuild/jhbuild$ echo 'export PATH=~/.local/bin:~/bin:$PATH' >> ~/.bashrc
ell@ub-vm:~/jhbuild/jhbuild$ source ~/.bashrc

Test jhbuild is on your path:

ell@ub-vm:~/jhbuild/jhbuild$ jhbuild
jhbuild: could not load config file, /home/ell/.jhbuildrc is missing

(This is the expected result.)

Installing a MeeGo JavaScript development environment

To setup the MeeGo environment, use the MeeGo jhbuild for netbook modulesets:

ell@ub-vm:~/jhbuild$ git clone git://gitorious.org/meego-netbook-ux/meego-jhbuild-netbook.git
ell@ub-vm:~/jhbuild$ cd meego-jhbuild-netbook
ell@ub-vm:~/jhbuild/meego-jhbuild-netbook$ make install
install -d -m 755 '/home/ell/bin'
install -m 755 build/jhbuild-meego.sh '/home/ell/bin/jhbuild-meego'
install -m 644 jhbuildrc-meego '/home/ell/.jhbuildrc-meego'
if test -f '/home/ell/.jhbuildrc-meego-custom' ; then \
  echo "*** Custom jhbuild config already exists - leaving well alone";\
	else install -m 644 jhbuildrc-meego-custom '/home/ell/.jhbuildrc-meego-custom' ;\
	fi
ell@ub-vm:~/jhbuild/meego-jhbuild-netbook$

This installs MeeGo jhbuild to your home directory, placing a jhbuild-meego script in ~/bin (which we cunningly added to the PATH environment variable already when we built jhbuild). This script just wraps jhbuild and points at the MeeGo jhbuild configuration files. See the README for meego-jhbuild for more information.

Next, bootstrap jhbuild to ensure you have all the required build tools installed:

ell@ub-vm:~/jhbuild$ jhbuild-meego bootstrap

This will download and install any build tools missing from your jhbuild environment.

Then run the MeeGo JavaScript SDK build:

ell@ub-vm:~/jhbuild$ jhbuild-meego build meta-meego-js

This runs jhbuild to build and locally install the Seed/Clutter/Mx moduleset. Note that it does this in a SAFE environment, without overriding your system libraries. It can also take a long time (especially WebKit).

Once your Seed environment is built, you can use jhbuild to give you a shell loaded with that environment (paths set up correctly etc.):

ell@ub-vm:~/jhbuild$ jhbuild-meego shell

You should then be able to check that Seed is working using it's REPL prompt:

ell@ub-vm:~/jhbuild$ seed
> 1+1
2
> print("Hello");
Hello

If you get this far, you're in a position to start JavaScript development for MeeGo.

What does a MeeGo JavaScript project look like?

JavaScript isn't a widespread development language for Linux desktop apps. There are a few example applications, however; perhaps the most informative is Lights Off, a GNOME game written in JavaScript, which can act as a good template for other applications.

However, Lights Off uses autotools for the project build and has a typical autotools project structure. I feel that using autotools to build a first JavaScript project is overkill: you only need a tiny part of their power, and don't need to compile anything. (Later, when you're dealing with internationalisation, desktop entry files, and the other paraphernalia of the GNOME desktop, they might become necessary.)

Anyway, for now, the project structure can be fairly simple and flat. It will initially consist of:

  • main.js, the JavaScript file which runs the application (it will contain the equivalent to C's main function.)
  • calculator.js, containing the Calculator class we're going to make. This will be loaded into main.js.
  • jscalculator, a shell script to execute Seed with the bootstrap file.

Create a new project directory, jscalculator. Then create two files inside it:

Example 1. jscalculator

#!/bin/bash
WD=`dirname $0`
seed $WD/main.js


This is a simple wrapper around the seed, which passes it the main.js file.

The jscalculator file will need to be executable (chmod +x jscalculator).

Example 2. main.js

var resource_directory = __script_path__  //  File:Docbook-Callout-1.png
print("Resource directory is " + resource_directory);

__script_path__ is a builtin Seed variable which contains the full path to the directory containing the current script file.


See this section for instructions on running your application. Once you run it, you should see some screen output:

$ jhbuild-meego run /home/ell/calculator/jscalculator
Resource directory is /home/ell/calculator

Running a JavaScript application with Seed

To run a JavaScript application, you'll need to be inside a Seed environment:

$ jhbuild-meego shell
$ ./jscalculator

Or run the script inside the jhbuild environment:

$ jhbuild-meego run ./jscalculator

  • Using Seed directly. Just invoke the jscalculator script directly on the command line:
$ ./jscalculator

Providing seed is on the path, it should work.

File:Docbook-Tip.pngInstalling Seed on MeeGo

Last time I looked, Seed was not part of the default disk image for MeeGo netbook. To install it yourself, use yum from the command line on the netbook:

$ yum install seed

This should also install gir-repository, which contains the JavaScript bindings for various MeeGo libraries.

Using Clutter from JavaScript

Next, we'll make the application do something useful, by displaying a Clutter window. However, we'll do this by creating a JavaScript class which encapsulates the calculator, and invoking it from main.js. Here, we'll see how to load both system libraries and your own JavaScript files.

Create a new file, calculator.js, in the same directory as main.js:

Example 3. calculator.js

Clutter = imports.gi.Clutter;  //  File:Docbook-Callout-1.png

Calculator = function(argv, resource_directory) {  //  File:Docbook-Callout-2.png
  Clutter.init(argv.length, argv);  //  File:Docbook-Callout-3.png

  // get an instance of Clutter.Stage (main window for the app)
  var stage = Clutter.Stage.get_default();
  stage.set_title('Calculator');

  stage.show_all();
  Clutter.main();
  stage.destroy();
};

imports.gi.Clutter loads the Clutter library classes and methods, making them available inside the application. Note that the import is assigned to the variable Clutter, which acts as a namespace for its classes and methods.

Defines a pure JavaScript class using the standard approach. Any code inside the class definition gets run whenever the class is instantiated (as per usual JavaScript). If you want to extend a JavaScript class which binds to a GObject C class (like Mx.Widget, for example), you have to use a special syntax: see this section for more details.

Initialise Clutter. The constructor for the class accepts an array of command line arguments; this is forwarded onto Clutter, so it's possible to use Clutter's command-line arguments when invoking the application (e.g. to turn on Clutter debugging).


Next, load calculator.js inside main.js and create an instance of the Calculator class:

Example 4. main.js

var resource_directory = __script_path__;
imports.searchPath.push(resource_directory);   //  File:Docbook-Callout-1.png

Calc = imports['calculator.js'];  //  File:Docbook-Callout-2.png

new Calc.Calculator(Seed.argv, resource_directory);   //  File:Docbook-Callout-3.png

imports.searchPath is an array of paths Seed will search for JavaScript files. Here we push the resource directory onto the end of it, so that we can load our own JavaScript files from this directory.

This is a "magic" method which tries to load a file called calculator.js on the imports.searchPath. We assign the loaded script to the Calc namespace: this is good practice, as it prevents code from different libraries overwriting each other's class and function definitions. If both our library and someone else's both provided a Calculator class, creating an instance of Calculator would be ambiguous (our Calculator or theirs?).

This creates an instance of the Calculator class, from our Calc namespace. Note that we pass the command line arguments (Seed.argv) and the resource_directory (where the main.js file resides) to the constructor. Knowing the latter will be necessary to load other resources from the filesystem, like stylesheets.

Note that Seed.argv is a zero-indexed array containing the command line arguments; Seed.argv[0] = 'seed' and Seed.argv[1] = '<path to directory>/main.js'.


You can now run this from your Seed environment (either on your dev machine or on a netbook). You should see this:

File:MeeGo-js-dev-basic-clutter-app.png

Naïve calculator with callbacks

The next step is to plug in some (very crude) calculator code. The aims here are to:

  • Make it the right size for a calculator (that window is too big...)
  • Add a display to show the calculation
  • Add some buttons
  • Wire the buttons up to the display
  • Calculate!

MeeGo UI Toolkit (aka Mx) is what's behind the netbook reference UI, so that's what we'll use here. It integrates very nicely with (and is founded on) Clutter.

Example 5. calculator.js

Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;  //  File:Docbook-Callout-1.png
Lang = imports.lang;  //  File:Docbook-Callout-2.png

Calculator = function(argv, resource_directory) {
  Clutter.init(argv.length, argv);

  var keys_per_row = 5;

  // dimensions for the app + widgets (in pixels)
  var key_side = 40;
  var width = keys_per_row * key_side;
  var height = 200;

  var stage = Clutter.Stage.get_default();
  stage.set_title('Calculator');

  // set the size of the main window
  stage.set_size(width, height);

  // layouts  File:Docbook-Callout-3.png
  var display_layout = new Mx.BoxLayout();

  var keys_layout = new Mx.Grid();
  keys_layout.set_max_stride(keys_per_row);

  var main_layout = new Mx.BoxLayout();
  main_layout.set_orientation(Mx.Orientation.VERTICAL);

  main_layout.add_actor(display_layout);
  main_layout.add_actor(keys_layout);

  // display  File:Docbook-Callout-4.png
  this.display = new Mx.Label();
  display_layout.add_actor(this.display);

  var key;

  // map from a key label to the character it appends to display
  var key_map = {};

  // number keys
  for (var i = 0; i <= 9; i++) {
    key_map[i] = "" + i;  //  File:Docbook-Callout-5.png
  }

  // operator keys
  var ops = {'.':'.','+':'+', '-':'-', '×':'*', '÷':'/'};
  for (var op in ops) {
    key_map[op] = ops[op];
  }

  // create and add keys   File:Docbook-Callout-6.png
  for (var label in key_map) {
    key = new Mx.Button({label: label});
    key.set_size(key_side, key_side);
    key.signal.clicked.connect(Lang.bind(this, this.append), key_map[label]);  //  File:Docbook-Callout-7.png
    keys_layout.add_actor(key);
  }

  // clear key
  key = new Mx.Button({label:'C'});
  key.set_size(key_side, key_side);
  key.signal.clicked.connect(Lang.bind(this, this.reset));
  keys_layout.add_actor(key);

  // equals key
  key = new Mx.Button({label:'='});
  key.set_size(key_side, key_side);
  key.signal.clicked.connect(Lang.bind(this, this.equals));
  keys_layout.add_actor(key);

  // reset display
  this.reset();

  // add layout to stage
  stage.add_actor(main_layout);

  stage.show_all();
  Clutter.main();
  stage.destroy();
};

Calculator.prototype.append = function(clicked, character) {
  var current = this.display.get_text();
  if (current == '0') {
    current = ;
  }
  this.display.set_text(current + character);
};


Calculator.prototype.reset = function() {  //  File:Docbook-Callout-8.png
  this.display.set_text('0');
};

Calculator.prototype.equals = function() {
  var result = eval(this.display.get_text());
  this.display.set_text(result);
};


Imports the MeeGo UI Toolkit classes into the Mx namespace.

Import the lang functions which enable binding of this in the context of closures (see below).

Add layouts to hold the widgets: main_layout, which lays out its contained widgets down the screen; display_layout, a simple box layout for the display; and keys_layout, a grid to hold the calculator "keys". See the Mx API docs for more details.

Use an Mx Label to show the calculation and result.

Although JavaScript is quite forgiving about weak typing, some of the underlying libraries are less so. So it's best to be explicit about the types of data in arrays and hashes. As we're going to use the values in this hash as labels on Mx buttons, it's best to explicitly "cast" them to the right type as they're added to the hash. Here, because we're adding numeric values, we convert them explicitly to strings.

Notice how the constructor for a GObject class like Mx Button can be passed an associative array of key:value pairs. These set properties on the internal GObject C class instance when it is constructed. You can normally work out the available properties from the GIR definitions (see this section for more about using JavaScript APIs). See this page about how Seed maps to C for more information (NB this link may be slightly out of date and/or volatile).

A handler can be attached to a GObject signal using the syntax shown here. Note the object.signal.signalname syntax. The first argument passed to this method is the callback method to use (in this case, it's actually a closure with "this" bound to the current object); the second argument is any object (basically equivalent to the userdata passed to GObject callbacks). See the section on variable binding in callbacks for more details about how this works.

Each callback will be passed a reference to the object which generated the signal. However, there's no need for the callback function to specify include this parameter in the function signature if it's not going to be used.

Here's what it looks like running on a MeeGo netbook:

File:MeeGo-js-dev-calculator-with-keys.png

Working out what's in the JavaScript APIs

To use the JavaScript bindings for MeeGo's libraries, you have to know what's in the bindings. There are two approaches you can take:

  1. Use the introspection metadata directly. Human-readable (in XML) versions of the bindings are available in the GObject Introspection Repository metadata; if you used jhbuild, you can find them in the ~/meego/install/share/gir-1.0/ directory. This is a bit of a tricky process, and works best if you know the name of the C function you want to get the JavaScript class/method for; it's not very good for getting an overview of the available classes and what they're for.
  2. Read API docs generated from the JavaScript bindings. A set of API documentation is maintained at http://devel.akbkhome.com/seed/index.shtml. This provides a much better mechanism for navigating the APIs, but may not have documentation for all of the libraries in MeeGo (e.g. librest and Mx are missing) and/or may cover the wrong library versions. To get a more complete set, you can use clone your own documentation generator from http://git.gnome.org/browse/introspection-doc-generator/. (You will need to use Seed to run it yourself.)

Variable binding in callbacks

When binding signals to callback methods, I used to find myself passing any data I wanted to act on into the callback method. This is because this pattern didn't seem to work for me:

Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;

TestClass = function() {
  this.count = 0;

  Clutter.init(0, null);
  var stage = Clutter.Stage.get_default();

  var button = new Mx.Button({label: 'Click me'});
  button.signal.clicked.connect(this.increment);
  stage.add_actor(button);

  stage.show();
  Clutter.main();
  stage.destroy();
};

TestClass.prototype.increment = function() {
  this.count++;
  print(this.count);
};

new TestClass();

Clicking on the Click me button produced the output nan, because this.count wasn't visible inside the callback method and wasn't getting incremented.

Compare a pure JavaScript class which works in a similar way:

TestClass = function() {
  this.count = 0;
};

TestClass.prototype.increment = function() {
  this.count++;
  print(this.count);
};

var tc = new TestClass();
tc.increment();
tc.increment();
tc.increment();

Running under Seed, this produces the output you'd expect, i.e. this.count is visible to the increment method and you see:

1
2
3

However, there is a way to make this work in Seed. You can use the bind method to bind an object to the variable this in the context of a closure:

Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;

// need to import the Lang namespace first
Lang = imports.lang;

TestClass = function() {
  this.count = 0;

  Clutter.init(0, null);
  var stage = Clutter.Stage.get_default();

  var button = new Mx.Button({label: 'Click me'});

  /*
   * generate a closure from the increment method,
   * where the current TestClass instance is bound to "this"
   * in the context of the closure
   */
  button.signal.clicked.connect(Lang.bind(this, this.increment));

  stage.add_actor(button);

  stage.show();
  Clutter.main();
  stage.destroy();
};

TestClass.prototype.increment = function() {
  this.count++;
  print(this.count);
};

new TestClass();

Clicking on the Click me button now gives the expected output. This makes code much tidier and more natural. (NB Gjs provides the same mechanism for binding this.)

Creating a calculator display widget

The application currently manipulates an Mx.Label instance to show the calculation: to begin with, the text is initialised to "0"; characters are appended to the label as buttons are pressed; pressing the = button performs the calculation and displays it; and pressing C clears the display.

However, it's clear there are some common display operations here, which could be encapsulated with the label to create a reusable "calculator display" widget:

  • Reset the display to "0"
  • Append the value of the pressed button to the display (up to some maximum string length)
  • Display a result
  • Display an error message (e.g. if the calculation is invalid) and lock until cleared with the C button

Below is an example of how to subclass . The mechanism for subclassing it uses is explained in this section.

Example 6. Adding a calculator display widget

Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;
GObject = imports.gi.GObject;  //  File:Docbook-Callout-1.png
Lang = imports.lang;

CalculatorDisplay = new GType({
  parent: Mx.Label.type,  //  File:Docbook-Callout-2.png
  name: "CalculatorDisplay",

  properties: [  //  File:Docbook-Callout-3.png
    {
      name: "size",
      type: GObject.TYPE_INT,
      default_value: 18,
      minimum_value: 10,
      maximum_value: 25
    }
  ],

  init: function() {  //  File:Docbook-Callout-4.png
    this.locked = false;
    this.reset();
  },

  class_init: function(klass, proto) {  //  File:Docbook-Callout-5.png

    proto.reset = function() {
      this.set_text('0');
      this.locked = false;
    };

    proto.error = function() {
      this.set_text('ERROR');
      this.locked = true;
    };

    // returns true if char_to_append was appended, false otherwise
    proto.append = function(char_to_append) {
      if (this.locked) {
        return false;
      }

      var current_text = this.get_text();

      // before appending, clean up the 0 if that's all we've got
      if (current_text == '0') {
        current_text = ;
      }

      if (current_text.length >= this.size) {  //  File:Docbook-Callout-6.png
        this.locked = true;
      }
      else {
        this.set_text(current_text + char_to_append);
      }

      return !this.locked;
    };
  }
});

Calculator = function(argv, resource_directory) {
  Clutter.init(argv.length, argv);

  // dimensions for the app + widgets
  var keys_per_row = 5;
  var key_side = 40;
  var width = keys_per_row * key_side;
  var height = 200;

  var stage = Clutter.Stage.get_default();
  stage.set_title('Calculator');

  // set the size of the main window
  stage.set_size(width, height);

  // layouts
  var display_layout = new Mx.BoxLayout();

  var keys_layout = new Mx.Grid();
  keys_layout.set_max_stride(keys_per_row);

  var main_layout = new Mx.BoxLayout();
  main_layout.set_orientation(Mx.Orientation.VERTICAL);

  main_layout.add_actor(display_layout);
  main_layout.add_actor(keys_layout);

  // display
  this.display = new CalculatorDisplay({size:20});  //  File:Docbook-Callout-7.png
  display_layout.add_actor(this.display);

  var key;

  // map from a key label to the character it appends to display
  var key_map = {};

  // number keys
  for (var i = 0; i <= 9; i++) {
    key_map[i] = "" + i;
  }

  // operator keys
  var ops = {'.':'.','+':'+', '-':'-', '×':'*', '÷':'/'};
  for (var op in ops) {
    key_map[op] = ops[op];
  }

  // create and add keys
  for (var label in key_map) {
    key = new Mx.Button({label: "" + label});
    key.set_size(key_side, key_side);
    key.signal.clicked.connect(Lang.bind(this, this.append), key_map[label]);
    keys_layout.add_actor(key);
  }

  // clear key
  key = new Mx.Button({label:'C'});
  key.set_size(key_side, key_side);
  key.signal.clicked.connect(Lang.bind(this, this.reset));
  keys_layout.add_actor(key);

  // equals key
  key = new Mx.Button({label:'='});
  key.set_size(key_side, key_side);
  key.signal.clicked.connect(Lang.bind(this, this.equals));
  keys_layout.add_actor(key);

  // add layout to stage
  stage.add_actor(main_layout);

  stage.show_all();
  Clutter.main();
  stage.destroy();
};

//  File:Docbook-Callout-8.png
Calculator.prototype.append = function(clicked, character) {
  this.display.append(character);
};

Calculator.prototype.reset = function() {
  this.display.reset();
};

Calculator.prototype.equals = function() {
  try {
    var result = eval(this.display.get_text());
    this.display.set_text(result);
  }
  catch (e) {
    print(e);
    this.display.error();
  }
};

Import GObject library, so we can define new properties on the subclass we're creating.

The parent should be assigned the value of the type property of the class you want to subclass (here, Mx.Label).

The properties key takes an array of hashes; each hash in the array defines a new GObject property. Inside the hash for a property, set the following keys:

  • name will become a property accessible on class instances; here, instance.size
  • type should be set to one of the GObject.TYPE_* constants; the most useful are:
    GObject.TYPE_INT
    GObject.TYPE_FLOAT
    GObject.TYPE_STRING
    GObject.TYPE_BOOLEAN
    GObject.TYPE_OBJECT (a GObject instance)
    Here is a full list of the types available (albeit in C form).The main reason for using GObject properties (rather than simpler JavaScript ones) is that are the only mechanism available for passing arguments to the constructor for a GObject subclass; it is also the only way to expose them to environments which manipulate GObject properties (e.g. ClutterSmith property editors and Clutter.Script JSON files). In most other situations, it's easier to use standard JavaScript properties.
  • flags can be used to set whether the read and write permissions for the property; use GObject.ParamFlags.READABLE, GObject.ParamFlags.WRITABLE, or both added together; the default is GObject.ParamFlags.READABLE + GObject.ParamFlags.WRITABLE

If the GType has an init key with an assigned function, that function is run each time the class is instantiated. Here it's used to set the JavaScript-only locked property and call the reset method on the instance (to set the display to "0").

The class_init key can be assigned a function which is run when the class is created, enabling you to assign new methods to the subclass or override methods from the parent class (see this section for more details).

Note that any GObject properties you defined are accessible via standard JavaScript property accessor syntax: this.size here, for example.

Instantiates an instance of the subclass, setting the size GObject property on the object.

The callbacks have been modified slightly so that they basically proxy onto methods on the CalculatorDisplay. This means the CalculatorDisplay methods can be cleaner, as their signatures don't have to accommodate the element which emitted the signal etc..


Running this version of the application isn't any different from running the previous one. However, we potentially have a reusable CalculatorDisplay component we could reuse in other projects.

Creating subclasses of GObject classes in Seed

Note that where you're using pure JavaScript classes, classical JavaScript subclassing approaches work; for example:

var A = function() {};

A.prototype.foo = function() {
  print("foo");
};

A.prototype.bar = function() {
  print("bar");
};

var B = function() {};

// B is a subclass of A
B.prototype = new A();

B.prototype.bar = function() {
  print("sub bar ftw");
};

var a = new A();
print("a says...");
a.foo();
a.bar();

var b = new B();
print("b says...");
b.foo();
b.bar();

Which yields this output:

a says...
foo
bar
b says...
foo
sub bar ftw

The bar method has been overridden in the subclass B, but the foo method is inherited from A, as you'd expect.

Unfortunately, this style of subclassing doesn't work in Seed for classes which are defined by GObject (i.e. all the classes you import with the imports.gi.* syntax). For example, here's a program which attempts to subclass Mx.Button:

// THIS DOESN'T WORK
Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;

Clutter.init(0, null);
var stage = Clutter.Stage.get_default();

// create a subclass of Mx.Button
var SpecialButton = function() {};
SpecialButton.prototype = new Mx.Button();

// try to instantiate it
var button = new SpecialButton({label: 'Click me'});

// replace the above line with the one below
// (where Mx.Button is used directly) and it does work
// var button = new Mx.Button({label: 'Click me'});

stage.add_actor(button);

stage.show();
Clutter.main();
stage.destroy();

This produces an error:

(seed:14212): Clutter-CRITICAL **: clutter_id_pool_add: assertion `id_pool != NULL' failed

** (seed:14212): CRITICAL **: Line 11 in gobject_subclassing_does_not_work.js: ArgumentError \
Unable to make argument 1 for function: add_actor.

NB the error is thrown when you try to instantiate and use the class, rather than at class definition time.

This is a problem if you want to add or override behaviour on something like an Mx widget, while still enabling it to be recognised as an Mx widget by other GObject classes (e.g. if you want to put it inside a Clutter stage). The correct approach in this case is to use a Seed work-around which allows subclassing of GObject classes. See this code for an example of how to subclass an Mx.Button.

Example 7. The right way to subclass GObject classes with Seed

Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;

Clutter.init(0, null);
var stage = Clutter.Stage.get_default();

SpecialButton = new GType({  //  File:Docbook-Callout-1.png
  parent: Mx.Button.type,
  name: "SpecialButton"
});

var button = new SpecialButton({label: 'Click me'});

stage.add_actor(button);

stage.show();
Clutter.main();
stage.destroy();

"The GType API is the foundation of the GObject system. It provides the facilities for registering and managing all fundamental data types, user-defined object and interface types." (http://library.gnome.org/devel/gobject/unstable/gobject-Type-Information.html) Seed uses GType as the mechanism for creating subclasses of existing GObject classes (the same way you would if using GObject from C), but provides some syntactic sugar around the process.


Note that Seed also provides an API for emitting and installing signals: see http://people.gnome.org/~racarr/seed/runtime.html for more details.

An aside: calling the superclass method from an overridden method in the subclass

An interesting one which I worked out while testing how method overriding in subclasses works. Add this inside the CalculatorDisplay class definition:

CalculatorDisplay = new GType({
  parent: Mx.Label.type,
  name: "CalculatorDisplay",

  // snip

  class_init: function(klass, proto) {
    // snip

    proto.set_text_parent = proto.set_text;

    proto.set_text = function(t) {
      this.set_text_parent(t);
      print(t);
    };
  }
});

What we're doing here is overriding the set_text of Mx.Label, so CalculatorDisplay prints to the console each time set_text is invoked. It works by:

  • Creating an alias set_text_parent for the superclass set_text method. This needs to be done before you override the method (a technique I borrowed from Ruby).
  • Creating a new set_text method with the same signature as the superclass method.
  • Calling the alias set_text_parent inside the new, overriding method, passing it the same arguments as set_text. Note that inside the method definition, the instance context is used, so set_text_parent is called on this.

If you run the application now, each time the calculator display changes, the new value is printed to the console too.

Summary

This is just a short introduction to some of the basic concepts of using JavaScript for MeeGo development. Other topics which I will cover at a later date (which I've tried but haven't fully documented yet):

  • Styling Mx widgets using CSS stylesheets
  • Loading a Clutter+Mx user interface definition from a JSON file (ClutterScript)

I also intend to show a more complex application (a PDF viewer) written in JavaScript.

And some open questions:

  • How best to use ClutterSmith?
  • How to build JavaScript projects? perhaps with autotools, perhaps with something else?
  • i18n and l10n - how do they work in a JavaScript context?
  • Desktop integration (.desktop files etc.) - best way to do that?
  • Can JavaScript applications be nicely integrated with MeeGo?
Personal tools