The Paint sample application demonstrates how you can use the EFL Evas API to draw the following rectangle, polygon, and line shapes.
The following figure illustrates the application main view.
Figure: Paint screen
You can:
- Clear the drawing area
- Select an object already in the drawing area
- Draw using the freehand, rectangle, circle, or line tool
- Remove the selected object
- Show and hide the colorselector panel
The following figure illustrates the structure of the user interface.
Figure: Paint UI layout structure
The application workflow can be divided into 2 logical blocks - startup and drawing - described in the following figure.
Figure: Application workflow
Implementation
Type Definitions
The appdata_s is the main data structure used as a container for the application data:
typedef struct { Evas_Object *win; // Application main window Evas_Object *layout; // Application layout Evas_Object *conform; // Application conformant Evas_Object *draw_area; // Drawing area Evas_Object *color_selector_panel; // Colorselector box icon_data_s icon_data[IMAGE_POOL_SIZE]; // Icon data used in the toolbar widget } appdata_s;
The s_info structure contains the data necessary for drawing the shapes:
static struct { Evas_Object *win; // Application main window Evas_Object *layout; // Application layout Evas_Object *draw_area; // Drawing area rectangle object_t *current_object; // Currently drawn object object_t *selected; // Currently selected object Evas_Object *selection_frame; // Frame surrounding the selected object Evas *evas; // Application's Evas canvas int r1; // Red component of the color from the MAIN colorselector int g1; // Green component of the color from the MAIN colorselector int b1; // Blue component of the color from the MAIN colorselector int a1; // Alpha component of the color from the MAIN colorselector int r2; // Red component of the color from the FILL colorselector int g2; // Green component of the color from the FILL colorselector int b2; // Blue component of the color from the FILL colorselector int a2; // Alpha component of the color from the FILL colorselector Evas_Coord_Point start; // Position in which the cursor was pressed Evas_Coord_Point curr; // Current cursor position Evas_Coord_Point prev; // Previous cursor position mode_type_t mode; // Application mode (such as rectangle drawing) mode_type_t prev_mode; // Previous mode the application was in bool mouse_pressed; // Flag indicating whether the mouse button is pressed Eina_List *objects; // List of drawn objects int win_layer; // Evas_Layer of the main window } s_info
The object_t structure contains the type of the drawn object and an Evas_Object pointer list of objects it is built with:
typedef struct { mode_type_t type; // Object type (such as LINE or CIRCLE) Eina_List *parts; // List containing the object parts } object_t;
The icon_data_s structure contains the data used by the toolbar icons:
typedef struct { const char *file_path; // Path to the icon image file const char *tooltip; // Icon tooltip text mode_type_t mode; // Application mode associated with the toolbar item Elm_Object_Item *item; // Object added to the toolbar } icon_data_s;
The mode_type_t enum contains the modes used by the application. The enum values are also used as indexes of the toolbar items.
typedef enum { CLEAN = 0, // Clear all (application never uses this mode) SELECT, // Object selection FREEHAND, // Freehand drawing RECTANGLE, // Rectangle drawing CIRCLE, // Circle drawing LINE, // Line drawing COLOR_SELECTOR, // Show and hide the colorselector panel (application never uses this mode) REMOVE, // Remove the selected object (application never uses this mode) } mode_type_t;
Application Initialization
The entire application life-cycle is implemented in the paint.c file, using the common Tizen application structure:
int main(int argc, char *argv[]) { appdata_s ad = {{0,},}; // Variable declaration and initialization event_callback.create = app_create; event_callback.terminate = app_terminate; event_callback.pause = app_pause; event_callback.resume = app_resume; event_callback.app_control = app_control; // Event handler assignment ret = ui_app_main(argc, argv, &event_callback, &ad); // Error handling return ret; }
The Paint sample application is implemented using the MVC design pattern. Its initialization is done within the app_create() callback function where the user interface creation and application data initialization is triggered using the create_base_gui() function.
static bool app_create(void *data) { // Hook to take necessary actions before main event loop starts // Initialize UI resources and application data // If this function returns true, the main loop of application starts // If this function returns false, the application is terminated appdata_s *ad = (appdata_s *)data; // Error handling return create_base_gui(ad); }
When the application terminates, the app_terminate() callback function frees the allocated resources. The deletion of the main window also deletes all of its children.
static void app_terminate(void *data) { // Release all resources appdata_s *ad = (appdata_s *)data; // Error handling evas_object_del(ad->win); }
View
The entire application layout is implemented using EDJE scripts. All the top level swallows are designed for EFL Elementary UI component embedding. The following EDJE swallow - EFL Elementary UI component relations and assigned functionalities are used (for more information, see the Paint UI layout structure figure):
- elm_toolbar (application toolbar):
- Sets the drawing mode the application is in
- Selects an object
- Removes the selected object
- Shows and hides the colorselector panel
- Clears the drawing area
- Evas_Object (drawing area rectangle): Area where the user can perform the drawing
The following table defines the code snippets that create the UI layout.
Code snippet | Figure |
---|---|
The main layout is defined in the paint.edc file: | |
collections { group { name: "main"; parts { // Toolbar part part { name: PART_TOOLBAR; type: SWALLOW; // PART_TOOLBAR height equals 10% of total window height // The toolbar created with toolbar_create() function is here } // Drawing area part part { name: PART_DRAW_AREA; // PART_DRAW_AREA is under the PART_TOOLBAR and covers the rest of the window // Rectangle created with draw_area_create() is here } } } } |
Based on the layout defined with the EDJE scripts, the application interface is created with the create_base_gui() function. The function takes 1 parameter: a pointer to the structure containing the application data (appdata_s). The functions responsible for creating the GUI are invoked in this function.
Eina_Bool create_base_gui(appdata_s *ad) { ad->win = app_win_create(); // Error handling ad->conform = conformant_create(ad->win); // Error handling ad->layout = layout_create(ad->conform, ad); // Error handling if (!toolbar_create(ad->layout, ad)) // Error handling if (!color_selector_panel_create(ad->win, ad->color_selector)) // Error handling if (!win_evas_get(ad->win)) // Error handling if (!selection_frame_create(ad)) // Error handling if (!draw_area_create(ad->layout)) // Error handling return EINA_TRUE; }
The following table defines the base view creation details.
Description | Code snippet | Figure |
---|---|---|
app_win_create():
Creates the application main window (ad->win). |
static Evas_Object* app_win_create(void) { Evas_Object *win = NULL; win = elm_win_util_standard_add(PACKAGE, PACKAGE); // Error handling // Standard window setup return win; } |
|
conformant_create():
Creates the conformant and inserts it into the main window (ad->conform). |
static Evas_Object* conformant_create(Evas_Object *win) { Evas_Object *conform = NULL; // Error handling conform = elm_conformant_add(win); // Error handling evas_object_size_hint_weight_set(conform, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); elm_win_resize_object_add(win, conform); evas_object_show(conform); return conform; } |
|
layout_create():
Creates the main layout by loading the main group from the EDJE layout (paint.edj file), and embeds it into the ad->layout container. The newly created layout is then added as the content to the conformant. |
static Evas_Object* layout_create(Evas_Object *conform, appdata_s *ad) { char path[PATH_MAX]; Evas_Object *layout = NULL; // Error handling app_get_resource(EDJ_FILE, path, (int)PATH_MAX); layout = elm_layout_add(conform); // Error handling if (!elm_layout_file_set(layout, path, GRP_MAIN)) // Error handling evas_object_size_hint_weight_set(layout, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); ecore_event_handler_add(ECORE_EVENT_KEY_DOWN, key_press_cb, ad); elm_object_content_set(conform, layout); s_info.layout = ad->layout; return layout; } |
|
toolbar_create():
Creates an elm_toolbar widget and adds it to the layout that was created earlier. The toolbar is then filled using the data from ad->icon_data. |
static Eina_Bool toolbar_create(Evas_Object *layout, appdata_s *ad) { Evas_Object* toolbar = NULL; int i = 0; // Error handling toolbar = elm_toolbar_add(layout); // Error handling // Toolbar items appending elm_toolbar_homogeneous_set(toolbar, EINA_TRUE); // Toolbar is expanded so that all of its items fit inside elm_toolbar_shrink_mode_set(toolbar, ELM_TOOLBAR_SHRINK_EXPAND); elm_layout_content_set(layout, PART_TOOLBAR, toolbar); return EINA_TRUE; } |
|
color_selector_create():
Creates 2 elm_colorselector instances. They are not used as content of a different UI component. |
static Evas_Object* color_selector_create(Evas_Object *win) { Evas_Object *cs = elm_colorselector_add(win); // Error handling evas_object_layer_set(cs, EVAS_LAYER_MAX); evas_object_smart_callback_add(cs, COLOR_SELECTOR_COLOR_SELECTED, colorselector_color_set_cb, NULL); return cs; } |
|
win_evas_get():
Retrieves the Evas of the window (ad->win). |
static Eina_Bool win_evas_get(Evas_Object *win) { s_info.evas = evas_object_evas_get(win); // Error handling return EINA_TRUE; } |
|
selection_frame_create():
Creates the selection frame image object. This object is later used to highlight the selected object. |
static Eina_Bool selection_frame_create(appdata_s *ad) { char path[PATH_MAX] = {0,}; // Error handling s_info.selection_frame = evas_object_image_filled_add(s_info.evas); // Error handling // Preparing the frame return EINA_TRUE; } |
|
draw_area_create():
Creates the drawing area object. This is a rectangle which is used as the drawing background. |
static Eina_Bool draw_area_create(Evas_Object *layout) { // Error handling s_info.draw_area = evas_object_rectangle_add(s_info.evas); // Error handling // Size setting // Callback invoked when the mouse button is pressed evas_object_event_callback_add(s_info.draw_area, EVAS_CALLBACK_MOUSE_DOWN, mouse_down_cb, NULL); // Callback invoked when the mouse button is released evas_object_event_callback_add(s_info.draw_area, EVAS_CALLBACK_MOUSE_UP, mouse_up_cb, NULL); // Callback invoked when the mouse cursor is moved // while the mouse button is pressed evas_object_event_callback_add(s_info.draw_area, EVAS_CALLBACK_MOUSE_MOVE, mouse_move_cb, NULL); elm_layout_content_set(layout, PART_DRAW_AREA, s_info.draw_area); return EINA_TRUE; } |
Callbacks
The following figure illustrates how various callbacks are invoked when mouse action takes place.
Figure: Mouse callback order
The mouse callbacks are added to the ad->draw_area component so that any action outside the drawing area (like a click on the toolbar) is ignored.
The following table defines the mouse callback details.
Description | Code snippet |
---|---|
mouse_down_cb():
Called when the user taps the ad->draw_area element. The tap position is stored in the s_info.start structure and the s_info.pressed flag is set. The flag is required because of the callback call order (see the above image). The functions called by the callback are invoked based on the mode set by the ad->toolbar element. |
static void mouse_down_cb(void *data, Evas *e, Evas_Object *obj, void *event_info) { Evas_Event_Mouse_Down *eemd = (Evas_Event_Mouse_Down *)event_info; // Error handling s_info.mouse_pressed = true; s_info.start.x = eemd->canvas.x; s_info.start.y = eemd->canvas.y; s_info.prev.x = eemd->canvas.x; s_info.prev.y = eemd->canvas.y; s_info.curr.x = eemd->canvas.x; s_info.curr.y = eemd->canvas.y; item_unselect(); if (s_info.mode == SELECT) { select_clicked_item(); } else { item_unselect(); } } |
mouse_move_cb():
Called whenever the user moves the cursor. Since the callback is connected to the ad->draw_area element, the function is not called if the cursor is moved outside its boundaries. The callback is not invoked when the mouse button is released. Based on the mode set using the ad->toolbar element, appropriate functions are called. The callback also sets the values of the current (s_info.current) and previous (s_info.previous) cursor positions. |
static void mouse_move_cb(void *data, Evas *e, Evas_Object *obj, void *event_info) { int x = 0, y = 0, w = 0, h = 0; Evas_Event_Mouse_Move *eemm = (Evas_Event_Mouse_Move *)event_info; // Error handling evas_object_geometry_get(obj, &x, &y, &w, &h); s_info.prev.x = s_info.curr.x; s_info.prev.y = s_info.curr.y; if (eemm->cur.canvas.x < x) { s_info.curr.x = x; } else if (eemm->cur.canvas.x > x + w) { s_info.curr.x = x + w; } else { s_info.curr.x = eemm->cur.canvas.x; } if (eemm->cur.canvas.y < y) { s_info.curr.y = y; } else if (eemm->cur.canvas.y > y + h) { s_info.curr.y = y + h; } else { s_info.curr.y = eemm->cur.canvas.y; } switch (s_info.mode) { case FREEHAND: freehand_update(); break; case RECTANGLE: rect_update(); break; case CIRCLE: circle_update(); break; case LINE: line_update(); break; case SELECT: object_move(); break; } } |
mouse_up_cb():
Called when the mouse button is released. The s_info.mouse_pressed flag is unset, and the s_info.current_object (recently drawn) object is set as the selected object. |
static void mouse_up_cb(void *data, Evas *e, Evas_Object *obj, void *event_info) { s_info.mouse_pressed = false; selected_item_set(s_info.current_object); s_info.current_object = NULL; } |
The following table defines the toolbar callback details.
Description | Code snippet |
---|---|
toolbar_clear_clicked_cb():
Called when the user wants to clear the drawing area. The area is cleared by deleting all the previously drawn objects and their parts (evas_object_delete()). |
static void toolbar_clear_clicked_cb(void *data, Evas_Object *obj, void *event_info) { draw_area_clear(); } static void draw_area_clear(void) { Eina_List *l = NULL; Evas_Object *obj = NULL; EINA_LIST_FOREACH(s_info.objects, l, obj) { // Error handling evas_object_del(obj); } s_info.objects = eina_list_free(s_info.objects); evas_object_hide(s_info.selection_frame); } |
toolbar_colorselector_show_cb():
Called when the user wants to select a new color. The colorselectors are shown, but the toolbar's colorselector item is not set as selected. |
static void toolbar_colorselector_show_cb(void *data, Evas_Object *obj, void *event_info) { appdata_s *ad = (appdata_s *)data; // Error handling elm_toolbar_item_selected_set(ad->icon_data[s_info.mode].item, EINA_TRUE); color_selector_panel_visibility_set(ad, EINA_TRUE); } |
toolbar_selected_item_del_cb():
Called when the user wants to deleted a selected item. The currently selected (s_info.selected) object and all of its parts are removed. |
static void toolbar_selected_item_del_cb(void *data, Evas_Object *obj, void *event_info) { Eina_List *l = NULL; Evas_Object *part = NULL; // Error handling elm_toolbar_item_selected_set(ad->icon_data[s_info.mode].item, EINA_TRUE); // Error handling EINA_LIST_FOREACH(s_info.selected->parts, l, part) { // Error handling dlog_print(DLOG_INFO, LOG_TAG, "Removing part of type: %s", evas_object_type_get(part)); evas_object_del(part); } s_info.objects = eina_list_remove(s_info.objects, s_info.selected); free(s_info.selected); item_unselect(); } |
The drawing and selection modes are described in Drawing.
Drawing
While the user is drawing, you must control the colorselectors, base shapes, and selections.
Colorselectors
The application uses 2 colorselectors:
- MAIN
Used to set the color of all the parts of the line and freehand shapes, and the boards of the rectangle and circle objects.
When the MAIN colorselector field is clicked, the s_info.r1, s_info.g1, s_info.b1, and s_info.a1 values are set.
- FILL
Used to control the color of the fill part of the rectangle and circle objects.
When the FILL colorselector field is clicked, the s_info.r2, s_info.g2, s_info.b2, and s_info.a2 values are set.
The values set with the colorselectors are used to set the color of the created or selected objects.
The color_set() function is called when the user changes the active color of one of the colorselectors. If an object is selected (s_info.selected != NULL), the color of its parts is also updated.
static void color_set(color_selector_t cs_num, int r, int g, int b, int a) { Eina_List *l = NULL; Evas_Object *part = NULL; if (cs_num == COLOR_SELECTOR_MAIN) { s_info.r1 = r; s_info.g1 = g; s_info.b1 = b; s_info.a1 = a; } else { s_info.r2 = r; s_info.g2 = g; s_info.b2 = b; s_info.a2 = a; } // Error handling if (s_info.selected->type == LINE || s_info.selected->type == FREEHAND) { EINA_LIST_FOREACH(s_info.selected->parts, l, part) { // Error handling evas_object_color_set(part, s_info.r1, s_info.g1, s_info.b1, s_info.a1); } } else { if (cs_num == COLOR_SELECTOR_MAIN) { part = eina_list_nth(s_info.selected->parts, 0); } else { part = eina_list_nth(s_info.selected->parts, 1); } evas_object_color_set(part, r, g, b, a); } }
Base Shapes
Every object that appears on the canvas is made from 1 or more rectangle, polygon, or line parts.
The drawing is performed inside the mouse_move_cb() function. If the s_info.current_object value is equal to NULL, a new object_t variable is created and added to the s_info.objects list. Usually, this happens in the first mouse_move_cb() call after the mouse_down_cb() call.
Depending on the drawing mode, new Evas_Objects are created or modified (with every mouse_move_cb() call) and added to the s_info.current_object structure as drawn object's parts. The object is modified (parts are added or resized) until the user releases the mouse button. When it happens, the newly created object is marked as selected (s_info.selected = s_info.current_object).
The following table defines the update functions for different base shapes.
Description | Code snippet |
---|---|
freehand_update():
Called by the mouse_move_cb() callback. With every mouse_move_cb() call (except the one before mouse_down_cb()), an Evas_Object line is created and added to the object_t object as a part. When the new part is created, the s_info.prev and s_info.curr values are used as its start and end points. The start point's vertical position value has to be lower than the end point's vertical position value. |
static void freehand_update(void) { Evas_Object *part = NULL; int x1 = 0, y1 = 0, x2 = 0, y2 = 0; // Error handling if (!s_info.current_object) { s_info.current_object = object_create(FREEHAND); s_info.objects = eina_list_append(s_info.objects, s_info.current_object); } part = evas_object_line_add(s_info.evas); // Error handling object_part_add(s_info.current_object, part); if (s_info.prev.y <= s_info.curr.y) { x1 = s_info.prev.x; x2 = s_info.curr.x; y1 = s_info.prev.y; y2 = s_info.curr.y; } else { x2 = s_info.prev.x; x1 = s_info.curr.x; y2 = s_info.prev.y; y1 = s_info.curr.y; } evas_object_color_set(part, s_info.r1, s_info.g1, s_info.b1, s_info.a1); evas_object_line_xy_set(part, x1, y1, x2, y2); dlog_print(DLOG_INFO, LOG_TAG, "Updating line at: [%d, %d] - [%d, %d]", x1, y1, x2, y2); evas_object_repeat_events_set(part, true); evas_object_show(part); evas_object_layer_set(part, s_info.win_layer + 1); } |
rect_update():
Creates a rectangle object. Rectangle is made with 2 parts: a frame and the fill. After the parts are created, the frame part is moved to the appropriate position. The fill part's initial position is translated (from the frame part's initial position) by OBJECT_BORDER in both horizontal and vertical directions. Each time the mouse_move_cb() callback is called, the frame and fill parts are resized accordingly. To calculate the width and the height of the frame part, subtract the mouse cursor coordinates (x2 = s_info.start.x < s_info.curr.x; y2 = s_info.start.y < s_info.curr.y;) from the start point coordinates. In case of the fill part, the size is further reduced by 2 * OBJECT_BORDER. |
static void rect_update(void) { int x1 = 0, y1 = 0, x2 = 0, y2 = 0; Evas_Object *part = NULL; // Error handling if (s_info.start.x < s_info.curr.x) { x1 = s_info.start.x; x2 = s_info.curr.x - s_info.start.x; } else { x1 = s_info.curr.x; x2 = s_info.start.x - s_info.curr.x; } if (s_info.start.y < s_info.curr.y) { y1 = s_info.start.y; y2 = s_info.curr.y - s_info.start.y; } else { y1 = s_info.curr.y; y2 = s_info.start.y - s_info.curr.y; } part = eina_list_nth(s_info.current_object->parts, 0); evas_object_move(part, x1, y1); evas_object_resize(part, x2, y2); part = eina_list_nth(s_info.current_object->parts, 1); evas_object_move(part, x1 + OBJECT_BORDER, y1 + OBJECT_BORDER); evas_object_resize(part, x2 - (OBJECT_BORDER*2), y2 - (OBJECT_BORDER*2)); } |
circle_update():
Creates and resizes a circle object based on user actions. The circle object is created from 2 parts: border and fill. Since the Evas API does not provide a circle, the Evas_Object polygon is used instead. The s_info.start value is used as the center of the circle (ellipse). Based on the difference between the s_info.start and the s_info.curr coordinates, the horizontal and vertical radius is calculated. The polygon is made out of 360 points placed on the canvas in appropriate positions calculated with the sin() and cos() functions. To update the size of the circle (at every mouse_move_cb() call), all of its points are removed and created again in new positions. The fill parts are added the same way, but the fill radius is smaller by OBJECT_BORDER. |
static void circle_update(void) { Evas_Object *part = NULL; int radius_x = 0; int radius_y = 0; int center_x = 0; int center_y = 0; // Error handling if (!s_info.current_object) { s_info.current_object = object_create(CIRCLE); circle_part_create(s_info.current_object, s_info.r1, s_info.g1, s_info.b1, s_info.a1); circle_part_create(s_info.current_object, s_info.r2, s_info.g2, s_info.b2, s_info.a2); s_info.objects = eina_list_append(s_info.objects, s_info.current_object); } radius_x = (s_info.curr.x - s_info.start.x) / 2; radius_y = (s_info.curr.y - s_info.start.y) / 2; center_x = s_info.start.x + radius_x; center_y = s_info.start.y + radius_y; radius_x = abs(radius_x); radius_y = abs(radius_y); part = eina_list_nth(s_info.current_object->parts, 0); circle_part_update(part, radius_x, radius_y, center_x, center_y); part = eina_list_nth(s_info.current_object->parts, 1); circle_part_update(part, radius_x - OBJECT_BORDER, radius_y - OBJECT_BORDER, center_x, center_y); } |
line_update():
Created from 2 points, where the beginning point's vertical position value has to be the smaller. Based on that, the s_info.start and s_info.curr structures are used. The points are updated at every mouse_move_cb() callback call. |
static void line_update(void) { int x1 = 0, y1 = 0, x2 = 0, y2 = 0; // Error handling if (!s_info.current_object) { line_create(); return; } if (s_info.start.y <= s_info.curr.y) { x1 = s_info.start.x; x2 = s_info.curr.x; y1 = s_info.start.y; y2 = s_info.curr.y; } else { x2 = s_info.start.x; x1 = s_info.curr.x; y2 = s_info.start.y; y1 = s_info.curr.y; } evas_object_line_xy_set(current_object_first_part_get(), x1, y1, x2, y2); } |
Selections
Objects are selected on 2 occasions:
- When the user finishes drawing a new object.
- When the user taps a part of an already drawn object when the application mode is set to SELECT.
When an object is selected, the s_info.selection_frame (Evas_Object image) is shown and its size and position are adjusted so that the frame surrounds all of the object parts. The object_move() function is called by the mouse_move_cb() callback. The function calculates the offset of the mouse position and translates all the object parts by that same distance.
When the user clicks the _info.selection_frame and there is no other object in that position, the current selection remains active.