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 |
In this tutorial, you'll build a simple PDF viewer for MeeGo, using the JavaScript programming language. The interface will be fairly simple, with this functionality:
The tutorial isn't a complete introduction to JavaScript programming, but aims to introduce how to develop JavaScript applications for MeeGo. It will also familiarise you with the following aspects of working with MeeGo's APIs:
You might also find it useful to look at this tutorial which introduces JavaScript development for MeeGo more generally.
During the tutorial, you can develop and run the application using one of the following approaches:
By the end of the tutorial, you'll have a simple but realistic application which can be deployed to a MeeGo 1.0 machine.
| Seed on MeeGo | |
|---|---|
Seed (which enables apps to run using WebKit's JavaScript engine) is not installed by default on MeeGo. To install it, from a command line do: sudo yum install seed |
This tutorial uses autotools to structure and build the project. I feel this needs some justification, as these tools are fairly difficult to work with: there is an overwhelming amount of documentation available, and getting started can be difficult.
The reason I chose to use autotools is that they give you immediate integration with Linux distributions: you can build a distribution tarball with little effort, and easily configure a build to fit a typical system (e.g. put the executables in the right place, integrate with menus, set up paths correctly). In the case of JavaScript applications, you don't need to compile the code itself; but the other standard build features are still very useful.
Having said this, I'm not going to attempt to explain the whole of autotools. Partly because that would need its own book, and partly because I don't know enough about them. But I will outline a minimal set of autotools artefacts you'll need for any typical Seed JavaScript project.
| Tip | |
|---|---|
I used the project structure of the GNOME Games lightsoff game to develop the structure below. Note that this is a very basic structure we'll add to throughout the tutorial, but it's one which provides a minimal, useful skeleton for any autotooled Seed JavaScript project. |
Create a new project directory and some files inside it as follows:
mkdir pdfviewer # main project directory mkdir pdfviewer/m4 # build macros mkdir pdfviewer/src # JavaScript source files and scripts mkdir pdfviewer/data # supporting assets, e.g. images, stylesheets touch pdfviewer/autogen.sh # standard script for building an autotooled project touch pdfviewer/configure.ac # for autoconf touch pdfviewer/Makefile.am # for automake touch pdfviewer/src/Makefile.am # for automake, making source files touch pdfviewer/src/main.js # main JavaScript file (the one which we run) touch pdfviewer/src/paths.js.in # defines paths to resources touch pdfviewer/src/pdfviewer.in # script which runs main.js under Seed # files required for GNU-style project touch pdfviewer/NEWS touch pdfviewer/README touch pdfviewer/AUTHORS touch pdfviewer/COPYING touch pdfviewer/ChangeLog # make the build script executable chmod +x pdfviewer/autogen.sh
Put the following text into the files:
Example 1. pdfviewer/autogen.sh
#!/bin/sh
ACLOCAL="${ACLOCAL-aclocal} $ACLOCAL_FLAGS" autoreconf -v -i || exit $?
./configure "$@"
Example 2. pdfviewer/configure.ac
# define some variables for version numbering m4_define(pdfviewer_version_major, 0) m4_define(pdfviewer_version_minor, 1) m4_define(pdfviewer_version_micro, 0) m4_define([pdfviewer_version], \ [pdfviewer_version_major.pdfviewer_version_minor.pdfviewer_version_micro]) AC_INIT(pdfviewer, pdfviewer_version) AC_CONFIG_MACRO_DIR([m4]) AM_INIT_AUTOMAKE(pdfviewer, pdfviewer_version) # check sed is available AC_PROG_SED AC_OUTPUT([ Makefile src/Makefile ])
Example 3. pdfviewer/Makefile.am
SUBDIRS = src ACLOCAL_AMFLAGS =-I m4 # cleans up build artefacts for maintainers MAINTAINERCLEANFILES = \ aclocal.m4 \ compile \ config.guess \ config.h.in \ config.sub \ configure.in \ configure \ depcomp \ gtk-doc.make \ install-sh \ ltmain.sh \ Makefile.in \ Makefile \ missing \ intltool-extract.in \ intltool-merge.in \ intltool-update.in \ mkinstalldirs
Example 4. pdfviewer/src/Makefile.am
pdfviewerdir = $(pkgdatadir) # these files go into $(prefix)/share/pdfviewer pdfviewer_DATA = \ main.js \ paths.js # these files go into $(prefix)/bin bin_SCRIPTS = \ pdfviewer # replace tokens in .in templates with real paths # to create installable files pdfviewer: pdfviewer.in paths.js $(SED) -e 's|%PKGDATADIR%|$(pdfviewerdir)|' $< > $@ paths.js: paths.js.in $(SED) -e 's|%PKGDATADIR%|$(pdfviewerdir)|' $< > $@ # files to include in the distribution tarball EXTRA_DIST = \ pdfviewer.in \ paths.js.in \ main.js # files to clean for maintainer purposes MAINTAINERCLEANFILES = \ Makefile.in # files to clean always CLEANFILES = \ paths.js \ pdfviewer
Example 5. pdfviewer/src/pdfviewer.in
This is a template script for running the application under Seed. During the build, the %PKGDATADIR% token is replaced with the real path to the directory containing the JavaScripts (e.g. /usr/local/share/pdfviewer). The output goes into the file pdfviewer/src/pdfviewer, which is the script for running the application.
#!/usr/bin/env bash /usr/bin/env seed %PKGDATADIR%/main.js $*
Example 6. pdfviewer/src/paths.js.in
A template file used to generate the pdfviewer/src/paths.js file. See this section for more information.
var resource_dir = '%PKGDATADIR%';
Example 7. pdfviewer/src/main.js
This script loads the paths.js file into the Paths namespace, then prints the path to the resource directory.
imports.searchPath.push(__script_path__);
Paths = imports['paths.js'];
print('Resource directory set to: ' + Paths.resource_dir);
You now have a minimal but functional autotooled JavaScript project on which you can use the standard make commands. To test the build and install, issue these commands:
jhbuild-meego shell # if you have Seed installed in a jhbuild environment ./autogen.sh --prefix=/usr/local # generate autotools scripts make # generate files from templates sudo make install # install to /usr/local
Test the application has installed correctly by running it from the command line:
$ jhbuild-meego run pdfviewer Resource directory set to: /usr/local/share/pdfviewer
To clean up afterwards:
make clean # clean generated files sudo make uninstall # remove installed files
You can also use other standard make targets:
make maintainer-clean # clean up build artefacts make dist # create distribution tarball
Once the project is set up and the build is working, you're ready to code.
Clutter is a powerful framework for creating GUIs, and is at the heart of the MeeGo user interface (known as UX). It is possible to use Clutter to build a whole interface, as it supports constructing and laying out windows, adding 2D elements like rectangles and images, handling input device events (mouse clicks, keyboard presses etc.), animating interface elements, etc..
However, the main purpose of Clutter is to provide a low-level foundation for more traditional, higher-level widget toolkits. The MeeGo UI Toolkit is one example framework, and is comparable to other widget toolkits (like GTK+ or Swing). It provides standard widgets like buttons, combo boxes, text entry fields and labels.
Clutter and the UI Toolkit are distributed as part of MeeGo 1.0. When used together, they provide a full range of components for building a user interface.
Clutter provides the following primitive UI elements:
The UI Toolkit extends Clutter's primitives to provide easier-to-use, theme-able UI elements. These make it simpler to make an application fit into the MeeGo look and feel:
In the following sections, we'll put these components together to create the UI for the PDF viewer.
In this section, we'll put the basic UI components together. A good place to start is with a rough idea of what the interface should look like. Use whichever tool or approach you like for this: I used Pencil, a basic sketching tool plugin for Firefox, but you can use paper and pencil if it suits. Here's roughly what the PDF viewer will look like:

The basic layout here is the one used for MeeGo applications: toolbar at the top with no menus (only buttons), and designed to fill the screen. Also note that the implementation will get more complicated as we go (for example, we'll need scrollbars).
We can break this down into an initial tree of components to implement:
In the next section, we'll start bringing the PDF viewer to life.
Now we have an idea of what the interface should look like, the first step in implementing it is to get a UI Toolkit Application to hold everything and give us a stage to work with. Inside your PDF viewer project, edit src/main.js, replacing the content with this:
Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;
imports.searchPath.push(__script_path__);
Paths = imports['paths.js'];
// class for the whole PDF viewer
PdfViewer = function() {
Clutter.init(0, null);
// create a single-instance UI Toolkit Application
var app = new Mx.Application({flags: Mx.ApplicationFlags.SINGLE_INSTANCE,
application_name: 'PDF viewer'});
var window = app.create_window();
// get the default Clutter stage;
// NB we set a size here otherwise the stage doesn't display
var stage = window.get_clutter_stage();
stage.set_size(600, 500);
// show the stage
stage.show_all();
// run the mainloop
app.run();
};
// create a PdfViewer instance
new PdfViewer();
Since we have a working application, now would be a good time to verify it works on a MeeGo machine. The first step is to set up some way of getting files to the device. Using SSH is probably the simplest, as packages are readily available for MeeGo. On the MeeGo machine, from a command line, do:
sudo yum install openssh-server sudo chkconfig --add sshd sudo /etc/init.d/sshd start
You should now be able to SSH files to the MeeGo machine using whatever tools you are familiar with.
There are a few approaches to getting the application working on the MeeGo device (once you have a way of getting files over to it):
Once you've deployed the application, you should be able to run it with the standard pdfviewer on the command line.
Next, we'll add some basic layouts.
To get elements in the right arrangement, we can use one of the various layout containers provided by the MeeGo UI Toolkit. These include:
For full details of how these layouts work, see the MeeGo UI Toolkit API documentation.
From the range of layouts available, we need to decide which ones we can use to create our user interface. For our purposes, we can just use two Mx.BoxLayout elements inside the stage to implement the rough design we put together earlier:
Here's how this arrangement can be implemented (note that I've added coloured rectangles temporarily, so that the layout elements are visible):
Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;
imports.searchPath.push(__script_path__);
Paths = imports['paths.js'];
// class for the whole PDF viewer
PdfViewer = function() {
Clutter.init(0, null);
// create a single-instance UI Toolkit Application
var app = new Mx.Application({flags: Mx.ApplicationFlags.SINGLE_INSTANCE,
application_name: 'PDF viewer'});
var window = app.create_window();
// get the default Clutter stage
var stage = window.get_clutter_stage();
var width = stage.get_width();
var height = stage.get_height();
// toolbar buttons + label container
// width is less than the full toolbar,
// to account for the close button
var toolbar_inner_width = width * 0.9;
var toolbar = window.get_toolbar();
var toolbar_height = toolbar.get_height();
var toolbar_inner = new Mx.BoxLayout();
toolbar_inner.set_size(toolbar_inner_width, toolbar_height);
var blue = new Clutter.Color({red:0, green:0, blue:255, alpha:255});
var blue_rect = new Clutter.Rectangle({color: blue,
width: toolbar_inner_width,
height: toolbar_height});
toolbar_inner.add_actor(blue_rect);
toolbar.add_actor(toolbar_inner);
// PDF panel
var pdf_panel_height = height - toolbar_height;
var pdf_panel = new Mx.BoxLayout();
pdf_panel.set_size(width, pdf_panel_height);
var green = new Clutter.Color({red:0, green:255, blue:0, alpha:255});
var green_rect = new Clutter.Rectangle({color: green,
width: width,
height: pdf_panel_height});
pdf_panel.add_actor(green_rect);
window.set_child(pdf_panel);
// show the stage
stage.show_all();
// run the mainloop
app.run();
};
// create a PdfViewer instance
new PdfViewer();
The application should look something like this when it's running:

The green area will eventually be where we display the PDF; and the blue area is the toolbar. (Note there is a small rendering artefact on the right-hand side of the layout beside the green rectangle.)
See the section on deploying the application to MeeGo for more details about getting your application onto a device.
The MeeGo UI Toolkit provides a range of widgets common to desktop applications. The image below gives examples of the most useful ones:

See the Mx API documentation for full details. Here are some brief notes on each:
In the rough design for the PDF viewer application, we have a toolbar with the following elements:
To implement them, we'll use Mx.Button and Mx.Label widgets. For the moment, they won't do anything, but be placeholders for functionality. Here's some sample code showing how to add buttons to the toolbar panel:
Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;
imports.searchPath.push(__script_path__);
Paths = imports['paths.js'];
PdfViewer = function() {
Clutter.init(0, null);
var app = new Mx.Application({flags: Mx.ApplicationFlags.SINGLE_INSTANCE,
application_name: 'PDF viewer'});
var window = app.create_window();
var stage = window.get_clutter_stage();
var width = stage.get_width();
var height = stage.get_height();
// toolbar buttons + label container
var toolbar_inner_width = width * 0.6;
var toolbar = window.get_toolbar();
var toolbar_height = toolbar.get_height();
var toolbar_inner = new Mx.BoxLayout();
toolbar_inner.set_size(toolbar_inner_width, toolbar_height);
// buttons and page label; text is temporary
var button_width = toolbar_inner_width * 0.2;
var label_width = toolbar_inner_width - (button_width * 4);
var first_button = new Mx.Button({name: 'first',
label: '<<',
width: button_width,
height: toolbar_height});
var previous_button = new Mx.Button({name: 'previous',
label: '<',
width: button_width,
height: toolbar_height});
var pages_label = new Mx.Label({name: 'pages_label',
text: 'Page X of Y',
width: label_width,
x_align: Mx.Align.MIDDLE,
y_align: Mx.Align.MIDDLE});
var next_button = new Mx.Button({name: 'next',
label: '>',
width: button_width,
height: toolbar_height});
var last_button = new Mx.Button({name: 'last',
label: '>>',
width: button_width,
height: toolbar_height});
toolbar_inner.add_actor(first_button);
toolbar_inner.add_actor(previous_button);
toolbar_inner.add_actor(pages_label);
toolbar_inner.add_actor(next_button);
toolbar_inner.add_actor(last_button);
// PDF panel
var pdf_panel_height = height - toolbar_height;
var pdf_panel = new Mx.BoxLayout();
pdf_panel.set_size(width, pdf_panel_height);
// add elements to main window
toolbar.add_actor(toolbar_inner);
window.set_child(pdf_panel);
stage.show_all();
app.run();
};
new PdfViewer();
Notice that the pages_label doesn't display the real number of pages yet, but just has some (unreadable) dummy text in it. The buttons also need icons rather than text. Most of this will be fixed in the next section using styling.
It's possible to style UI Toolkit applications using CSS-like stylesheets. These do not support the full range of CSS properties, but provide enough functionality to help your application fit into the desktop.
There are two parts to adding stylesheets to the application:
Follow the steps below to create a stylesheet and integrate it with the build.
Create a new file, pdfviewer/data/styles.css, with this content:
Example 8. pdfviewer/data/styles.css
#first {
-mx-icon-name: go-first;
}
#previous {
-mx-icon-name: go-previous;
}
#next {
-mx-icon-name: go-next;
}
#last {
-mx-icon-name: go-last;
}
#pages_label {
font-size: 18px;
}
The # selectors apply a style to any UI element with a matching name: so the styling for the selector #next will be applied to any widget in the UI with the name next (The name property for each button was set when it was created.)
We're using a special, non-CSS property, -mx-icon-name, to do the styling. The UI Toolkit will assign the appropriate default icon (with filename matching the -mx-icon-name) from the theme to each button.
Next, create pdfviewer/data/Makefile.am to add the stylesheet to the files deployed for the application:
Example 9. pdfviewer/data/Makefile.am
pdfviewerdir = $(pkgdatadir) pdfviewer_DATA = \ styles.css EXTRA_DIST = \ styles.css
Now tell pdfviewer/configure.ac to generate the Makefile for the data directory by editing the AC_OUTPUT lines (near the end of the file):
Example 10. pdfviewer/configure.ac
# ... snipped ... AC_OUTPUT([ Makefile data/Makefile src/Makefile ])
And, finally, add it to the SUBDIRS variable in pdfviewer/Makefile.am:
Example 11. pdfviewer/Makefile.am
SUBDIRS = data src # ... snipped ...
Test the build integration by ensuring that data/styles.css is:
The stylesheet should be loaded before the Mx.Application and its widgets are created, to ensure styling gets applied correctly. The typical approach is to get the default UI Toolkit stylesheet, then load your changes over the top from your own stylesheet, as follows:
Example 12. pdfviewer/src/main.js
Clutter = imports.gi.Clutter;
Mx = imports.gi.Mx;
imports.searchPath.push(__script_path__);
Paths = imports['paths.js'];
PdfViewer = function() {
Clutter.init(0, null);
// get the default UI Toolkit styling
var style = Mx.Style.get_default();
// load our stylesheet (from the resource directory)
style.load_from_file(Paths.resource_dir + '/styles.css');
var app = new Mx.Application({flags: Mx.ApplicationFlags.SINGLE_INSTANCE,
application_name: 'PDF viewer'});
var window = app.create_window();
var stage = window.get_clutter_stage();
var width = stage.get_width();
var height = stage.get_height();
// toolbar buttons + label container
var toolbar_inner_width = width * 0.6;
var toolbar = window.get_toolbar();
var toolbar_height = toolbar.get_height();
var toolbar_inner = new Mx.BoxLayout();
toolbar_inner.set_size(toolbar_inner_width, toolbar_height);
// buttons and page label;
// let the toolkit do button sizing
var label_width = toolbar_inner_width * 0.5;
var first_button = new Mx.Button({name: 'first'});
var previous_button = new Mx.Button({name: 'previous'});
var pages_label = new Mx.Label({name: 'pages_label',
text: 'Page X of Y',
width: label_width,
x_align: Mx.Align.MIDDLE,
y_align: Mx.Align.MIDDLE});
var next_button = new Mx.Button({name: 'next'});
var last_button = new Mx.Button({name: 'last'});
toolbar_inner.add_actor(first_button);
toolbar_inner.add_actor(previous_button);
toolbar_inner.add_actor(pages_label);
toolbar_inner.add_actor(next_button);
toolbar_inner.add_actor(last_button);
// PDF panel
var pdf_panel_height = height - toolbar_height;
var pdf_panel = new Mx.BoxLayout();
pdf_panel.set_size(width, pdf_panel_height);
// add elements to main window
toolbar.add_actor(toolbar_inner);
window.set_child(pdf_panel);
stage.show_all();
app.run();
};
new PdfViewer();
Here we're using the path to the resource directory, loaded via the paths.js file (which is specific to the installation prefix), to find the stylesheet.
Now that we're using stock icons for the buttons, we can leave the styling engine to determine how wide they are and no longer need to set any text on them.
The result looks like this:
With the basic UI elements in place, we can now start to implement the PDF viewing functionality. For this, we do the following:
We can use the builtin Seed.argv global variable to get at the command line arguments passed to the application and grab the filename passed to the PDF viewer application:
Example 13. pdfviewer/src/main.js
// ... snipped ...
// get the file URI from the command line
var file_uri = Seed.argv[2];
if (!file_uri) {
throw {name: 'ArgumentError', message: 'No PDF file URI specified'};
}
print('Loading ' + file_uri);
var pdfviewer = new PdfViewer();
Note that we use Seed.argv[2] as the first command line argument is seed, and the second is the path to the main.js script. The third argument is the URI of the file to load. Also note that an error is thrown if a file URI is not specified.
The PDF viewer can now be invoked with the command line:
pdfviewer file:///path/to/file.pdf
In the next section, we'll do something with the PDF file.
Poppler is a PDF rendering library; it provides a variety of bindings, but we'll be using the GLib ones (poppler-glib) from JavaScript to:
We'll implement a thin wrapper around the Poppler.Document class, with additional methods to track paging; we can also set it up to emit a signal (like other GObject classes) when the page changes:
Example 14. PdfDocument class
Poppler = imports.gi.Poppler; GObject = imports.gi.GObject; // See this tutorial for // more information about declaring new GObject classes in JavaScript PdfDocument = new GType({ name: 'PdfDocument', parent: GObject.Object.type, // add a new 'page_changed' signal signals: [ { name: 'page_changed', /* params: current page, number of pages, page */ parameters: [GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_OBJECT] } ], // args is a hash which can be parsed in the constructor; // it should contain a 'file_uri' to load init: function(args) { // nothing loaded yet, so set these outside normal range this.current_page = -1; this.num_pages = -1; this.load(args['file_uri']); }, class_init: function(klass, proto) { proto.set_current_page = function(page_num) { if (page_num == this.current_page) { return; } if (page_num >= 0 && this.num_pages > page_num) { this.current_page = page_num; // emit the 'page_changed' signal this.signal.page_changed.emit(this.current_page + 1, this.num_pages, this.document.get_page(this.current_page)); } }, // return the current page of the PDF as a Poppler.Page proto.fetch_current_page() { return this.document.get_page(this.current_page); }, proto.goto_first = function() { this.set_current_page(0); }, proto.goto_next = function() { this.set_current_page(this.current_page + 1); }, proto.goto_previous = function() { this.set_current_page(this.current_page - 1); }, proto.goto_last = function() { this.set_current_page(this.num_pages - 1); }, // load the PDF file at file_uri, resetting page properties proto.load = function(file_uri) { this.document = new Poppler.Document.from_file(file_uri, ); this.num_pages = this.document.get_n_pages(); print('Loaded document ' + file_uri + ', which has ' + this.num_pages + ' pages'); } } });
This can be added to the top of the pdfviewer/src/main.js file.
Next, we modify pdfviewer/src/main.js to construct a new PdfDocument and pass it into the constructor for the PdfViewer:
// ... snipped ...
var file_uri = Seed.argv[2];
if (!file_uri) {
throw {name: 'ArgumentError', message: 'No PDF file URI specified'};
}
var doc = new PdfDocument({file_uri: file_uri});
var pdfviewer = new PdfViewer(doc);
Notice here that the new PdfDocument instance is passed a hash containing a file_uri key, set to the URI of the file passed in from the command line.
And finally alter the PdfViewer constructor to accept an instance of PdfDocument and keep a reference to it:
PdfViewer = function(document) {
Clutter.init(0, null);
this.document = document;
// ... snipped ...
};
The final step is to draw pages of the PDF onto the pdf_texture inside the interface. Poppler has functions for drawing a Poppler.Page onto a Cairo context. Cairo is a generic graphics library which is included in MeeGo Core, and can be used for drawing 2D shapes: Poppler simply uses it as a target surface for rendering PDF pages.
Fortunately for us, Clutter provides Cairo integration through the ClutterCairoTexture actor, which effectively embeds a Cairo context inside a Clutter actor. So it's just a case of putting the this together with Poppler's Cairo drawing functions as follows:
Here's what the new parts of the code look like:
// import Seed's canvas library for drawing on Cairo surfaces
Canvas = imports.canvas;
PdfDocument = new GType({
// ... snipped ...
class_init: function(klass, proto) {
// ... snipped ...
// new method to return the current Poppler.Page
proto.fetch_current_page = function() {
return this.document.get_page(this.current_page);
}
}
});
PdfViewer = function(document) {
Clutter.init(0, null);
this.document = document;
// ... snipped ...
// PDF panel
var pdf_panel_height = height - toolbar_height;
var pdf_panel = new Mx.BoxLayout();
pdf_panel.set_size(width, pdf_panel_height);
// texture for drawing the PDF onto;
// note it's a member variable so it can be referenced
// from instance methods
this.pdf_texture = new Clutter.CairoTexture();
pdf_panel.add_actor(this.pdf_texture);
// go to the first page of the document
this.document.goto_first();
// draw the current page of the PDF onto the texture
this.draw_page();
// add elements to main window
toolbar.add_actor(toolbar_inner);
window.set_child(pdf_panel);
stage.show_all();
app.run();
};
PdfViewer.prototype.draw_page = function() {
var page = this.document.fetch_current_page();
var size = page.get_size();
var width = size['width'];
var height = size['height'];
// set the Cairo context to the page size for now
this.pdf_texture.set_surface_size(width, height);
// clear anything currently on the texture
this.pdf_texture.clear();
// create the Cairo context
var cr = this.pdf_texture.create();
// render the page onto the context
page.render(cr);
// create and destroy canvas to finish rendering,
// using Seed's Cairo canvas
var ctx = new Canvas.CairoCanvas(cr);
ctx.destroy();
};
// ... snipped ...
The result looks like this:

