Support
Contribute
Contact
Tracker
Navigation
Personal tools
 

Creating Edje User Interfaces

Contents

Introduction

This article describes how to easily create a user interface using:

  1. Python Bindings;
  2. Edje;
  3. Embryo;
  4. Animations;

The application will be a small application launcher and the actual interface will be written using edje and animations. The python part doesn't have any knowledge of the interface, where the icons are placed, how they should move, nor if they are using some kind of effect, like for instance pulsing. All it does is listen for "selected" signals.

A video demostration the application can be found on Gustavo's blog [1]

Code can be found at E17 SVN, under [2]

Image:EdjeAppLauncher.png

Python Code

Let's start looking at the python part. This tutorial is based on revision 1.1 of code, that can be found at [3] of the Enlightenment SVN.

Initialization code

After module imports and parsing the command line options we get the first set of interesting lines (39-51):

if options.engine == "x11":
    f = ecore.evas.SoftwareX11
elif options.engine == "x11-16":
    if ecore.evas.engine_type_supported_get("software_x11_16"):
        f = ecore.evas.SoftwareX11_16
    else:
        print "warning: x11-16 is not supported, fallback to x11"
        f = ecore.evas.SoftwareX11

w, h = options.geometry
ee = f(w=w, h=h)
ee.fullscreen = not options.no_fullscreen
edje.frametime_set(1.0 / options.fps)

Creating Edje objects

By this time we have EcoreEvas and thus our canvas setup, then we can create our Edje with the theme file, lines 60-68:

try:
    edje_obj = edje.Edje(canvas, file=edje_file, group="main")
except Exception, e: # should be EdjeLoadError, but it's wrong on python2.5
    raise SystemExit("Failed to load Edje file: %s" % edje_file)

# resize edje to fit our window, show and remeber it for later use
edje_obj.size = canvas.size
edje_obj.show()
ee.data["edje"] = edje_obj

We try-except around Edje creation so we can present user with a more useful message if file is not found or corrupted. Note that exception is not EdjeLoadError as python 2.5 + PyRex 0.9.4 don't get together that well, this problem goes away with Python 2.4 or using PyRex 0.9.5, then we can use EdjeLoadError fine.

Then we setup object size and show, making it visible on canvas. Notice that nothing will show on screen at this time since canvas itself is hidden.

The last part is to remember this edje object inside EcoreEvas general purpose dictionary. This will be useful when we're called on resize event.

Callbacks

Now setup some event callbacks, lines 72-92:

def resize_cb(ee):
    r = ee.evas.rect
    ee.data["edje"].size = r.size

ee.callback_resize = resize_cb


def key_down_cb(bg, event, ee):
    k = event.key
    if k == "Escape":
        ecore.main_loop_quit()
    if k in ("F6", "f"):
        ee.fullscreen = not ee.fullscreen

edje_obj.on_key_down_add(key_down_cb, ee)


def icon_selected(edje_obj, signal, source):
    print "icon_selected", source

edje_obj.signal_callback_add("selected", "*", icon_selected)

The first callback is from EcoreEvas, when user resizes the window. See that we resize canvas objects we want, in this case just our edje, remembered previously.

The second is an evas object callback, when user press some key it will go to the focused object, that will call this key_down_cb(), we use it to toggle fullscreen and exit application.

Last but not least, we provide an edje signal callback. Edje signals have a name (emission) and source, in this case emission is "selected", from any Edje part ("*"). This is used to get informed when some item is selected. Notice that we don't have any other knowledge of the UI other than this signal, we don't know how many icons, where they are laid out, etc.

Edje Code

Now let's look at the Edje code. Not everything will be explained, but only the basic parts to understand what is going on. This tutorial is based on revision 30969 of code, that can be found at [4] of the Enlightenment SVN.

The Edje file make use of CPP macros to avoid typing, another feature rarely documented. This is quite useful as we can avoid duplicating code for each of the objects.

Here is a small example (lines 61-143):

