Calculator Sample Overview

Mobile native

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

Calculator screen

Calculator screen

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

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