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 |
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:
And more specifically, how to go about writing a simple calculator application using it:
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.
| Technology preview alert! | |
|---|---|
The stuff I'm writing about here is highly experimental. Stuff might blow up or something. Use with care. |
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.
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 |
|---|---|---|---|
|
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.
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.
| A 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).
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).
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).
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.)
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.
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:
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__ //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
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
$ ./jscalculator
Providing seed is on the path, it should work.
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; //Calculator = function(argv, resource_directory) { //
Clutter.init(argv.length, argv); //
// 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); //Calc = imports['calculator.js']; //
new Calc.Calculator(Seed.argv, resource_directory); //
![]()
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:
The next step is to plug in some (very crude) calculator code. The aims here are to:
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; //Lang = imports.lang; //
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
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 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; //
} // 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); // 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() { //
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:
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:
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.)
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:
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; //Lang = imports.lang; CalculatorDisplay = new GType({ parent: Mx.Label.type, //
name: "CalculatorDisplay", properties: [ //
{ name: "size", type: GObject.TYPE_INT, default_value: 18, minimum_value: 10, maximum_value: 25 } ], init: function() { //
this.locked = false; this.reset(); }, class_init: function(klass, proto) { //
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) { //
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}); //
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(); }; //
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:
| ||||||
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.
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({ //
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 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:
If you run the application now, each time the calculator display changes, the new value is printed to the console too.
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):
I also intend to show a more complex application (a PDF viewer) written in JavaScript.
And some open questions: