252 lines
11 KiB
C
252 lines
11 KiB
C
/*
|
|
* This file is part of the zh_menu component, which is a derivative work
|
|
* based on the original project "menu_manager" by Marcio Bulla, available at:
|
|
* https://github.com/MarcioBulla/menu_manager
|
|
* The original project is distributed under the Apache License, Version 2.0.
|
|
* The original code was adapted, modified and extended for use with ESP-IDF
|
|
* as the zh_menu component.
|
|
*/
|
|
|
|
#include "zh_menu.h"
|
|
|
|
static const char *TAG = "zh_menu";
|
|
|
|
#define ZH_LOGI(msg, ...) ESP_LOGI(TAG, msg, ##__VA_ARGS__)
|
|
#define ZH_LOGE(msg, err, ...) ESP_LOGE(TAG, "[%s:%d:%s] " msg, __FILE__, __LINE__, esp_err_to_name(err), ##__VA_ARGS__)
|
|
|
|
#define ZH_ERROR_CHECK(cond, err, cleanup, msg, ...) \
|
|
if (!(cond)) \
|
|
{ \
|
|
ZH_LOGE(msg, err, ##__VA_ARGS__); \
|
|
cleanup; \
|
|
return err; \
|
|
}
|
|
|
|
#define ZH_MENU_MAX_DEPTH 8 /*!< Maximum depth of nested submenus. Limits the navigation stack size */
|
|
|
|
/**
|
|
* @brief Internal representation of the menu handle.
|
|
*
|
|
* Stores the stack of traversed levels, the current and root nodes,
|
|
* user callbacks, state flags and the current index of the highlighted
|
|
* item.
|
|
*
|
|
* @note The menu_stack is used to return to the previous level on the ZH_MENU_BACK command.
|
|
*
|
|
* @warning When ZH_MENU_MAX_DEPTH is reached, further descent is not performed to avoid stack overflow.
|
|
*/
|
|
struct _zh_menu_handle_t
|
|
{
|
|
zh_menu_node_t *menu_stack[ZH_MENU_MAX_DEPTH]; /*!< Stack of traversed menu levels */
|
|
zh_menu_node_t *current_menu; /*!< Current active menu level */
|
|
zh_menu_node_t *root; /*!< Root node of the menu tree */
|
|
void (*change_cb)(zh_menu_node_t *current_path, uint8_t current_index); /*!< Callback for the current position change */
|
|
void (*enter_cb)(void *args); /*!< Callback for entering the menu */
|
|
void (*exit_cb)(void *args); /*!< Callback for exiting the menu */
|
|
bool loop; /*!< Cyclic navigation flag */
|
|
uint8_t menu_depth; /*!< Current nesting depth */
|
|
uint8_t current_index; /*!< Index of the highlighted item on the current level */
|
|
bool is_activated; /*!< Menu activity flag */
|
|
bool is_selected; /*!< Selection state flag of the current item */
|
|
};
|
|
|
|
esp_err_t zh_menu_init(const zh_menu_init_config_t *config, zh_menu_handle_t **handle)
|
|
{
|
|
ZH_LOGI("Menu initialization started.");
|
|
ZH_ERROR_CHECK(config != NULL && config->root != NULL && handle != NULL && config->root->submenu != NULL && config->root->item_numbers != 0, ESP_ERR_INVALID_ARG, NULL, "Menu initialization failed. Invalid argument.");
|
|
ZH_ERROR_CHECK(*handle == NULL, ESP_ERR_INVALID_STATE, NULL, "Menu initialization failed. Menu is already initialized.");
|
|
*handle = heap_caps_calloc(1, sizeof(zh_menu_handle_t), MALLOC_CAP_8BIT);
|
|
ZH_ERROR_CHECK(*handle != NULL, ESP_ERR_NO_MEM, NULL, "Menu initialization failed. Failed to allocate menu handle.");
|
|
(*handle)->change_cb = config->change_cb;
|
|
(*handle)->enter_cb = config->enter_cb;
|
|
(*handle)->exit_cb = config->exit_cb;
|
|
(*handle)->current_menu = config->root;
|
|
(*handle)->root = config->root;
|
|
(*handle)->loop = config->loop;
|
|
ZH_LOGI("Menu initialization completed successfully.");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t zh_menu_deinit(zh_menu_handle_t **handle)
|
|
{
|
|
ZH_LOGI("Menu deinitialization started.");
|
|
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu deinitialization failed. Invalid argument.");
|
|
heap_caps_free(*handle);
|
|
*handle = NULL;
|
|
ZH_LOGI("Menu deinitialization completed successfully.");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t zh_menu_set(zh_menu_handle_t **handle, zh_menu_navigate_t command, void *arg)
|
|
{
|
|
ZH_LOGI("Menu set position started.");
|
|
ZH_ERROR_CHECK(handle != NULL && *handle != NULL && command >= ZH_MENU_ENTER && command < ZH_MENU_MAX, ESP_ERR_INVALID_ARG, NULL, "Menu set position failed. Invalid argument.");
|
|
switch (command)
|
|
{
|
|
case ZH_MENU_ENTER:
|
|
if ((*handle)->is_activated == false)
|
|
{
|
|
(*handle)->is_activated = true;
|
|
(*handle)->is_selected = false;
|
|
(*handle)->current_menu = (*handle)->root;
|
|
(*handle)->current_index = 0;
|
|
(*handle)->menu_depth = 0;
|
|
memset((*handle)->menu_stack, 0, sizeof((*handle)->menu_stack));
|
|
if ((*handle)->enter_cb != NULL)
|
|
{
|
|
(*handle)->enter_cb(arg);
|
|
}
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
}
|
|
break;
|
|
case ZH_MENU_SHOW:
|
|
if ((*handle)->is_activated == true)
|
|
{
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
}
|
|
break;
|
|
case ZH_MENU_UP:
|
|
if ((*handle)->current_menu == NULL || (*handle)->current_menu->item_numbers == 0)
|
|
{
|
|
break;
|
|
}
|
|
if ((*handle)->is_activated == true && (*handle)->is_selected == false)
|
|
{
|
|
if ((*handle)->current_index > 0)
|
|
{
|
|
--(*handle)->current_index;
|
|
}
|
|
else if ((*handle)->loop == true)
|
|
{
|
|
(*handle)->current_index = (*handle)->current_menu->item_numbers - 1;
|
|
}
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
}
|
|
break;
|
|
case ZH_MENU_DOWN:
|
|
if ((*handle)->current_menu == NULL || (*handle)->current_menu->item_numbers == 0)
|
|
{
|
|
break;
|
|
}
|
|
if ((*handle)->is_activated == true && (*handle)->is_selected == false)
|
|
{
|
|
if ((*handle)->current_index < (*handle)->current_menu->item_numbers - 1)
|
|
{
|
|
++(*handle)->current_index;
|
|
}
|
|
else if ((*handle)->loop == true)
|
|
{
|
|
(*handle)->current_index = 0;
|
|
}
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
}
|
|
break;
|
|
case ZH_MENU_SELECT:
|
|
if ((*handle)->is_activated == true)
|
|
{
|
|
if ((*handle)->current_menu->submenu == NULL || (*handle)->current_index >= (*handle)->current_menu->item_numbers)
|
|
{
|
|
break;
|
|
}
|
|
if ((*handle)->is_selected == true)
|
|
{
|
|
if ((*handle)->current_menu->submenu[(*handle)->current_index].item_cb != NULL)
|
|
{
|
|
(*handle)->current_menu->submenu[(*handle)->current_index].item_cb(arg);
|
|
}
|
|
}
|
|
else if ((*handle)->current_menu->submenu[(*handle)->current_index].selectable == true)
|
|
{
|
|
(*handle)->is_selected = true;
|
|
if ((*handle)->current_menu->submenu[(*handle)->current_index].item_cb != NULL)
|
|
{
|
|
(*handle)->current_menu->submenu[(*handle)->current_index].item_cb(arg);
|
|
}
|
|
}
|
|
else if ((*handle)->current_menu->submenu[(*handle)->current_index].item_cb != NULL)
|
|
{
|
|
(*handle)->current_menu->submenu[(*handle)->current_index].item_cb(arg);
|
|
}
|
|
else if ((*handle)->current_menu->submenu[(*handle)->current_index].submenu != NULL && (*handle)->current_menu->submenu[(*handle)->current_index].item_numbers > 0)
|
|
{
|
|
if ((*handle)->menu_depth >= ZH_MENU_MAX_DEPTH)
|
|
{
|
|
break;
|
|
}
|
|
(*handle)->menu_stack[(*handle)->menu_depth] = (*handle)->current_menu;
|
|
++(*handle)->menu_depth;
|
|
(*handle)->current_menu = &(*handle)->current_menu->submenu[(*handle)->current_index];
|
|
(*handle)->current_index = 0;
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case ZH_MENU_BACK:
|
|
if ((*handle)->is_activated == true)
|
|
{
|
|
if ((*handle)->is_selected == true)
|
|
{
|
|
(*handle)->is_selected = false;
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
break;
|
|
}
|
|
if ((*handle)->menu_depth > 0)
|
|
{
|
|
--(*handle)->menu_depth;
|
|
(*handle)->current_menu = (*handle)->menu_stack[(*handle)->menu_depth];
|
|
(*handle)->menu_stack[(*handle)->menu_depth] = NULL;
|
|
(*handle)->current_index = 0;
|
|
if ((*handle)->change_cb != NULL)
|
|
{
|
|
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
(*handle)->is_activated = false;
|
|
(*handle)->is_selected = false;
|
|
(*handle)->current_menu = (*handle)->root;
|
|
(*handle)->current_index = 0;
|
|
(*handle)->menu_depth = 0;
|
|
memset((*handle)->menu_stack, 0, sizeof((*handle)->menu_stack));
|
|
if ((*handle)->exit_cb != NULL)
|
|
{
|
|
(*handle)->exit_cb(arg);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
ZH_LOGI("Menu set position completed successfully.");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t zh_menu_get(zh_menu_handle_t **handle, zh_menu_status_t *status)
|
|
{
|
|
ZH_LOGI("Menu get status started.");
|
|
ZH_ERROR_CHECK(handle != NULL && *handle != NULL && status != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu get status failed. Invalid argument.");
|
|
status->is_active = (*handle)->is_activated;
|
|
status->is_selected = (*handle)->is_selected;
|
|
ZH_LOGI("Menu get status completed successfully.");
|
|
return ESP_OK;
|
|
} |