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

Building a PDF viewer in JavaScript

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:

  1. Open a PDF file and display it
  2. Show the current page number and total number of pages
  3. Navigate between pages (one page at a time)
  4. Navigate to the first or last page
  5. Scroll the page up and down

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:

  • Overview of the MeeGo graphics libraries (Clutter and MeeGo UI Toolkit)
  • A brief introduction to MeeGo UI Toolkit layouts and widgets
  • Styling a MeeGo UI Toolkit interface with CSS-like stylesheets
  • Events processing in a Clutter and MeeGo UI Toolkit application (signals)

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:

  1. Use a MeeGo-like environment to develop and run the application, and deploy to a real MeeGo machine occasionally for verification. This is the approach I assume in this document; see this tutorial with information about running a MeeGo development environment inside jhbuild; see this section of this tutorial for instructions on deploying to a MeeGo device.
  2. Use a standard development environment (where the application doesn't run) but deploy to MeeGo for testing. This setup doesn't really have much to recommend it, but may be more useful when MeeGo is available as a Qemu virtual machine image.
  3. Develop and run the application entirely on a netbook. If you decide to do this, you'll need to install autotools for MeeGo (all the required tools are available).

By the end of the tutorial, you'll have a simple but realistic application which can be deployed to a MeeGo 1.0 machine.

File:Docbook-Tip.pngSeed 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

Starting the project

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.

Project directory structure

File:Docbook-Tip.pngTip

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

File:Docbook-Tip.pngThe need for Seed

Notice above that we used jhbuild-meego run in front of the pdfviewer command. This ensures that the PDF viewer is run inside the MeeGo jhbuild environment. If you have Seed "properly" installed on your system (e.g. on a netbook) you should just be able to run pdfviewer directly.

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.

Building a basic UI

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:

  1. Stage: There is usually a single Clutter stage, which represents the main window.
  2. Actors: Actors are individual visual elements. These can be simple graphical elements like rectangles or images, or more complex elements like UI Toolkit buttons or text entry areas. Actors are added to the stage or other containers on the stage. (Note that the stage itself is an actor.)
  3. Containers: A container is an actor into which other actors can be placed. The stage is a root container for all other actors; containers within the stage can in turn contain other child actors, including other containers. The box is one example container, which uses a layout to organise and present the actors it contains.
  4. Events and signals: When UI events occur on an actor, the actor may emit a signal. For example, if the mouse cursor moves over an actor, it may emit an enter-event signal. (Note that an actor can be configured to ignore events). A callback function can be attached to one or more of the signals emitted by actors. Callbacks allow the application to respond to events occurring on the user interface: for example, a mouse click could cause data to be saved, a field to be cleared, an HTTP request to be initiated, etc..
  5. Animation framework: Clutter makes it fairly simple to animate actors using a variety of familiar effects (fades, slides, rotations etc.), and supports animating 2D interface elements in 3D space. Animation isn't covered in this tutorial.

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:

  1. Application: Provides access to a basic window, making your application look like part of the MeeGo UI. Can also integrate with DBus to ensure that only a single instance of an application is running at any one time.
  2. Window: A wrapper round a Clutter Stage, with a default toolbar. Also handles changing its size depending on the size of the display.
  3. Toolbar: Provides a close button for the application, as well as being a convenient container for adding buttons or other navigational widgets.
  4. Layouts: Layouts specify how children inside a container should be arranged. A few alternatives are available, including basic box layouts (all child actors in a row) and a grid layout (child actors in rows and columns).
  5. Widgets: A full range of UI widgets, like buttons, text entry boxes, and labels.
  6. Deforms: Written on top of Clutter's animation facilities, the UI Toolkit provides some high level deformation effects, like page turns and bow ties (some examples). These aren't covered in this tutorial.

In the following sections, we'll put these components together to create the UI for the PDF viewer.

Roughing out a design

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:

File:MeeGo-js-pdf-rough_ui.png

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:

  • app UI Toolkit application
    • toolbar Default toolbar (accessed from the application window)
      • toolbar_inner Container for buttons and paging label
        • start_button Go to page 1 (button)
        • previous_button Go back to previous page (button)
        • pages_label Text showing where the viewer is in the document: Page <current page> of <total pages> (label)
        • next_button Go forward to next page (button)
        • last_button Go to last page (button)
    • stage Main window (the stage, accessed from the application)
      • pdf_panel Panel to hold the display area for the content of the PDF (a container)
        • pdf_texture A texture to write the PDF content onto (an actor)


In the next section, we'll start bringing the PDF viewer to life.

Setting the stage

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();


File:Docbook-Tip.pngBuild-time vs. run-time dependencies

Note that as we're not compiling any code, we haven't added any PKG_CHECK_MODULES lines to configure.ac, as there are no build-time dependencies on those libraries. However, at runtime, it's possible that required libraries may be missing. If that's the case, you'll get an error message something like this:

** (seed:19437): CRITICAL **: Line 2 in /usr/local/share/pdfviewer/main.js: 
GIrepositoryError Typelib file for namespace 'Mx' (any version) not found

You could add some error-trapping code around your imports statements to sanitise these errors and make them intelligible to end users.

Deploying to a MeeGo machine

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):

  • Copy the whole project as-is to the device, then build and install it there. You'll need autotools on the MeeGo machine to do this.
  • Create a dist tarball from the project, copy that to the MeeGo machine, and build it there. This is easier to build, as you only need make and a few common utilities installed on the MeeGo machine (because there's no compile step in the build).
  • Create an RPM from the project and deploy that to the MeeGo machine. These instructions cover how to use the deprecated (but still useful) Moblin Package Creator to create an RPM from an autotooled project.

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.

Introducing 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:

  • Mx.BoxLayout: This is a layout container which arranges children in a single line, either horizontally (the default) or vertically.
  • Mx.Expander: This is a container can show or hide its single child actor in response to user interactions. It displays a clickable bar (with a text label), which (by default) toggles display of the child.
  • Mx.Grid: This is a flow layout container which can hold multiple children: the children can "flow" around inside the layout as the grid's size changes.
  • Mx.ScrollView: This layout puts appropriate horizontal and vertical scrollbars around a scrollable child actor.
  • Mx.Table: This can be used to layout multiple child actors based on row and column positions.
  • Mx.Viewport: This layout can be used to add scrollbars to actors which are not normally scrollable (like images or text).
  • Mx.ItemView and Mx.ListView: These are data views which provide a means to automatically generate a layout based on the content of a ClutterModel. ClutterModel is a generic API which can be used to maintain a collection of GObjects, providing iterators, selectors, filtering, sorting, insertion/deletion, and signals for a collection. These layouts are beyond the scope of this tutorial.

For full details of how these layouts work, see the MeeGo UI Toolkit API documentation.

Using layouts in the PDF viewer

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:

  • pdf_panel, a horizontal Mx.BoxLayout containing the rendered current page of the PDF; this will be placed inside the Clutter stage
  • toolbar_inner, a horizontal Mx.BoxLayout containing the application buttons and paging label; this will be placed inside the application's default toolbar

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:

File:MeeGo-js-pdf-pdfviewer.png

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.

Adding the toolbar buttons

The MeeGo UI Toolkit provides a range of widgets common to desktop applications. The image below gives examples of the most useful ones:

File:MeeGo-js-pdf-mx-widgets-annotated.png

See the Mx API documentation for full details. Here are some brief notes on each:

  • Mx.Icon Displays a single image (e.g. from a png file).
  • Mx.Label Displays non-editable text. Normally used for labelling other widgets.
  • Mx.Tooltip Displays a short piece of text associated with another widget, usually used to hint at how to use the widget. In the example, hovering a mouse cursor over the Name entry causes the tooltip to appear; moving it away hides the tooltip again.
  • Mx.Entry For entering and editing text.
  • Mx.ComboBox For selecting from a list of options; displays as a drop-down box.
  • Mx.Button A clickable button with a text or image label.

In the rough design for the PDF viewer application, we have a toolbar with the following elements:

  • start_button Go to page 1 (button)
  • previous_button Go back to previous page (button)
  • pages_label Text showing where the viewer is in the document: "Page <current page> of <total pages>" (label)
  • next_button Go forward to next page (button)
  • last_button Go to last page (button)

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.

File:MeeGo-js-pdf-pdfviewer-toolbar-no-functions.png

Styling with the UI Toolkit

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:

  1. Add the stylesheet file to the build
  2. Load the stylesheet file into the application at runtime

Integrating a stylesheet into the build

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:

  • part of the tarball created by make dist.
  • deployed with the application when you run make install.

Loading a stylesheet into the application

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:

File:MeeGo-js-pdf-pdfviewer_styled.png

Loading the PDF

With the basic UI elements in place, we can now start to implement the PDF viewing functionality. For this, we do the following:

  1. Get the name of a PDF file to load from the command line.
  2. Read the PDF file into a format suitable for display. We'll use Poppler to do this.
  3. Display the PDF inside the pdf_panel in the PDF viewer's interface. We'll do this by rendering from Poppler onto a Clutter Cairo actor.

Parsing command line options

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.

Loading the PDF with Poppler

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:

  • Read the PDF file
  • Get a count of the pages in the PDF
  • Retrieve a single page of the PDF
  • Render a page onto a Cairo surface

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 ...
};