At the moment, the PDF is being squashed up, as the page is larger than the area available to display it. Next, we'll add scrollbars to fix this.
The PDF viewer currently squashes the page, as the Clutter.CairoTexture it is being rendered onto is smaller than the page of the PDF.
The solution is to make the widget containing the texture scrollable, so that some of the page is visible and the rest can be scrolled to.
Earlier, we saw the range of layouts available in the MeeGo UI Toolkit. One of these was the Mx.ScrollView, which adds scrollbars to the widget inside it: this fits our use case. But, for this to work, the contained widget must implement the Mx.Scrollable interface: unfortunately, Clutter.CairoTexture does not.
Fortunately, there is a specialised layout, Mx.Viewport, designed to wrap widgets which aren't scrollable by default. We can use this to wrap the Clutter.CairoTexture; then in turn we can wrap the viewport with an Mx.ScrollView to get a scrollbar.
To implement this, we add a new viewport around the Clutter.CairoTexture; and a scrollview around that. The Mx.ScrollView then goes inside the pdf_panel. The Mx.ScrollView will draw scrollbars around the viewport as it grows beyond the size of the pdf_panel:
// ... snipped ...
PdfViewer = function(document) {
Clutter.init(0, null);
// ... snipped ...
// PDF panel
var pdf_panel_height = height - toolbar_height;
var pdf_panel = new Mx.BoxLayout();
pdf_panel.set_size(width, pdf_panel_height);
this.pdf_texture = new Clutter.CairoTexture();
this.viewport = new Mx.Viewport();
this.viewport.add_actor(this.pdf_texture);
this.scrollview = new Mx.ScrollView();
this.scrollview.add_actor(this.viewport);
pdf_panel.add_actor(this.scrollview);
// store the top-left corner of the pdf_panel
// so we can ensure it's visible when the page changes
this.top_left_corner = new Clutter.Geometry({x: 0, y: 0});
// go to the first page of the document
this.document.goto_first();
// draw the current page of the PDF onto the texture
this.draw_page();
// add elements to main window
toolbar.add_actor(toolbar_inner);
window.set_child(pdf_panel);
stage.show_all();
app.run();
};
// ... snipped ...
Finally, in the draw_page method, we resize the resize the viewport to the same size as the texture each time a page is drawn:
PdfViewer.prototype.draw_page = function() {
var page = this.document.fetch_current_page();
var size = page.get_size();
var width = size['width'];
var height = size['height'];
// reset the size of the viewport when the page is rendered
this.viewport.set_size(width, height);
// force the top-left corner of the scrollview to be visible
this.scrollview.ensure_visible(this.top_left_corner);
// ... snipped ...
ctx.destroy();
};
The resulting application looks like this (notice the vertical scrollbar):
The MeeGo Core libraries (including Clutter and the MeeGo UI Toolkit) make extensive use of GObject, the object-oriented framework for C (also accessible from JavaScript). One element of the framework is a signal system, which enables GObjects (including Clutter.Actors) to emit signals when events occur during their lifecycle. (For example, a mouse click on a button causes the button to emit a clicked signal.) The signals system works as follows:
The application can wire specific signals emitted by specific actors to callbacks: functions which are called when a particular signal is emitted by an actor. A callback is invoked with a set of arguments dependent on the type of signal:
The callback does its work, then should either return true if the event should not be propagated to any other actors (e.g. if it has fully handled a button press event) or false if the event should "bubble" up to its parent actor (and to their parents until a callback returns true). The full event flow is described in the ClutterActor documentation.
The signals we want to handle are all to do with button click events, which are represented by a clicked signal emitted by the button which was clicked. (The full list of signals emitted by Clutter actors is available in the Clutter C documentation.)
We also want to attach to the page_changed signal emitted by the PdfDocument instance when the page changes. When that happens, we update the paging label and draw the page. As a side effect, we will also be able to make the code much cleaner, as the PdfDocument instance can pass useful data to the signal handler when the page_changed signal is emitted.
Attaching handlers to signals in JavaScript is made considerably simpler than it is in C, by virtue of the fact that you can easily create anonymous functions as handlers.
Adding these features gives us the final, complete application. The code below is annotated so you can see where the changes were made. Also note that the whole application is crammed into one file here: in reality, you could put the PdfDocument class into its own file and load it from main.js.
Example 15. pdfviewer/src/main.js
Clutter = imports.gi.Clutter; Mx = imports.gi.Mx; Poppler = imports.gi.Poppler; GObject = imports.gi.GObject; Canvas = imports.canvas; // import lang moduleLang = imports.lang; imports.searchPath.push(__script_path__); Paths = imports['paths.js']; PdfDocument = new GType({ name: 'PdfDocument', parent: GObject.Object.type, signals: [ { name: 'page_changed', /* params: current page, number of pages, page */ parameters: [GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_OBJECT] } ], init: function(args) { // nothing loaded yet, so set these outside normal range this.current_page = -1; this.num_pages = -1; this.load(args['file_uri']); }, class_init: function(klass, proto) { proto.set_current_page = function(page_num) { if (page_num == this.current_page) { return; } if (page_num >= 0 && this.num_pages > page_num) { this.current_page = page_num; this.signal.page_changed.emit(this.current_page + 1, this.num_pages, this.document.get_page(this.current_page)); } }, proto.goto_first = function() { this.set_current_page(0); }, proto.goto_next = function() { this.set_current_page(this.current_page + 1); }, proto.goto_previous = function() { this.set_current_page(this.current_page - 1); }, proto.goto_last = function() { this.set_current_page(this.num_pages - 1); }, proto.load = function(file_uri) { this.document = new Poppler.Document.from_file(file_uri, ); this.num_pages = this.document.get_n_pages(); print('Loaded document ' + file_uri + ', which has ' + this.num_pages + ' pages'); } } }); PdfViewer = function(document) { Clutter.init(0, null); this.document = document; // get the default UI Toolkit styling var style = Mx.Style.get_default(); // load our stylesheet (from the resource directory) style.load_from_file(Paths.resource_dir + '/styles.css'); var app = new Mx.Application({flags: Mx.ApplicationFlags.SINGLE_INSTANCE, application_name: 'PDF viewer'}); var window = app.create_window(); var stage = window.get_clutter_stage(); var width = stage.get_width(); var height = stage.get_height(); // toolbar buttons + label container var toolbar_inner_width = width * 0.6; var toolbar = window.get_toolbar(); var toolbar_height = toolbar.get_height(); var toolbar_inner = new Mx.BoxLayout(); toolbar_inner.set_size(toolbar_inner_width, toolbar_height); // buttons and page label var label_width = toolbar_inner_width * 0.5; var first_button = new Mx.Button({name: 'first'}); var previous_button = new Mx.Button({name: 'previous'}); // make pages_label a member variable so it's visible // to methods this.pages_label = new Mx.Label({name: 'pages_label', text: 'Page X of Y', width: label_width, x_align: Mx.Align.MIDDLE, y_align: Mx.Align.MIDDLE}); var next_button = new Mx.Button({name: 'next'}); var last_button = new Mx.Button({name: 'last'}); toolbar_inner.add_actor(first_button); toolbar_inner.add_actor(previous_button); toolbar_inner.add_actor(this.pages_label); toolbar_inner.add_actor(next_button); toolbar_inner.add_actor(last_button); // PDF panel var pdf_panel_height = height - toolbar_height; var pdf_panel = new Mx.BoxLayout(); pdf_panel.set_size(width, pdf_panel_height); this.pdf_texture = new Clutter.CairoTexture(); this.viewport = new Mx.Viewport(); this.viewport.add_actor(this.pdf_texture); this.scrollview = new Mx.ScrollView(); this.scrollview.add_actor(this.viewport); pdf_panel.add_actor(this.scrollview); // store the top-left corner of the pdf_panel // so we can ensure it's visible when the page changes this.top_left_corner = new Clutter.Geometry({x: 0, y: 0}); // bind button clicks to PDF page navigation
first_button.signal.clicked.connect(Lang.bind(this.document, this.document.goto_first)); previous_button.signal.clicked.connect(Lang.bind(this.document, this.document.goto_previous)); next_button.signal.clicked.connect(Lang.bind(this.document, this.document.goto_next)); last_button.signal.clicked.connect(Lang.bind(this.document, this.document.goto_last)); // bind to the page_changed signal from the PdfDocument instance this.document.signal.page_changed.connect(Lang.bind(this, this.page_changed_cb)); // add elements to main window toolbar.add_actor(toolbar_inner); window.set_child(pdf_panel); // go to the first page of the document; NB this will // trigger the page_changed signal from PdfDocument, // which will get handled by the page_changed_cb method // on this class this.document.goto_first(); stage.show_all(); app.run(); }; // this method has been renamed, as it also updates the label now
PdfViewer.prototype.page_changed_cb = function(doc, current_page, num_pages, page) { // update the pages label this.pages_label.text = current_page + ' of ' + num_pages; var size = page.get_size(); var width = size['width']; var height = size['height']; // reset the size of the viewport when the page is rendered this.viewport.set_size(width, height); // force the top-left corner of the scrollview to be visible this.scrollview.ensure_visible(this.top_left_corner); // set the texture to the page size this.pdf_texture.set_surface_size(width, height); // clear anything currently on the texture this.pdf_texture.clear(); // create a Cairo context var cr = this.pdf_texture.create(); // render the page onto the context page.render(cr); // create and destroy canvas to finish rendering, // using Seed's Cairo canvas var ctx = new Canvas.CairoCanvas(cr); ctx.destroy(); }; var file_uri = Seed.argv[2]; if (!file_uri) { throw {name: 'ArgumentError', message: 'No PDF file URI specified'}; } var doc = new PdfDocument({file_uri: file_uri}); var pdfviewer = new PdfViewer(doc);
The lang module is imported so we can use its bind method to bind the right objects when connecting signals to callbacks (see below). | |
These are standard Seed-style signal connections, but in this instance Lang.bind is binding the PdfDocument instance to this in the context of the closure. This ensures that when the callback is triggered, any references to this in the callback resolve to the PdfDocument instance. If Lang.bind is not used in this way, the JavaScript interpreter will use this as it is bound when the signal is connected. In other words, although we'd be binding an event in the PdfViewer instance to a method in the PdfDocument instance, when the callback was triggered, this would be incorrectly interpreted as referring to the PdfViewer instance inside the callback. For more information about variable binding when connecting to signals, see this tutorial. | |
The GObject signal system allows data to be passed to any callback function connected to a signal. In the case of the page_changed signal, this data includes the current page number, the number of pages in the PDF, and the current page (a Poppler.Page instance). Now that we're using signal handling with callbacks, we can change the method signature to accept this data included with the signal. The parameters on the method should first include the triggering object (in this case, doc, which is the PdfDocument instance). Following that are further parameters, in the order specified in the signal declaration. In the case of the page_changed signal:
So, rather than pulling the data off of this.document as we did before, we can use the data passed to the callback with the signal; which also means we can lose the fetch_current_page method from PdfDocument. |
Here's the final result:
This tutorial demonstrated how to write an application using Clutter and the MeeGo UI Toolkit.
The resulting application is not perfect and has many missing features:
However, some of these refinements could be added using techniques similar to the ones used through this tutorial.
Full source code for this project is available from the Moblin SDK examples repository. Check it out using git with:
$ git clone git://git.moblin.org/moblin-sdk-examples
The JavaScript PDF viewer is in apps/pdfviewer_js.
If you are interested in learning more about Clutter, have a look at the Clutter project website. There are also other Clutter tutorials available, like Programming with Clutter and Clutter: a Beginner's Tutorial.
If you'd like to find out about other aspects of MeeGo application development, further SDK tutorials covering more-specific topics are available.