#define ICON(part_name, relx, rely, part_label)                         \
         part {                                                         \
            name: part_name"_area";                                     \
...

We define a macro ICON that take as parameter part_name, relx, rely and part_label, we use it to avoid typing 4 times the almost-the-same code. Notice that macros should be placed in just one line, if you want multi-line, as I did, you must finish each line with a backslash. If you want to concatenate strings, pay attention to place them together (without spaces), like part_name"_area", where I concatenate the macro argument (part_name) to "_area".


Defining resources

Let us now look at the EDC code. We are dealing with one font and 5 images: one for the background and 4 for the application icons, see lines 1-11:

fonts {
   font: "VeraBd.ttf" "Sans";
}

images {
   image: "background.jpeg" LOSSY 95;
   image: "audio_player.png" COMP;
   image: "image_viewer.png" COMP;
   image: "video_player.png" COMP;
   image: "web_browser.png" COMP;
}

We now can refer to the font as "Sans", images by their name. Images will be stored according to their specification, in this case "background.png" will use lossy compression at 95%, while others will be stored as is, but compressed for size saving, these are the most common used options.

Creating background image

Let's skip the script section and go define our background, it is as easy as (lines 49-59):

         part {
            name: "background";
            type: IMAGE;
            mouse_events: 0;
            description {
               state: "default" 0.0;
               rel1 { relative: 0.0 0.0; }
               rel2 { relative: 1.0 1.0; offset: -1 -1; }
               image { normal: "background.jpeg"; }
            }
         }

It should be pretty auto-explanatory, with this part being an image, that get no mouse events, default state being positioned with top-left (rel1) at (0,0) and bottom-right (rel2) at object bottom-right edje, using regular image "background.jpeg".

Just pay attention and understand what rel2 { relative: 1.0 1.0; offset: -1 -1; } means. Let's take Edje object coordinate to be 800x480, then relative: 1.0 1.0; would mean point (800,480), but since we count from 0, we need to subtract one to get the correct point, that's why we have offset: -1 -1;, resulting in the point (799,479).

Also, don't bother with image { normal: "background.jpeg"; }, this will just change when you want to do animations by changing images, then you'll have to add "tween" commands.

Defining ICON() macro

The user interface works the following way. We have 4 icons placed in each corner (top, right; top, left; bottom, right; bottom, left), and when one is selected the icon moves to the middle and starts pulsing (the icon and the label becomes bigger and the label is centered on the icon. At the same time, a "shadow" of the icon pulses in the background of the icon).

In order to accomplish this we use 4 parts.

<part_name>_area
the clicking area (rectangle) where we accept mouse clicks for the object.
<part_name>_pulser
the application icon centered on the background with a pulsing shadow.
<part_name>
the application icon, shown in one corner that will move to the center
<part_name>_label
the application label.

We make use of the ICON() macro to accomplish this, with the following parts:

ICON() <part_name>_area

Lines 62-72 defines the clicking area with position being the macro parameters (relx and rely). It accept mouse events, but color is transparent (color: 0 0 0 0;), so not visible to user:

         part {                                                         \
            name: part_name"_area";                                     \
            type: RECT;                                                 \
            mouse_events: 1;                                            \
            description {                                               \
               state: "default" 0.0;                                    \
               rel1 { relative: relx rely; offset: -96 -96; }           \
               rel2 { relative: relx rely; offset:  95  95; }           \
               color: 0 0 0 0;                                          \
            }                                                           \
         }                                                              \

ICON() <part_name>_pulser

Lines 73-98 defines the pulser object, they all will be centered, by default objects are transparent so will not be visible, with "selected" state being initially (state: "selected" 0.0;)half transparent (color: 255 255 255 128;) and in the end (state: "selected" 1.0;) 1.5 times bigger and also fully transparent:

         part {                                                         \
            name: part_name"_pulser";                                   \
            type: IMAGE;                                                \
            mouse_events: 0;                                            \
            description {                                               \
               state: "default" 0.0;                                    \
               rel1 { relative: 0.5 0.5; offset: -64 -64; }             \
               rel2 { relative: 0.5 0.5; offset:  63  63; }             \
               color: 0 0 0 0;                                          \
               image { normal: part_name".png"; }                       \
            }                                                           \
            description {                                               \
               state: "selected" 0.0;                                   \
               inherit: "default" 0.0;                                  \
               rel1 { relative: 0.5 0.5; offset: -64 -64; }             \
               rel2 { relative: 0.5 0.5; offset:  63  63; }             \
               color: 255 255 255 128;                                  \
            }                                                           \
            description {                                               \
               state: "selected" 1.0;                                   \
               inherit: "default" 0.0;                                  \
               rel1 { relative: 0.5 0.5; offset: -96 -96; }             \
               rel2 { relative: 0.5 0.5; offset:  95  95; }             \
               color: 255 255 255 0;                                    \
            }                                                           \
         }                                                              \

Note that pulser is aligned to the center by defining both rel1 and rel2 relative to be 0.5 0.5, the object size is defined in pixels by offset, with sizes being 128x128 (with offset: -64 -64; and offset: 63 63;) and 192x192 (offset: -96 -96; and offset: 95 95;).


ICON() <part_name>

Lines 99-115 define the part that initially will be placed on one corner and then will move to the center. This is simple to do by defining two states, "default" with relative positioning based on macro arguments relx and rely and "selected" state, centered and a double the size.

         part {                                                         \
            name: part_name;                                            \
            type: IMAGE;                                                \
            mouse_events: 0;                                            \
            description {                                               \
               state: "default" 0.0;                                    \
               rel1 { relative: relx rely; offset: -32 -32; }           \
               rel2 { relative: relx rely; offset:  31  31; }           \
               image { normal: part_name".png"; }                       \
            }                                                           \
            description {                                               \
               state: "selected" 0.0;                                   \
               inherit: "default" 0.0;                                  \
               rel1 { relative: 0.5 0.5; offset: -64 -64; }             \
               rel2 { relative: 0.5 0.5; offset:  63  63; }             \
            }                                                           \
         }                                                              \

ICON() <part_name>_label

Lines 116-143 define the text label, initially centered at corner specified by macro arguments relx and rely and later centered at </code>0.5 0.5</code>, but a bit bigger. Pay attention to fit: 1 1, this makes the text size be recalculated to fit the specified box from rel1 to rel2.

         part {                                                         \
            name: part_name"_label";                                    \
            type: TEXT;                                                 \
            effect: SHADOW;                                             \
            mouse_events: 0;                                            \
            description {                                               \
               state: "default" 0.0;                                    \
               rel1 { relative: relx rely; offset: -100  33; }          \
               rel2 { relative: relx rely; offset:   99  53; }          \
               align: 0.5 0.5;                                          \
               color: 255 255 255 255;                                  \
               color2: 0 0 0 255;                                       \
               color3: 0 0 0 255;                                       \
               text {                                                   \
                  font: "Sans";                                         \
                  size: 18;                                             \
                  text: part_label;                                     \
                  min: 1 1;                                             \
                  fit: 1 1;                                             \
               }                                                        \
            }                                                           \
            description {                                               \
               state: "selected" 0.0;                                   \
               inherit: "default" 0.0;                                  \
               rel1 { relative: 0.5 0.5; offset: -128 -25; }            \
               rel2 { relative: 0.5 0.5; offset:  127  24; }            \
            }                                                           \
         }

Defining logic to control selected item

We go back to the script block at lines 18-46 we skipped earlier. This code is "embryo", an implementation of PAWN language (a C subset, see the language guide for more in depth explanation).

      script {
         public selected = 0;
         public pulsing = 0;
         public stop_pulsing_timer_id = 0;
         const Float:pulse_timeout = 10.0;

         public unselect() {
            if (get_int(selected) == 0)
               return;
            run_program(get_int(selected));
            set_int(selected, 0);
         }

         public stop_pulsing() {
            if (get_int(pulsing) == 0)
               return;
            set_state(get_int(pulsing), "default", 0.0);
            set_int(pulsing, 0);
            if (get_int(stop_pulsing_timer_id) != 0) {
               cancel_timer(get_int(stop_pulsing_timer_id));
               set_int(stop_pulsing_timer_id, 0);
            }
         }

         public stop_pulsing_cb(val) {
            stop_pulsing();
            return 0;
         }
      }

The logic is pretty simple and done in unselect() and stop_pulsing(), with stop_pulsing_cb() just a wrapper to return 0 in order to stop the scheduled timer after this function is run from it.

Although Embryo/PAWN is simple, you need to be careful with public/global values, here selected, pulsing and stop_pulsing_timer_id. You must use get_int()/set_int() and similar get_float()/set_float(). Failing to do so will issue no error and you'll get a bogus value (like accessing the pointer instead of the value it points to).


Defining ICON_PROGRAMS() macro

Edje programs are simple way to do some action based on signals. You can change states, trigger transitions or even run some Embryo code.

We use the following programs:

set_selected_<part_name>
connected to click of mouse left-button click (mouse,clicked,1) of icon's click area (<part_name>_area), it will stop pulsing any previously pulser, unselect any previously selected icon and then emit "selected" signal, to inform Python code and also trigger the next program select_<part_name>.
select_<part_name>
This trigger a linear transition of 0.2 second to change state of both <part_name>and <part_name>_label to "selected", then it will run start_pulse_<part_name>.
start_pulse_<part_name>
stop pulsing any existent pulsing, then set the global variable of "who is pulsing" to this icon pulser, schedule a timer to stop pulsing in pulse_timeout (10 seconds) and then run the program to reset pulser state to "selected" 0.0 (or shrunk, shrink_<part_name>).
shrink_<part_name>
reset state to "selected" 0.0 and then run program to start growing the pulser again.
grow_<part_name>
trigger a linear transition of 0.5 second to the "selected" 1.0 state, then run check_continue_pulsing_<part_name>.
check_continue_pulsing_<part_name>
check if still pulsing, if so run shrink_<part_name>, otherwise reset pulser to default state.
unselect_<part_name>
trigger a linear transition of 0.2 second on both <part_name>, <part_name>_label and <part_name>_pulser.
#define ICON_PROGRAMS(part_name)                                        \
         program {                                                      \
            name: "set_selected_"part_name;                             \
            signal: "mouse,clicked,1";                                  \
            source: part_name"_area";                                   \
            script {                                                    \
               const pid = PROGRAM:"unselect_"part_name;                \
               if (get_int(selected) == pid)                            \
                  return;                                               \
               stop_pulsing();                                          \
               unselect();                                              \
               set_int(selected, pid);                                  \
               emit("selected", part_name);                             \
            }                                                           \
         }                                                              \
         program {                                                      \
            name: "select_"part_name;                                   \
            signal: "selected";                                         \
            source: part_name;                                          \
            action: STATE_SET "selected" 0.0;                           \
            target: part_name;                                          \
            target: part_name"_label";                                  \
            transition: LINEAR 0.2;                                     \
            after: "start_pulse_"part_name;                             \
         }                                                              \
         program {                                                      \
            name: "start_pulse_"part_name;                              \
            script {                                                    \
               stop_pulsing();                                          \
               set_int(pulsing, PART:part_name"_pulser");               \
               new i = timer(pulse_timeout, "stop_pulsing_cb", 0);      \
               set_int(stop_pulsing_timer_id, i);                       \
               run_program(PROGRAM:"shrink_"part_name);                 \
            }                                                           \
         }                                                              \
         program {                                                      \
            name: "shrink_"part_name;                                   \
            action: STATE_SET "selected" 0.0;                           \
            target: part_name"_pulser";                                 \
            after: "grow_"part_name;                                    \
         }                                                              \
         program {                                                      \
            name: "grow_"part_name;                                     \
            action: STATE_SET "selected" 1.0;                           \
            target: part_name"_pulser";                                 \
            transition: LINEAR 0.5;                                     \
            after: "check_continue_pulsing_"part_name;                  \
         }                                                              \
         program {                                                      \
            name: "check_continue_pulsing_"part_name;                   \
            script {                                                    \
               if (get_int(pulsing) == PART:part_name"_pulser")         \
                  run_program(PROGRAM:"shrink_"part_name);              \
               else                                                     \
                  set_state(PART:part_name"_pulser", "default", 0.0);   \
            }                                                           \
         }                                                              \
         program {                                                      \
            name: "unselect_"part_name;                                 \
            signal: "unselect_"part_name;                               \
            action: STATE_SET "default" 0.0;                            \
            target: part_name;                                          \
            target: part_name"_label";                                  \
            target: part_name"_pulser";                                 \
            transition: LINEAR 0.2;                                     \
         }

References

API (C, but can be applied to Python):

Further reading: