The Calculator sample demonstrates how to implement a complex view using recursive composition of the standard EFL UI components and containers in a UI component hierarchy. It aims to explain how such an application can be developed using view and model parts.
The sample uses UI components (such as elm_conformant and elm_naviframe) for the view management, containers (such as elm_grid and elm_table) for UI component management inside the view, and UI components (such as elm_bg, elm_button, and elm_label) for the content inside view.
The following figure illustrates the main screen of the Calculator (portrait and landscape), the wireframe structure, and the UI component tree.
Figure: Calculator screen
Implementation
The _on_create_cb() callback function initializes the application model and user interface.
static Evas_Object *_add_naviframe(app_data *app) { Evas_Object *result = NULL; Evas_Object *parent = win_get_host_layout(app->win); if (parent) { result = ui_utils_navi_add(parent, _navi_back_cb, app); if (result) { win_set_layout(app->win, result); } } return result; } static bool _on_create_cb(void *user_data) { app_data *app = user_data; RETVM_IF(!app, false, "ad is NULL"); result_type result = RES_OK; app->model = calc_create(&result); RETVM_IF(!app->model, false, "calc instance creation failed: %d", result); app->win = win_create(); RETVM_IF(!app->win, false, "Window creation failed."); app->navi = _add_naviframe(app); RETVM_IF(!app->navi, false, "Naviframe creation failed."); main_view_add(app); return true; }
The win_create() function creates the window which consists of the indicator (elm_conformant) and the background with a red color to easily see if the view does not have its own background component.
window_obj *win_create() { window_obj *obj = calloc(1, sizeof(window_obj)); RETVM_IF(!obj, NULL, "Cannot allocate memory"); obj->win = elm_win_add(NULL, APP_NAME, ELM_WIN_BASIC); elm_win_wm_rotation_available_rotations_set(obj->win, AVAILABLE_ROTATIONS, AVAILABLE_ROTATIONS_LENGTH); elm_win_conformant_set(obj->win, EINA_TRUE); evas_object_show(obj->win); obj->conform = elm_conformant_add(obj->win); evas_object_size_hint_weight_set(obj->conform, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); elm_win_resize_object_add(obj->win, obj->conform); evas_object_show(obj->conform); obj->bg = elm_bg_add(obj->conform); elm_bg_color_set(obj->bg, WIN_FAIL_BG_COLOR); evas_object_size_hint_weight_set(obj->bg, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); evas_object_show(obj->bg); elm_object_part_content_set(obj->conform, "elm.swallow.bg", obj->bg); return obj; }
The main_view_add() function creates the main view layout and all its UI components and pushes itself to the naviframe. This function also registers the rotation change callback with the evas_object_smart_callback_add() function, the callback on model display change with the calc_set_display_change_cb() function and the region format change callback by assigning a function pointer and data to the main application structure.
void main_view_add(app_data *app) { RETM_IF(!app, "app is NULL"); main_view_data *data = calloc(1, sizeof(main_view_data)); RETM_IF(!data, "Failed to allocate the instance"); result_type result = _main_view_init(data, app); if (RES_OK != result) { _main_view_del(data); } } result_type _main_view_init(main_view_data *data, app_data *app) { RETVM_IF(!app->model, RES_ILLEGAL_ARGUMENT, "app->model is NULL"); RETVM_IF(!app->win, RES_ILLEGAL_ARGUMENT, "app->win is NULL"); RETVM_IF(!app->win->win, RES_ILLEGAL_ARGUMENT, "app->win->win is NULL"); RETVM_IF(!app->navi, RES_ILLEGAL_ARGUMENT, "app->navi is NULL"); data->app = app; data->model = app->model; data->window = app->win->win; data->orientation = elm_win_rotation_get(data->window); result_type result = RES_INTERNAL_ERROR; result = _main_view_make_layout(data, app->navi); RETVM_IF(RES_OK != result, result, "Failed to make layout: %d", result); result = _main_view_make_widgets(data); RETVM_IF(RES_OK != result, result, "Failed to make widgets: %d", result); _main_view_repack_widgets(data); Elm_Object_Item *navi_item = elm_naviframe_item_push(app->navi, STR_MAIN_TITLE, NULL, NULL, data->layout, NULL); RETVM_IF(!navi_item, RES_OUT_OF_MEMORY, "Failed to push naviframe item"); evas_object_smart_callback_add(data->window, "wm,rotation,changed", _main_view_win_rotate_cb, data); calc_set_display_change_cb(data->model, _main_view_display_changed_cb, data); app->region_fmt_change_cb = _main_view_region_fmt_changed_cb; app->region_fmt_change_cb_data = data; return RES_OK; }
The _main_view_make_layout() function creates the main view layout components:
- Table: Holds other UI components and adds border padding around the view
- Grid: Positions all UI components on the view in a scalable manner
- Background: Colors the view with the default background color
Figure: Calculator main view layout
The view object is deleted when the main layout component (table) is deleted by its parent. The callback is registered using the evas_object_event_callback_add() function.
result_type _main_view_make_layout(main_view_data *data, Evas_Object *parent) { int scaled_border_padding = (int)utils_round(ELM_SCALE_SIZE(VIEW_BORDER_WIDTH)); // Create a single cell table so the internal grid can support padding Evas_Object *layout = elm_table_add(parent); RETVM_IF(!layout, RES_OUT_OF_MEMORY, "Failed to create elm_table"); data->layout = layout; elm_table_homogeneous_set(layout, EINA_TRUE); evas_object_size_hint_weight_set(layout, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND); evas_object_event_callback_add(layout, EVAS_CALLBACK_FREE, _main_view_layout_free_cb, data); evas_object_show(layout); // Add a background to the view with the default theme color Evas_Object *bg = elm_bg_add(layout); RETVM_IF(!bg, RES_OUT_OF_MEMORY, "Failed to create elm_bg"); elm_table_pack(layout, bg, 0, 0, 1, 1); evas_object_size_hint_align_set(bg, EVAS_HINT_FILL, EVAS_HINT_FILL); evas_object_show(bg); // Create a grid for holding and aligning the UI components Evas_Object *grid = elm_grid_add(layout); RETVM_IF(!grid, RES_OUT_OF_MEMORY, "Failed to create elm_grid"); data->grid = grid; elm_table_pack(layout, grid, 0, 0, 1, 1); evas_object_size_hint_align_set(grid, EVAS_HINT_FILL, EVAS_HINT_FILL); evas_object_size_hint_padding_set(grid, scaled_border_padding, scaled_border_padding, scaled_border_padding, scaled_border_padding); evas_object_show(grid); return RES_OK; }
The _main_view_make_widgets() function creates all UI components (the display label and buttons), but does not pack them to the grid yet. For each UI component, a separate single cell table is created. This table is needed to add padding around a specific UI component, since the grid does not support padding for its elements.
You can use a single table instead of a grid (without additional single cell tables), but the grid is more stable as it resizes all UI components in a single calculation and does not stretch if the UI components are too large. To handle button events, a smart clicked callback is registered with the evas_object_smart_callback_add() function.
result_type _main_view_make_widgets(main_view_data *data) { int scaled_widget_padding = (int)utils_round(ELM_SCALE_SIZE(WIDGET_PADDING)); int scaled_number_padding = (int)utils_round(ELM_SCALE_SIZE(DISP_NUMBER_RIGHT_PADDING)); int btn_idx = 0; for (int i = 0; i < WIDGETS_COUNT; ++i) { const widget_params *params = &WIDGET_PARAMS[i]; // For each UI component in the grid cell, create an additional single cell table // This allows adding paddings for UI components because grid does not support paddings Evas_Object *table = elm_table_add(data->grid); RETVM_IF(!table, RES_OUT_OF_MEMORY, "Failed to create elm_table"); data->widgets[i] = table; // Registering the UI component in the array to pack it to the grid later elm_table_homogeneous_set(table, EINA_TRUE); evas_object_show(table); bool is_button = (CALC_KEY_NONE != params->key); Evas_Object *obj = is_button ? elm_button_add(table) : elm_label_add(table); RETVM_IF(!obj, RES_OUT_OF_MEMORY, "Failed to create widget object"); elm_table_pack(table, obj, 0, 0, 1, 1); if (is_button) { RETVM_IF(BUTTONS_COUNT == btn_idx, RES_INTERNAL_ERROR, "Too many buttons in params!"); main_view_btn_data *btn_data = &data->btns_data[btn_idx]; ++btn_idx; btn_data->view = data; btn_data->key = params->key; evas_object_smart_callback_add(obj, "clicked", _main_view_btn_click_cb, btn_data); int text_size = (int)utils_round(ELM_SCALE_SIZE(((CALC_KEY_ERASE == params->key) ? ERASE_BTN_TEXT_SIZE : GENERAL_BTN_TEXT_SIZE))); char buff[DISP_STR_SIZE] = {'\0'}; if (CALC_KEY_DEC == params->key) { snprintf(buff, DISP_STR_SIZE, params->text, text_size, calc_get_dec_point_str(data->model)); } else { snprintf(buff, DISP_STR_SIZE, params->text, text_size); } elm_object_text_set(obj, buff); evas_object_size_hint_align_set(obj, EVAS_HINT_FILL, EVAS_HINT_FILL); evas_object_size_hint_padding_set(obj, scaled_widget_padding, scaled_widget_padding, scaled_widget_padding, scaled_widget_padding); } else { data->label = obj; _main_view_update_disp_label(data); evas_object_size_hint_align_set(obj, 1, EO_ALIGN_HINT_CENTER); evas_object_size_hint_padding_set(obj, scaled_widget_padding, scaled_widget_padding + scaled_number_padding, scaled_widget_padding, scaled_widget_padding); } evas_object_show(obj); } return RES_OK; }
The _main_view_repack_widgets() function clears the grid from UI components (if any) and packs it again according to the current screen orientation.
void _main_view_repack_widgets(main_view_data *data) { // Remove all UI components from the grid without deleting elm_grid_clear(data->grid, EINA_FALSE); // Obtain the UI component position parameters and resize the grid according to the orientation const widget_pos *pos_array = NULL; if ((((abs(data->orientation) + CIRCLE_HALF_QUARTER_DEG) / CIRCLE_QUARTER_DEG) & 1) == 0) { pos_array = PORT_WPOS; elm_grid_size_set(data->grid, PORT_GRID_W, PORT_GRID_H); } else { pos_array = LAND_WPOS; elm_grid_size_set(data->grid, LAND_GRID_W, LAND_GRID_H); } // Packing UI components to the grid using information from the array for (int i = 0; i < WIDGETS_COUNT; ++i) { elm_grid_pack(data->grid, data->widgets[i], pos_array[i].x, pos_array[i].y, pos_array[i].w, pos_array[i].h); } }
The _main_view_layout_free_cb() function removes the view object from the memory when the main layout component is deleted by the parent.
void _main_view_layout_free_cb(void *data, Evas *e, Evas_Object *obj, void *event_info) { main_view_data *view_data = data; // NULL the layout so you do not delete it twice in _main_view_del() view_data->layout = NULL; // Delete self when layout is deleted by parent object _main_view_del(view_data); }
The _main_view_display_changed_cb() function handles the model display change event and updates the display label. The current display value is retrieved from the model using the calc_get_display_str() function. The label is updated with the elm_object_text_set() function.
void _main_view_display_changed_cb(void *data) { _main_view_update_disp_label((main_view_data *)data); } void _main_view_update_disp_label(main_view_data *data) { RETM_IF(!data->label, "label is NULL"); int text_size = (int)utils_round(ELM_SCALE_SIZE(DISP_NUMBER_TEXT_SIZE)); char buff[DISP_STR_SIZE] = {'\0'}; snprintf(buff, DISP_STR_SIZE, "<font_size=%d>%s</font_size>", text_size, calc_get_display_str(data->model)); elm_object_text_set(data->label, buff); }
The _main_view_region_fmt_changed_cb() function handles the device region format change event. Before the view update, call the calc_update_region_fmt() function to update the model. After this, the decimal point button text can be updated using the result of the calc_get_dec_point_str() function.
void _main_view_region_fmt_changed_cb(void *data) { _main_view_update_region_fmt((main_view_data *)data); } void _main_view_update_region_fmt(main_view_data *data) { RETM_IF(!data->dec_btn, "dec_btn is NULL"); calc_update_region_fmt(data->model); int text_size = (int)utils_round(ELM_SCALE_SIZE(GENERAL_BTN_TEXT_SIZE)); char buff[DISP_STR_SIZE] = {'\0'}; snprintf(buff, DISP_STR_SIZE, "<font_size=%d>%s</font_size>", text_size, calc_get_dec_point_str(data->model)); elm_object_text_set(data->dec_btn, buff); }
The _main_view_btn_click_cb() function handles the button clock event. The function is implemented mostly by calling the calc_handle_key_press() model function. The model method can generate the display change event described earlier.
void _main_view_btn_click_cb(void *data, Evas_Object *obj, void *event_info) { main_view_btn_data *btn_data = data; _main_view_handle_key_press(btn_data->view, btn_data->key); } void _main_view_handle_key_press(main_view_data *data, key_type key) { result_type result = calc_handle_key_press(data->model, key); if (RES_MAX_DIGITS_REACHED == result) { char msg[DISP_STR_SIZE] = {'\0'}; snprintf(msg, DISP_STR_SIZE, "<p align=center>"STR_POPUP_MAX_INPUT"</p>", DISP_MAX_DIGITS); _main_view_create_msg_popup(data, msg); } }
The computation logic of the application is implemented in a separate calc.c module. Before using the module, create the calc object using the calc_create() function. The calc_handle_key_press() function handles a specific calculator key specified by the key_type enumeration.
Handling keys usually changes the calculator display string, which the calc_get_display_str() function can retrieve.
To listen on display string change events, the application can register a callback using the calc_set_display_change_cb() function. By registering this callback, the view can avoid manual checking of the display string value after each key handling.
// Create and initialize a calc object instance calc *calc_create(result_type *result); // Destroy and free the calc instance referenced void calc_destroy(calc *obj); // Register the callback to be invoked on display string change void calc_set_display_change_cb(calc *obj, notify_cb cb, void *cb_data); // Get current display string message const char *calc_get_display_str(calc *obj); // Handle calculator key press result_type calc_handle_key_press(calc *obj, key_type key); // Update region format dependent properties (decimal point character) void calc_update_region_fmt(calc *obj); // Get current decimal point character used by the object const char *calc_get_dec_point_str(calc *obj);
The _calc structure in the calc.c module defines the calc object structure. A pointer to this structure is passed to all functions of this module.
// Internal data structure of the calc object struct _calc { // Error flag to indicate critical failure and block computations until reset bool error; // Major state variables of the calculator bool dirty; // In the middle of expression calculation bool num_is_new; // Indicates that number in num register is new (entered by user of by unary function) bool in_edit_mode; // Indicates that the user is entering number to the display key_type op; // Current operation to apply to operands // Calculator registers used in the computations double num; // Corresponds to the number on display (not actual in edit mode) double acc; // Left operand of the operation, stores the result of the operation double rep; // Special repeat register, used to repeat last operation on result key // Display object to handle I/O display disp; // 1 char string to hold current decimal point character char dec_point_str[CALC_DEC_POINT_STR_SIZE]; };
The following example shows how to implement the calc.c module functions:
result_type calc_handle_key_press(calc *obj, key_type key) { RETVM_IF(!obj, RES_INTERNAL_ERROR, "obj is NULL"); if (CALC_KEY_RESET == key) { _calc_reset(obj); return RES_OK; } RETVM_IF(obj->error, RES_ILLEGAL_STATE, "Calculator is in error state"); result_type result = RES_INTERNAL_ERROR; switch (key & CALC_KEY_TYPE_MASK) { case CALC_KEY_TYPE_NUM: result = _calc_handle_num_key(obj, key); break; case CALC_KEY_TYPE_OP: result = _calc_handle_op_key(obj, key); break; default: result = _calc_handle_etc_key(obj, key); break; } if (RES_OK != result) { _calc_handle_error(obj, result); return result; } return RES_OK; } result_type _calc_handle_etc_key(calc *obj, key_type key) { switch (key) { case CALC_KEY_RESULT: return _calc_handle_result_key(obj); case CALC_KEY_SIGN: return _calc_handle_sign_key(obj); case CALC_KEY_ERASE: return _calc_handle_erase_key(obj); default: return RES_ILLEGAL_ARGUMENT; } } result_type _calc_handle_num_key(calc *obj, key_type key) { bool reset = false; if (!obj->in_edit_mode) { obj->in_edit_mode = true; reset = true; } return display_enter_key(&obj->disp, key, reset); } result_type _calc_handle_op_key(calc *obj, key_type key) { switch (key) { case CALC_KEY_ADD: case CALC_KEY_SUB: case CALC_KEY_MUL: case CALC_KEY_DIV: break; default: return RES_ILLEGAL_ARGUMENT; } _calc_end_edit_mode(obj); result_type result = RES_OK; if (!obj->dirty) { obj->dirty = true; obj->acc = obj->num; } else if (obj->num_is_new) { result = _calc_perform_op(obj, obj->num); } obj->num_is_new = false; obj->op = key; return result; } result_type _calc_handle_result_key(calc *obj) { _calc_end_edit_mode(obj); if (obj->dirty) { obj->dirty = false; obj->rep = obj->num; } else if (obj->num_is_new) { obj->acc = obj->num; } obj->num_is_new = false; return _calc_perform_op(obj, obj->rep); } result_type _calc_handle_sign_key(calc *obj) { if (obj->in_edit_mode) { display_negate(&obj->disp); } else { _calc_perform_negate_func(obj); } return RES_OK; } result_type _calc_handle_erase_key(calc *obj) { RETVM_IF(!obj->in_edit_mode, RES_ILLEGAL_STATE, "Not in edit mode"); return display_erase(&obj->disp); } void _calc_end_edit_mode(calc *obj) { if (obj->in_edit_mode) { obj->in_edit_mode = false; obj->num = display_get_number(&obj->disp); obj->num_is_new = true; // This removes extra chars from the current display (for example: 2.300 -> 2.3; -0.0 -> 0) display_set_number(&obj->disp, obj->num); } } result_type _calc_perform_op(calc *obj, double rv) { switch (obj->op) { case CALC_KEY_ADD: obj->acc += rv; break; case CALC_KEY_SUB: obj->acc -= rv; break; case CALC_KEY_MUL: obj->acc *= rv; break; case CALC_KEY_DIV: if (fabs(rv) <= CALC_ZERO_EPS) { return ((fabs(obj->acc) <= CALC_ZERO_EPS) ? RES_UNDEFINED_VALUE : RES_DIVISION_BY_ZERO); } obj->acc /= rv; break; default: return RES_INTERNAL_ERROR; } result_type result = display_set_number(&obj->disp, obj->acc); RETVM_IF(RES_OK != result, result, "Failed to set display number: f", obj->acc); // Get number back from display to get rounded value obj->num = display_get_number(&obj->disp); obj->acc = obj->num; return RES_OK; } void _calc_perform_negate_func(calc *obj) { if (0.0 != obj->num) { obj->num = -obj->num; (void)display_set_number(&obj->disp, obj->num); } obj->num_is_new = true; }
The disp.c module handles user number input and rounds calculation results in a string representation. This module contains the following functions:
- display_reset() function resets the display to the initial state (0).
- display_set_number() function is used to set a number to the display after the computations in the model. The number is rounded and converted to a string.
- display_get_number() function converts a string number on the display to a double value.
- display_enter_key() and display_erase() functions are used to edit a number in the display.
- display_negate() function adds or removes a negative sign on the non-zero number on the display. If the number is zero, this function does nothing.
- display_set_str() function is used in special cases to set an error message to the display of the calculator. After this function, the display must be reset to work properly.
// Data structure of the display object typedef struct { char str[DISP_STR_SIZE]; // String buffer of the display int len; // Length of the string in the buffer int dig_count; // Count of decimal digits in the string bool is_neg; // Is number in the string negative bool has_dec_point; // Does number in the string contain decimal point notify_cb change_cb; // Callback to be invoked on display string change void *change_cb_data; // Data pointer for the callback char dec_point_char; // Current decimal point character used by the display } display; // Reset display to "0" void display_reset(display *obj); // Set new decimal pointer character to use void display_set_dec_point_char(display *obj, char new_dec_point_char); // Set double value number to the display result_type display_set_number(display *obj, double value); // Get number from display as double value double display_get_number(display *obj); // Process number key category to enter new characters to display result_type display_enter_key(display *obj, key_type key, bool reset); // Erase last entered character result_type display_erase(display *obj); // Negate number on display void display_negate(display *obj); // Set string message to display void display_set_str(display *obj, const char *new_str);