Drawing a page of the PDF

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:

  1. Create a Clutter.CairoTexture texture and put it inside the pdf_panel.
  2. Add a method to PdfDocument to return the current Poppler.Page.
  3. Add a method on PdfViewer to fetch a Poppler.Page from the document and draw it onto the Cairo context. We can call this to load the first page when the application starts, and reuse it to draw other pages when the toolbar buttons are clicked.

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:

File:MeeGo-js-pdf-pdfviewer-page-loaded.png

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.

Adding scrollbars

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):

File:MeeGo-js-pdf-pdfviewer-scrollbars.png

Events and making the buttons work

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:

  • Users do things with the interface: move the mouse around, click mouse buttons, enter text via the keyboard etc.. (By default, most Clutter.Actors (apart from the stage) don't respond to events unless they are first made reactive with set_reactive(true).)
  • The application also does things with the user interface: updates some text when it fetches new data from an HTTP server, changes an icon when the network connection is dropped etc..
  • Clutter.Actors detect both types of event and "emit" signals about the events they were involved in (e.g. if they were clicked on or had their content changed).
  • Parts of the application (GObjects, ClutterActors, or functions) listen for signals, and respond when they receive the ones they are "interested" in.

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:

  • All signals include the actor which emitted the signal.
  • Some signals include the event which triggered the signal, e.g. a button event.
  • The signal can optionally have an associated data structure.

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 module  File:Docbook-Callout-1.png
Lang = 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  File:Docbook-Callout-2.png
  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   File:Docbook-Callout-3.png
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:

  • A GObject.TYPE_INT, the current page number = current_page in the method signature
  • A GObject.TYPE_INT, the total number of pages in the document = num_pages in the method signature
  • A GObject.TYPE_OBJECT, the current Poppler.Page = page in the method signature

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:

File:MeeGo-js-pdf-pdfviewer-working-paging-label.png

Summary

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:

  • There's no zoom function. This is pretty important. A related issue is that the scaling of the PDF page doesn't change as the size of the window changes.
  • The scrollbar "sticks" to the right-hand side of the PDF, rather than aligning itself to the right of the window.
  • The PDF page isn't centred in the scrollview and always stays on the left-hand side.
  • There's no way to open a new PDF file (e.g. by browsing to it in a file chooser).

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.

Personal tools