[FL-2627] Flipper applications: SDK, build and debug system (#1387)
* Added support for running applications from SD card (FAPs - Flipper Application Packages) * Added plugin_dist target for fbt to build FAPs * All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default * Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them * Added debugging support for FAPs with fbt debug & VSCode * Added public firmware API with automated versioning Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
343
applications/services/gui/modules/button_menu.c
Normal file
343
applications/services/gui/modules/button_menu.c
Normal file
@@ -0,0 +1,343 @@
|
||||
#include "button_menu.h"
|
||||
#include "gui/canvas.h"
|
||||
#include "gui/elements.h"
|
||||
#include "input/input.h"
|
||||
#include <m-array.h>
|
||||
#include <furi.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define ITEM_FIRST_OFFSET 17
|
||||
#define ITEM_NEXT_OFFSET 4
|
||||
#define ITEM_HEIGHT 14
|
||||
#define ITEM_WIDTH 64
|
||||
#define BUTTONS_PER_SCREEN 6
|
||||
|
||||
struct ButtonMenuItem {
|
||||
const char* label;
|
||||
int32_t index;
|
||||
ButtonMenuItemCallback callback;
|
||||
ButtonMenuItemType type;
|
||||
void* callback_context;
|
||||
};
|
||||
|
||||
ARRAY_DEF(ButtonMenuItemArray, ButtonMenuItem, M_POD_OPLIST);
|
||||
|
||||
struct ButtonMenu {
|
||||
View* view;
|
||||
bool freeze_input;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
ButtonMenuItemArray_t items;
|
||||
uint8_t position;
|
||||
const char* header;
|
||||
} ButtonMenuModel;
|
||||
|
||||
static void button_menu_draw_control_button(
|
||||
Canvas* canvas,
|
||||
uint8_t item_position,
|
||||
const char* text,
|
||||
bool selected) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(text);
|
||||
|
||||
uint8_t item_x = 0;
|
||||
uint8_t item_y = ITEM_FIRST_OFFSET + (item_position * (ITEM_HEIGHT + ITEM_NEXT_OFFSET));
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(selected) {
|
||||
elements_slightly_rounded_box(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
item_x + (ITEM_WIDTH / 2),
|
||||
item_y + (ITEM_HEIGHT / 2),
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
text);
|
||||
}
|
||||
|
||||
static void button_menu_draw_common_button(
|
||||
Canvas* canvas,
|
||||
uint8_t item_position,
|
||||
const char* text,
|
||||
bool selected) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(text);
|
||||
|
||||
uint8_t item_x = 0;
|
||||
uint8_t item_y = ITEM_FIRST_OFFSET + (item_position * (ITEM_HEIGHT + ITEM_NEXT_OFFSET));
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(selected) {
|
||||
canvas_draw_rbox(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 5);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
canvas_draw_rframe(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 5);
|
||||
}
|
||||
|
||||
string_t disp_str;
|
||||
string_init_set_str(disp_str, text);
|
||||
elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6);
|
||||
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
item_x + (ITEM_WIDTH / 2),
|
||||
item_y + (ITEM_HEIGHT / 2),
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
string_get_cstr(disp_str));
|
||||
|
||||
string_clear(disp_str);
|
||||
}
|
||||
|
||||
static void button_menu_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(_model);
|
||||
|
||||
ButtonMenuModel* model = (ButtonMenuModel*)_model;
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
uint8_t item_position = 0;
|
||||
int8_t active_screen = model->position / BUTTONS_PER_SCREEN;
|
||||
size_t items_size = ButtonMenuItemArray_size(model->items);
|
||||
int8_t max_screen = ((int16_t)items_size - 1) / BUTTONS_PER_SCREEN;
|
||||
ButtonMenuItemArray_it_t it;
|
||||
|
||||
if(active_screen > 0) {
|
||||
canvas_draw_icon(canvas, 28, 1, &I_InfraredArrowUp_4x8);
|
||||
}
|
||||
|
||||
if(max_screen > active_screen) {
|
||||
canvas_draw_icon(canvas, 28, 123, &I_InfraredArrowDown_4x8);
|
||||
}
|
||||
|
||||
if(model->header) {
|
||||
string_t disp_str;
|
||||
string_init_set_str(disp_str, model->header);
|
||||
elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 10, AlignCenter, AlignCenter, string_get_cstr(disp_str));
|
||||
string_clear(disp_str);
|
||||
}
|
||||
|
||||
for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
|
||||
ButtonMenuItemArray_next(it), ++item_position) {
|
||||
if(active_screen == (item_position / BUTTONS_PER_SCREEN)) {
|
||||
if(ButtonMenuItemArray_cref(it)->type == ButtonMenuItemTypeControl) {
|
||||
button_menu_draw_control_button(
|
||||
canvas,
|
||||
item_position % BUTTONS_PER_SCREEN,
|
||||
ButtonMenuItemArray_cref(it)->label,
|
||||
(item_position == model->position));
|
||||
} else if(ButtonMenuItemArray_cref(it)->type == ButtonMenuItemTypeCommon) {
|
||||
button_menu_draw_common_button(
|
||||
canvas,
|
||||
item_position % BUTTONS_PER_SCREEN,
|
||||
ButtonMenuItemArray_cref(it)->label,
|
||||
(item_position == model->position));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void button_menu_process_up(ButtonMenu* button_menu) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
if(model->position > 0) {
|
||||
model->position--;
|
||||
} else {
|
||||
model->position = ButtonMenuItemArray_size(model->items) - 1;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void button_menu_process_down(ButtonMenu* button_menu) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
if(model->position < (ButtonMenuItemArray_size(model->items) - 1)) {
|
||||
model->position++;
|
||||
} else {
|
||||
model->position = 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void button_menu_process_ok(ButtonMenu* button_menu, InputType type) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
ButtonMenuItem* item = NULL;
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
if(model->position < (ButtonMenuItemArray_size(model->items))) {
|
||||
item = ButtonMenuItemArray_get(model->items, model->position);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if(item) {
|
||||
if(item->type == ButtonMenuItemTypeControl) {
|
||||
if(type == InputTypeShort) {
|
||||
if(item && item->callback) {
|
||||
item->callback(item->callback_context, item->index, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(item->type == ButtonMenuItemTypeCommon) {
|
||||
if((type == InputTypePress) || (type == InputTypeRelease)) {
|
||||
if(item && item->callback) {
|
||||
item->callback(item->callback_context, item->index, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool button_menu_view_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(event);
|
||||
|
||||
ButtonMenu* button_menu = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->key == InputKeyOk) {
|
||||
if((event->type == InputTypeRelease) || (event->type == InputTypePress)) {
|
||||
consumed = true;
|
||||
button_menu->freeze_input = (event->type == InputTypePress);
|
||||
button_menu_process_ok(button_menu, event->type);
|
||||
} else if(event->type == InputTypeShort) {
|
||||
consumed = true;
|
||||
button_menu_process_ok(button_menu, event->type);
|
||||
}
|
||||
}
|
||||
|
||||
if(!button_menu->freeze_input &&
|
||||
((event->type == InputTypeRepeat) || (event->type == InputTypeShort))) {
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
consumed = true;
|
||||
button_menu_process_up(button_menu);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
consumed = true;
|
||||
button_menu_process_down(button_menu);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
View* button_menu_get_view(ButtonMenu* button_menu) {
|
||||
furi_assert(button_menu);
|
||||
return button_menu->view;
|
||||
}
|
||||
|
||||
void button_menu_reset(ButtonMenu* button_menu) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
ButtonMenuItemArray_reset(model->items);
|
||||
model->position = 0;
|
||||
model->header = NULL;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void button_menu_set_header(ButtonMenu* button_menu, const char* header) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
model->header = header;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
ButtonMenuItem* button_menu_add_item(
|
||||
ButtonMenu* button_menu,
|
||||
const char* label,
|
||||
int32_t index,
|
||||
ButtonMenuItemCallback callback,
|
||||
ButtonMenuItemType type,
|
||||
void* callback_context) {
|
||||
ButtonMenuItem* item = NULL;
|
||||
furi_assert(label);
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
item = ButtonMenuItemArray_push_new(model->items);
|
||||
item->label = label;
|
||||
item->index = index;
|
||||
item->type = type;
|
||||
item->callback = callback;
|
||||
item->callback_context = callback_context;
|
||||
return true;
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
ButtonMenu* button_menu_alloc(void) {
|
||||
ButtonMenu* button_menu = malloc(sizeof(ButtonMenu));
|
||||
button_menu->view = view_alloc();
|
||||
view_set_orientation(button_menu->view, ViewOrientationVertical);
|
||||
view_set_context(button_menu->view, button_menu);
|
||||
view_allocate_model(button_menu->view, ViewModelTypeLocking, sizeof(ButtonMenuModel));
|
||||
view_set_draw_callback(button_menu->view, button_menu_view_draw_callback);
|
||||
view_set_input_callback(button_menu->view, button_menu_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
ButtonMenuItemArray_init(model->items);
|
||||
model->position = 0;
|
||||
model->header = NULL;
|
||||
return true;
|
||||
});
|
||||
|
||||
button_menu->freeze_input = false;
|
||||
return button_menu;
|
||||
}
|
||||
|
||||
void button_menu_free(ButtonMenu* button_menu) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
ButtonMenuItemArray_clear(model->items);
|
||||
return true;
|
||||
});
|
||||
view_free(button_menu->view);
|
||||
free(button_menu);
|
||||
}
|
||||
|
||||
void button_menu_set_selected_item(ButtonMenu* button_menu, uint32_t index) {
|
||||
furi_assert(button_menu);
|
||||
|
||||
with_view_model(
|
||||
button_menu->view, (ButtonMenuModel * model) {
|
||||
uint8_t item_position = 0;
|
||||
ButtonMenuItemArray_it_t it;
|
||||
for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
|
||||
ButtonMenuItemArray_next(it), ++item_position) {
|
||||
if((uint32_t)ButtonMenuItemArray_cref(it)->index == index) {
|
||||
model->position = item_position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
94
applications/services/gui/modules/button_menu.h
Normal file
94
applications/services/gui/modules/button_menu.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file button_menu.h
|
||||
* GUI: ButtonMenu view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** ButtonMenu anonymous structure */
|
||||
typedef struct ButtonMenu ButtonMenu;
|
||||
|
||||
/** ButtonMenuItem anonymous structure */
|
||||
typedef struct ButtonMenuItem ButtonMenuItem;
|
||||
|
||||
/** Callback for any button menu actions */
|
||||
typedef void (*ButtonMenuItemCallback)(void* context, int32_t index, InputType type);
|
||||
|
||||
/** Type of button. Difference in drawing buttons. */
|
||||
typedef enum {
|
||||
ButtonMenuItemTypeCommon,
|
||||
ButtonMenuItemTypeControl,
|
||||
} ButtonMenuItemType;
|
||||
|
||||
/** Get button menu view
|
||||
*
|
||||
* @param button_menu ButtonMenu instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* button_menu_get_view(ButtonMenu* button_menu);
|
||||
|
||||
/** Clean button menu
|
||||
*
|
||||
* @param button_menu ButtonMenu instance
|
||||
*/
|
||||
void button_menu_reset(ButtonMenu* button_menu);
|
||||
|
||||
/** Add item to button menu instance
|
||||
*
|
||||
* @param button_menu ButtonMenu instance
|
||||
* @param label text inside new button
|
||||
* @param index value to distinct between buttons inside
|
||||
* ButtonMenuItemCallback
|
||||
* @param callback The callback
|
||||
* @param type type of button to create. Differ by button
|
||||
* drawing. Control buttons have no frames, and
|
||||
* have more squared borders.
|
||||
* @param callback_context The callback context
|
||||
*
|
||||
* @return pointer to just-created item
|
||||
*/
|
||||
ButtonMenuItem* button_menu_add_item(
|
||||
ButtonMenu* button_menu,
|
||||
const char* label,
|
||||
int32_t index,
|
||||
ButtonMenuItemCallback callback,
|
||||
ButtonMenuItemType type,
|
||||
void* callback_context);
|
||||
|
||||
/** Allocate and initialize new instance of ButtonMenu model
|
||||
*
|
||||
* @return just-created ButtonMenu model
|
||||
*/
|
||||
ButtonMenu* button_menu_alloc(void);
|
||||
|
||||
/** Free ButtonMenu element
|
||||
*
|
||||
* @param button_menu ButtonMenu instance
|
||||
*/
|
||||
void button_menu_free(ButtonMenu* button_menu);
|
||||
|
||||
/** Set ButtonMenu header on top of canvas
|
||||
*
|
||||
* @param button_menu ButtonMenu instance
|
||||
* @param header header on the top of button menu
|
||||
*/
|
||||
void button_menu_set_header(ButtonMenu* button_menu, const char* header);
|
||||
|
||||
/** Set selected item
|
||||
*
|
||||
* @param button_menu ButtonMenu instance
|
||||
* @param index index of ButtonMenu to be selected
|
||||
*/
|
||||
void button_menu_set_selected_item(ButtonMenu* button_menu, uint32_t index);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
383
applications/services/gui/modules/button_panel.c
Normal file
383
applications/services/gui/modules/button_panel.c
Normal file
@@ -0,0 +1,383 @@
|
||||
#include "button_panel.h"
|
||||
#include "furi_hal_resources.h"
|
||||
#include "gui/canvas.h"
|
||||
#include <m-array.h>
|
||||
#include <m-i-list.h>
|
||||
#include <m-list.h>
|
||||
#include <furi.h>
|
||||
#include <gui/elements.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
// uint16_t to support multi-screen, wide button panel
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
Font font;
|
||||
const char* str;
|
||||
} LabelElement;
|
||||
|
||||
LIST_DEF(LabelList, LabelElement, M_POD_OPLIST)
|
||||
#define M_OPL_LabelList_t() LIST_OPLIST(LabelList)
|
||||
|
||||
typedef struct {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
const Icon* name;
|
||||
const Icon* name_selected;
|
||||
} IconElement;
|
||||
|
||||
typedef struct ButtonItem {
|
||||
uint32_t index;
|
||||
ButtonItemCallback callback;
|
||||
IconElement icon;
|
||||
void* callback_context;
|
||||
} ButtonItem;
|
||||
|
||||
ARRAY_DEF(ButtonArray, ButtonItem*, M_PTR_OPLIST);
|
||||
#define M_OPL_ButtonArray_t() ARRAY_OPLIST(ButtonArray, M_PTR_OPLIST)
|
||||
ARRAY_DEF(ButtonMatrix, ButtonArray_t);
|
||||
#define M_OPL_ButtonMatrix_t() ARRAY_OPLIST(ButtonMatrix, M_OPL_ButtonArray_t())
|
||||
|
||||
struct ButtonPanel {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
ButtonMatrix_t button_matrix;
|
||||
LabelList_t labels;
|
||||
uint16_t reserve_x;
|
||||
uint16_t reserve_y;
|
||||
uint16_t selected_item_x;
|
||||
uint16_t selected_item_y;
|
||||
} ButtonPanelModel;
|
||||
|
||||
static ButtonItem** button_panel_get_item(ButtonPanelModel* model, size_t x, size_t y);
|
||||
static void button_panel_process_up(ButtonPanel* button_panel);
|
||||
static void button_panel_process_down(ButtonPanel* button_panel);
|
||||
static void button_panel_process_left(ButtonPanel* button_panel);
|
||||
static void button_panel_process_right(ButtonPanel* button_panel);
|
||||
static void button_panel_process_ok(ButtonPanel* button_panel);
|
||||
static void button_panel_view_draw_callback(Canvas* canvas, void* _model);
|
||||
static bool button_panel_view_input_callback(InputEvent* event, void* context);
|
||||
|
||||
ButtonPanel* button_panel_alloc() {
|
||||
ButtonPanel* button_panel = malloc(sizeof(ButtonPanel));
|
||||
button_panel->view = view_alloc();
|
||||
view_set_orientation(button_panel->view, ViewOrientationVertical);
|
||||
view_set_context(button_panel->view, button_panel);
|
||||
view_allocate_model(button_panel->view, ViewModelTypeLocking, sizeof(ButtonPanelModel));
|
||||
view_set_draw_callback(button_panel->view, button_panel_view_draw_callback);
|
||||
view_set_input_callback(button_panel->view, button_panel_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
model->reserve_x = 0;
|
||||
model->reserve_y = 0;
|
||||
model->selected_item_x = 0;
|
||||
model->selected_item_y = 0;
|
||||
ButtonMatrix_init(model->button_matrix);
|
||||
LabelList_init(model->labels);
|
||||
return true;
|
||||
});
|
||||
|
||||
return button_panel;
|
||||
}
|
||||
|
||||
void button_panel_reserve(ButtonPanel* button_panel, size_t reserve_x, size_t reserve_y) {
|
||||
furi_check(reserve_x > 0);
|
||||
furi_check(reserve_y > 0);
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
model->reserve_x = reserve_x;
|
||||
model->reserve_y = reserve_y;
|
||||
ButtonMatrix_reserve(model->button_matrix, model->reserve_y);
|
||||
for(size_t i = 0; i > model->reserve_y; ++i) {
|
||||
ButtonArray_t* array = ButtonMatrix_get(model->button_matrix, i);
|
||||
ButtonArray_init(*array);
|
||||
ButtonArray_reserve(*array, reserve_x);
|
||||
// TODO: do we need to clear allocated memory of ptr-s to ButtonItem ??
|
||||
}
|
||||
LabelList_init(model->labels);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void button_panel_free(ButtonPanel* button_panel) {
|
||||
furi_assert(button_panel);
|
||||
|
||||
button_panel_reset(button_panel);
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
LabelList_clear(model->labels);
|
||||
ButtonMatrix_clear(model->button_matrix);
|
||||
return true;
|
||||
});
|
||||
|
||||
view_free(button_panel->view);
|
||||
free(button_panel);
|
||||
}
|
||||
|
||||
void button_panel_reset(ButtonPanel* button_panel) {
|
||||
furi_assert(button_panel);
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
for(size_t x = 0; x < model->reserve_x; ++x) {
|
||||
for(size_t y = 0; y < model->reserve_y; ++y) {
|
||||
ButtonItem** button_item = button_panel_get_item(model, x, y);
|
||||
free(*button_item);
|
||||
*button_item = NULL;
|
||||
}
|
||||
}
|
||||
model->reserve_x = 0;
|
||||
model->reserve_y = 0;
|
||||
LabelList_reset(model->labels);
|
||||
ButtonMatrix_reset(model->button_matrix);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static ButtonItem** button_panel_get_item(ButtonPanelModel* model, size_t x, size_t y) {
|
||||
furi_assert(model);
|
||||
|
||||
furi_check(x < model->reserve_x);
|
||||
furi_check(y < model->reserve_y);
|
||||
ButtonArray_t* button_array = ButtonMatrix_safe_get(model->button_matrix, x);
|
||||
ButtonItem** button_item = ButtonArray_safe_get(*button_array, y);
|
||||
return button_item;
|
||||
}
|
||||
|
||||
void button_panel_add_item(
|
||||
ButtonPanel* button_panel,
|
||||
uint32_t index,
|
||||
uint16_t matrix_place_x,
|
||||
uint16_t matrix_place_y,
|
||||
uint16_t x,
|
||||
uint16_t y,
|
||||
const Icon* icon_name,
|
||||
const Icon* icon_name_selected,
|
||||
ButtonItemCallback callback,
|
||||
void* callback_context) {
|
||||
furi_assert(button_panel);
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
ButtonItem** button_item_ptr =
|
||||
button_panel_get_item(model, matrix_place_x, matrix_place_y);
|
||||
furi_check(*button_item_ptr == NULL);
|
||||
*button_item_ptr = malloc(sizeof(ButtonItem));
|
||||
ButtonItem* button_item = *button_item_ptr;
|
||||
button_item->callback = callback;
|
||||
button_item->callback_context = callback_context;
|
||||
button_item->icon.x = x;
|
||||
button_item->icon.y = y;
|
||||
button_item->icon.name = icon_name;
|
||||
button_item->icon.name_selected = icon_name_selected;
|
||||
button_item->index = index;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
View* button_panel_get_view(ButtonPanel* button_panel) {
|
||||
furi_assert(button_panel);
|
||||
return button_panel->view;
|
||||
}
|
||||
|
||||
static void button_panel_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(_model);
|
||||
|
||||
ButtonPanelModel* model = _model;
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
for(size_t x = 0; x < model->reserve_x; ++x) {
|
||||
for(size_t y = 0; y < model->reserve_y; ++y) {
|
||||
ButtonItem* button_item = *button_panel_get_item(model, x, y);
|
||||
const Icon* icon_name = button_item->icon.name;
|
||||
if((model->selected_item_x == x) && (model->selected_item_y == y)) {
|
||||
icon_name = button_item->icon.name_selected;
|
||||
}
|
||||
canvas_draw_icon(canvas, button_item->icon.x, button_item->icon.y, icon_name);
|
||||
}
|
||||
}
|
||||
|
||||
for
|
||||
M_EACH(label, model->labels, LabelList_t) {
|
||||
canvas_set_font(canvas, label->font);
|
||||
canvas_draw_str(canvas, label->x, label->y, label->str);
|
||||
}
|
||||
}
|
||||
|
||||
static void button_panel_process_down(ButtonPanel* button_panel) {
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
uint16_t new_selected_item_x = model->selected_item_x;
|
||||
uint16_t new_selected_item_y = model->selected_item_y;
|
||||
size_t i;
|
||||
|
||||
if(new_selected_item_y >= (model->reserve_y - 1)) return false;
|
||||
|
||||
++new_selected_item_y;
|
||||
|
||||
for(i = 0; i < model->reserve_x; ++i) {
|
||||
new_selected_item_x = (model->selected_item_x + i) % model->reserve_x;
|
||||
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i == model->reserve_x) return false;
|
||||
|
||||
model->selected_item_x = new_selected_item_x;
|
||||
model->selected_item_y = new_selected_item_y;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void button_panel_process_up(ButtonPanel* button_panel) {
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
size_t new_selected_item_x = model->selected_item_x;
|
||||
size_t new_selected_item_y = model->selected_item_y;
|
||||
size_t i;
|
||||
|
||||
if(new_selected_item_y <= 0) return false;
|
||||
|
||||
--new_selected_item_y;
|
||||
|
||||
for(i = 0; i < model->reserve_x; ++i) {
|
||||
new_selected_item_x = (model->selected_item_x + i) % model->reserve_x;
|
||||
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i == model->reserve_x) return false;
|
||||
|
||||
model->selected_item_x = new_selected_item_x;
|
||||
model->selected_item_y = new_selected_item_y;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void button_panel_process_left(ButtonPanel* button_panel) {
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
size_t new_selected_item_x = model->selected_item_x;
|
||||
size_t new_selected_item_y = model->selected_item_y;
|
||||
size_t i;
|
||||
|
||||
if(new_selected_item_x <= 0) return false;
|
||||
|
||||
--new_selected_item_x;
|
||||
|
||||
for(i = 0; i < model->reserve_y; ++i) {
|
||||
new_selected_item_y = (model->selected_item_y + i) % model->reserve_y;
|
||||
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i == model->reserve_y) return false;
|
||||
|
||||
model->selected_item_x = new_selected_item_x;
|
||||
model->selected_item_y = new_selected_item_y;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void button_panel_process_right(ButtonPanel* button_panel) {
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
uint16_t new_selected_item_x = model->selected_item_x;
|
||||
uint16_t new_selected_item_y = model->selected_item_y;
|
||||
size_t i;
|
||||
|
||||
if(new_selected_item_x >= (model->reserve_x - 1)) return false;
|
||||
|
||||
++new_selected_item_x;
|
||||
|
||||
for(i = 0; i < model->reserve_y; ++i) {
|
||||
new_selected_item_y = (model->selected_item_y + i) % model->reserve_y;
|
||||
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i == model->reserve_y) return false;
|
||||
|
||||
model->selected_item_x = new_selected_item_x;
|
||||
model->selected_item_y = new_selected_item_y;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void button_panel_process_ok(ButtonPanel* button_panel) {
|
||||
ButtonItem* button_item = NULL;
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
button_item =
|
||||
*button_panel_get_item(model, model->selected_item_x, model->selected_item_y);
|
||||
return true;
|
||||
});
|
||||
|
||||
if(button_item && button_item->callback) {
|
||||
button_item->callback(button_item->callback_context, button_item->index);
|
||||
}
|
||||
}
|
||||
|
||||
static bool button_panel_view_input_callback(InputEvent* event, void* context) {
|
||||
ButtonPanel* button_panel = context;
|
||||
furi_assert(button_panel);
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeShort) {
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
consumed = true;
|
||||
button_panel_process_up(button_panel);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
consumed = true;
|
||||
button_panel_process_down(button_panel);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
consumed = true;
|
||||
button_panel_process_left(button_panel);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
consumed = true;
|
||||
button_panel_process_right(button_panel);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
consumed = true;
|
||||
button_panel_process_ok(button_panel);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void button_panel_add_label(
|
||||
ButtonPanel* button_panel,
|
||||
uint16_t x,
|
||||
uint16_t y,
|
||||
Font font,
|
||||
const char* label_str) {
|
||||
furi_assert(button_panel);
|
||||
|
||||
with_view_model(
|
||||
button_panel->view, (ButtonPanelModel * model) {
|
||||
LabelElement* label = LabelList_push_raw(model->labels);
|
||||
label->x = x;
|
||||
label->y = y;
|
||||
label->font = font;
|
||||
label->str = label_str;
|
||||
return true;
|
||||
});
|
||||
}
|
105
applications/services/gui/modules/button_panel.h
Normal file
105
applications/services/gui/modules/button_panel.h
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @file button_panel.h
|
||||
* GUI: ButtonPanel view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Button panel module descriptor */
|
||||
typedef struct ButtonPanel ButtonPanel;
|
||||
|
||||
/** Callback type to call for handling selecting button_panel items */
|
||||
typedef void (*ButtonItemCallback)(void* context, uint32_t index);
|
||||
|
||||
/** Allocate new button_panel module.
|
||||
*
|
||||
* @return ButtonPanel instance
|
||||
*/
|
||||
ButtonPanel* button_panel_alloc(void);
|
||||
|
||||
/** Free button_panel module.
|
||||
*
|
||||
* @param button_panel ButtonPanel instance
|
||||
*/
|
||||
void button_panel_free(ButtonPanel* button_panel);
|
||||
|
||||
/** Free items from button_panel module. Preallocated matrix stays unchanged.
|
||||
*
|
||||
* @param button_panel ButtonPanel instance
|
||||
*/
|
||||
void button_panel_reset(ButtonPanel* button_panel);
|
||||
|
||||
/** Reserve space for adding items.
|
||||
*
|
||||
* One does not simply use button_panel_add_item() without this function. It
|
||||
* should be allocated space for it first.
|
||||
*
|
||||
* @param button_panel ButtonPanel instance
|
||||
* @param reserve_x number of columns in button_panel
|
||||
* @param reserve_y number of rows in button_panel
|
||||
*/
|
||||
void button_panel_reserve(ButtonPanel* button_panel, size_t reserve_x, size_t reserve_y);
|
||||
|
||||
/** Add item to button_panel module.
|
||||
*
|
||||
* Have to set element in bounds of allocated size by X and by Y.
|
||||
*
|
||||
* @param button_panel ButtonPanel instance
|
||||
* @param index value to pass to callback
|
||||
* @param matrix_place_x coordinates by x-axis on virtual grid, it
|
||||
* is only used for naviagation
|
||||
* @param matrix_place_y coordinates by y-axis on virtual grid, it
|
||||
* is only used for naviagation
|
||||
* @param x x-coordinate to draw icon on
|
||||
* @param y y-coordinate to draw icon on
|
||||
* @param icon_name name of the icon to draw
|
||||
* @param icon_name_selected name of the icon to draw when current
|
||||
* element is selected
|
||||
* @param callback function to call when specific element is
|
||||
* selected (pressed Ok on selected item)
|
||||
* @param callback_context context to pass to callback
|
||||
*/
|
||||
void button_panel_add_item(
|
||||
ButtonPanel* button_panel,
|
||||
uint32_t index,
|
||||
uint16_t matrix_place_x,
|
||||
uint16_t matrix_place_y,
|
||||
uint16_t x,
|
||||
uint16_t y,
|
||||
const Icon* icon_name,
|
||||
const Icon* icon_name_selected,
|
||||
ButtonItemCallback callback,
|
||||
void* callback_context);
|
||||
|
||||
/** Get button_panel view.
|
||||
*
|
||||
* @param button_panel ButtonPanel instance
|
||||
*
|
||||
* @return acquired view
|
||||
*/
|
||||
View* button_panel_get_view(ButtonPanel* button_panel);
|
||||
|
||||
/** Add label to button_panel module.
|
||||
*
|
||||
* @param button_panel ButtonPanel instance
|
||||
* @param x x-coordinate to place label
|
||||
* @param y y-coordinate to place label
|
||||
* @param font font to write label with
|
||||
* @param label_str string label to write
|
||||
*/
|
||||
void button_panel_add_label(
|
||||
ButtonPanel* button_panel,
|
||||
uint16_t x,
|
||||
uint16_t y,
|
||||
Font font,
|
||||
const char* label_str);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
781
applications/services/gui/modules/byte_input.c
Normal file
781
applications/services/gui/modules/byte_input.c
Normal file
@@ -0,0 +1,781 @@
|
||||
#include "byte_input.h"
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
struct ByteInput {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const uint8_t value;
|
||||
const uint8_t x;
|
||||
const uint8_t y;
|
||||
} ByteInputKey;
|
||||
|
||||
typedef struct {
|
||||
const char* header;
|
||||
uint8_t* bytes;
|
||||
uint8_t bytes_count;
|
||||
|
||||
ByteInputCallback input_callback;
|
||||
ByteChangedCallback changed_callback;
|
||||
void* callback_context;
|
||||
|
||||
bool selected_high_nibble;
|
||||
uint8_t selected_byte;
|
||||
int8_t selected_row; // row -1 - input, row 0 & 1 - keyboard
|
||||
uint8_t selected_column;
|
||||
uint8_t first_visible_byte;
|
||||
} ByteInputModel;
|
||||
|
||||
static const uint8_t keyboard_origin_x = 7;
|
||||
static const uint8_t keyboard_origin_y = 31;
|
||||
static const uint8_t keyboard_row_count = 2;
|
||||
static const uint8_t enter_symbol = '\r';
|
||||
static const uint8_t backspace_symbol = '\b';
|
||||
static const uint8_t max_drawable_bytes = 8;
|
||||
|
||||
static const ByteInputKey keyboard_keys_row_1[] = {
|
||||
{'0', 0, 12},
|
||||
{'1', 11, 12},
|
||||
{'2', 22, 12},
|
||||
{'3', 33, 12},
|
||||
{'4', 44, 12},
|
||||
{'5', 55, 12},
|
||||
{'6', 66, 12},
|
||||
{'7', 77, 12},
|
||||
{backspace_symbol, 103, 4},
|
||||
};
|
||||
|
||||
static const ByteInputKey keyboard_keys_row_2[] = {
|
||||
{'8', 0, 26},
|
||||
{'9', 11, 26},
|
||||
{'A', 22, 26},
|
||||
{'B', 33, 26},
|
||||
{'C', 44, 26},
|
||||
{'D', 55, 26},
|
||||
{'E', 66, 26},
|
||||
{'F', 77, 26},
|
||||
{enter_symbol, 95, 17},
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Get row size
|
||||
*
|
||||
* @param row_index Index of row
|
||||
* @return uint8_t Row size
|
||||
*/
|
||||
static uint8_t byte_input_get_row_size(uint8_t row_index) {
|
||||
uint8_t row_size = 0;
|
||||
|
||||
switch(row_index + 1) {
|
||||
case 1:
|
||||
row_size = sizeof(keyboard_keys_row_1) / sizeof(ByteInputKey);
|
||||
break;
|
||||
case 2:
|
||||
row_size = sizeof(keyboard_keys_row_2) / sizeof(ByteInputKey);
|
||||
break;
|
||||
}
|
||||
|
||||
return row_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get row pointer
|
||||
*
|
||||
* @param row_index Index of row
|
||||
* @return const ByteInputKey* Row pointer
|
||||
*/
|
||||
static const ByteInputKey* byte_input_get_row(uint8_t row_index) {
|
||||
const ByteInputKey* row = NULL;
|
||||
|
||||
switch(row_index + 1) {
|
||||
case 1:
|
||||
row = keyboard_keys_row_1;
|
||||
break;
|
||||
case 2:
|
||||
row = keyboard_keys_row_2;
|
||||
break;
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get text from nibble
|
||||
*
|
||||
* @param byte byte value
|
||||
* @param high_nibble Get from high nibble, otherwise low nibble
|
||||
* @return char nibble text
|
||||
*/
|
||||
static char byte_input_get_nibble_text(uint8_t byte, bool high_nibble) {
|
||||
if(high_nibble) {
|
||||
byte = byte >> 4;
|
||||
}
|
||||
byte = byte & 0x0F;
|
||||
|
||||
switch(byte & 0x0F) {
|
||||
case 0x0:
|
||||
case 0x1:
|
||||
case 0x2:
|
||||
case 0x3:
|
||||
case 0x4:
|
||||
case 0x5:
|
||||
case 0x6:
|
||||
case 0x7:
|
||||
case 0x8:
|
||||
case 0x9:
|
||||
byte = byte + '0';
|
||||
break;
|
||||
case 0xA:
|
||||
case 0xB:
|
||||
case 0xC:
|
||||
case 0xD:
|
||||
case 0xE:
|
||||
case 0xF:
|
||||
byte = byte - 0xA + 'A';
|
||||
break;
|
||||
default:
|
||||
byte = '!';
|
||||
break;
|
||||
}
|
||||
|
||||
return byte;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw input box (common view)
|
||||
*
|
||||
* @param canvas
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_draw_input(Canvas* canvas, ByteInputModel* model) {
|
||||
const uint8_t text_x = 8;
|
||||
const uint8_t text_y = 25;
|
||||
|
||||
elements_slightly_rounded_frame(canvas, 6, 14, 116, 15);
|
||||
|
||||
canvas_draw_icon(canvas, 2, 19, &I_ButtonLeftSmall_3x5);
|
||||
canvas_draw_icon(canvas, 123, 19, &I_ButtonRightSmall_3x5);
|
||||
|
||||
for(uint8_t i = model->first_visible_byte;
|
||||
i < model->first_visible_byte + MIN(model->bytes_count, max_drawable_bytes);
|
||||
i++) {
|
||||
uint8_t byte_position = i - model->first_visible_byte;
|
||||
|
||||
if(i == model->selected_byte) {
|
||||
canvas_draw_frame(canvas, text_x + byte_position * 14, text_y - 9, 15, 11);
|
||||
|
||||
if(model->selected_high_nibble) {
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 8 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], false));
|
||||
canvas_draw_box(canvas, text_x + 1 + byte_position * 14, text_y - 8, 7, 9);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_line(
|
||||
canvas,
|
||||
text_x + 14 + byte_position * 14,
|
||||
text_y - 6,
|
||||
text_x + 14 + byte_position * 14,
|
||||
text_y - 2);
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 2 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], true));
|
||||
canvas_invert_color(canvas);
|
||||
} else {
|
||||
canvas_draw_box(canvas, text_x + 7 + byte_position * 14, text_y - 8, 7, 9);
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 2 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], true));
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_line(
|
||||
canvas,
|
||||
text_x + byte_position * 14,
|
||||
text_y - 6,
|
||||
text_x + byte_position * 14,
|
||||
text_y - 2);
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 8 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], false));
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
} else {
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 2 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], true));
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 8 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], false));
|
||||
}
|
||||
}
|
||||
|
||||
if(model->bytes_count - model->first_visible_byte > max_drawable_bytes) {
|
||||
canvas_draw_icon(canvas, 123, 21, &I_ButtonRightSmall_3x5);
|
||||
}
|
||||
|
||||
if(model->first_visible_byte > 0) {
|
||||
canvas_draw_icon(canvas, 1, 21, &I_ButtonLeftSmall_3x5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw input box (selected view)
|
||||
*
|
||||
* @param canvas
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_draw_input_selected(Canvas* canvas, ByteInputModel* model) {
|
||||
const uint8_t text_x = 7;
|
||||
const uint8_t text_y = 25;
|
||||
|
||||
canvas_draw_box(canvas, 0, 12, 127, 19);
|
||||
canvas_invert_color(canvas);
|
||||
|
||||
elements_slightly_rounded_frame(canvas, 6, 14, 115, 15);
|
||||
canvas_draw_icon(canvas, 2, 19, &I_ButtonLeftSmall_3x5);
|
||||
canvas_draw_icon(canvas, 122, 19, &I_ButtonRightSmall_3x5);
|
||||
|
||||
for(uint8_t i = model->first_visible_byte;
|
||||
i < model->first_visible_byte + MIN(model->bytes_count, max_drawable_bytes);
|
||||
i++) {
|
||||
uint8_t byte_position = i - model->first_visible_byte;
|
||||
|
||||
if(i == model->selected_byte) {
|
||||
canvas_draw_box(canvas, text_x + 1 + byte_position * 14, text_y - 9, 13, 11);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 2 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], true));
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 8 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], false));
|
||||
canvas_invert_color(canvas);
|
||||
} else {
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 2 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], true));
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
text_x + 8 + byte_position * 14,
|
||||
text_y,
|
||||
byte_input_get_nibble_text(model->bytes[i], false));
|
||||
}
|
||||
}
|
||||
|
||||
if(model->bytes_count - model->first_visible_byte > max_drawable_bytes) {
|
||||
canvas_draw_icon(canvas, 123, 21, &I_ButtonRightSmall_3x5);
|
||||
}
|
||||
|
||||
if(model->first_visible_byte > 0) {
|
||||
canvas_draw_icon(canvas, 1, 21, &I_ButtonLeftSmall_3x5);
|
||||
}
|
||||
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set nibble at position
|
||||
*
|
||||
* @param data where to set nibble
|
||||
* @param position byte position
|
||||
* @param value char value
|
||||
* @param high_nibble set high nibble
|
||||
*/
|
||||
static void byte_input_set_nibble(uint8_t* data, uint8_t position, char value, bool high_nibble) {
|
||||
switch(value) {
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
value = value - '0';
|
||||
break;
|
||||
case 'A':
|
||||
case 'B':
|
||||
case 'C':
|
||||
case 'D':
|
||||
case 'E':
|
||||
case 'F':
|
||||
value = value - 'A' + 10;
|
||||
break;
|
||||
default:
|
||||
value = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if(high_nibble) {
|
||||
data[position] &= 0x0F;
|
||||
data[position] |= value << 4;
|
||||
} else {
|
||||
data[position] &= 0xF0;
|
||||
data[position] |= value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief What currently selected
|
||||
*
|
||||
* @return true - keyboard selected, false - input selected
|
||||
*/
|
||||
static bool byte_input_keyboard_selected(ByteInputModel* model) {
|
||||
return model->selected_row >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Do transition from keyboard
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_transition_from_keyboard(ByteInputModel* model) {
|
||||
model->selected_row += 1;
|
||||
model->selected_high_nibble = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Increase selected byte position
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_inc_selected_byte(ByteInputModel* model) {
|
||||
if(model->selected_byte < model->bytes_count - 1) {
|
||||
model->selected_byte += 1;
|
||||
|
||||
if(model->bytes_count > max_drawable_bytes) {
|
||||
if(model->selected_byte - model->first_visible_byte > (max_drawable_bytes - 2)) {
|
||||
if(model->first_visible_byte < model->bytes_count - max_drawable_bytes) {
|
||||
model->first_visible_byte++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decrease selected byte position
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_dec_selected_byte(ByteInputModel* model) {
|
||||
if(model->selected_byte > 0) {
|
||||
model->selected_byte -= 1;
|
||||
|
||||
if(model->selected_byte - model->first_visible_byte < 1) {
|
||||
if(model->first_visible_byte > 0) {
|
||||
model->first_visible_byte--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Call input callback
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_call_input_callback(ByteInputModel* model) {
|
||||
if(model->input_callback != NULL) {
|
||||
model->input_callback(model->callback_context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Call changed callback
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_call_changed_callback(ByteInputModel* model) {
|
||||
if(model->changed_callback != NULL) {
|
||||
model->changed_callback(model->callback_context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear selected byte
|
||||
*/
|
||||
|
||||
static void byte_input_clear_selected_byte(ByteInputModel* model) {
|
||||
model->bytes[model->selected_byte] = 0;
|
||||
model->selected_high_nibble = true;
|
||||
byte_input_dec_selected_byte(model);
|
||||
byte_input_call_changed_callback(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle up button
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_handle_up(ByteInputModel* model) {
|
||||
if(model->selected_row > -1) {
|
||||
model->selected_row -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle down button
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_handle_down(ByteInputModel* model) {
|
||||
if(byte_input_keyboard_selected(model)) {
|
||||
if(model->selected_row < keyboard_row_count - 1) {
|
||||
model->selected_row += 1;
|
||||
}
|
||||
} else {
|
||||
byte_input_transition_from_keyboard(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle left button
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_handle_left(ByteInputModel* model) {
|
||||
if(byte_input_keyboard_selected(model)) {
|
||||
if(model->selected_column > 0) {
|
||||
model->selected_column -= 1;
|
||||
} else {
|
||||
model->selected_column = byte_input_get_row_size(model->selected_row) - 1;
|
||||
}
|
||||
} else {
|
||||
byte_input_dec_selected_byte(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle right button
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_handle_right(ByteInputModel* model) {
|
||||
if(byte_input_keyboard_selected(model)) {
|
||||
if(model->selected_column < byte_input_get_row_size(model->selected_row) - 1) {
|
||||
model->selected_column += 1;
|
||||
} else {
|
||||
model->selected_column = 0;
|
||||
}
|
||||
} else {
|
||||
byte_input_inc_selected_byte(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle OK button
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
static void byte_input_handle_ok(ByteInputModel* model) {
|
||||
if(byte_input_keyboard_selected(model)) {
|
||||
uint8_t value = byte_input_get_row(model->selected_row)[model->selected_column].value;
|
||||
|
||||
if(value == enter_symbol) {
|
||||
byte_input_call_input_callback(model);
|
||||
} else if(value == backspace_symbol) {
|
||||
byte_input_clear_selected_byte(model);
|
||||
} else {
|
||||
byte_input_set_nibble(
|
||||
model->bytes, model->selected_byte, value, model->selected_high_nibble);
|
||||
if(model->selected_high_nibble == true) {
|
||||
model->selected_high_nibble = false;
|
||||
} else {
|
||||
byte_input_inc_selected_byte(model);
|
||||
model->selected_high_nibble = true;
|
||||
}
|
||||
byte_input_call_changed_callback(model);
|
||||
}
|
||||
} else {
|
||||
byte_input_transition_from_keyboard(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw callback
|
||||
*
|
||||
* @param canvas
|
||||
* @param _model
|
||||
*/
|
||||
static void byte_input_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
ByteInputModel* model = _model;
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
canvas_draw_str(canvas, 2, 9, model->header);
|
||||
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
|
||||
if(model->selected_row == -1) {
|
||||
byte_input_draw_input_selected(canvas, model);
|
||||
} else {
|
||||
byte_input_draw_input(canvas, model);
|
||||
}
|
||||
|
||||
for(uint8_t row = 0; row < keyboard_row_count; row++) {
|
||||
const uint8_t column_count = byte_input_get_row_size(row);
|
||||
const ByteInputKey* keys = byte_input_get_row(row);
|
||||
|
||||
for(size_t column = 0; column < column_count; column++) {
|
||||
if(keys[column].value == enter_symbol) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySaveSelected_24x11);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySave_24x11);
|
||||
}
|
||||
} else if(keys[column].value == backspace_symbol) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeyBackspaceSelected_16x9);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeyBackspace_16x9);
|
||||
}
|
||||
} else {
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x - 3,
|
||||
keyboard_origin_y + keys[column].y - 10,
|
||||
11,
|
||||
13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else if(model->selected_row == -1 && row == 0 && model->selected_column == column) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_frame(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x - 3,
|
||||
keyboard_origin_y + keys[column].y - 10,
|
||||
11,
|
||||
13);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
keys[column].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Input callback
|
||||
*
|
||||
* @param event
|
||||
* @param context
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
static bool byte_input_view_input_callback(InputEvent* event, void* context) {
|
||||
ByteInput* byte_input = context;
|
||||
furi_assert(byte_input);
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
|
||||
switch(event->key) {
|
||||
case InputKeyLeft:
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_handle_left(model);
|
||||
return true;
|
||||
});
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_handle_right(model);
|
||||
return true;
|
||||
});
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyUp:
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_handle_up(model);
|
||||
return true;
|
||||
});
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_handle_down(model);
|
||||
return true;
|
||||
});
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyOk:
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_handle_ok(model);
|
||||
return true;
|
||||
});
|
||||
consumed = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if((event->type == InputTypeLong || event->type == InputTypeRepeat) &&
|
||||
event->key == InputKeyBack) {
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_clear_selected_byte(model);
|
||||
return true;
|
||||
});
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset all input-related data in model
|
||||
*
|
||||
* @param model ByteInputModel
|
||||
*/
|
||||
static void byte_input_reset_model_input_data(ByteInputModel* model) {
|
||||
model->bytes = NULL;
|
||||
model->bytes_count = 0;
|
||||
model->selected_high_nibble = true;
|
||||
model->selected_byte = 0;
|
||||
model->selected_row = 0;
|
||||
model->selected_column = 0;
|
||||
model->first_visible_byte = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Allocate and initialize byte input. This byte input is used to enter bytes.
|
||||
*
|
||||
* @return ByteInput instance pointer
|
||||
*/
|
||||
ByteInput* byte_input_alloc() {
|
||||
ByteInput* byte_input = malloc(sizeof(ByteInput));
|
||||
byte_input->view = view_alloc();
|
||||
view_set_context(byte_input->view, byte_input);
|
||||
view_allocate_model(byte_input->view, ViewModelTypeLocking, sizeof(ByteInputModel));
|
||||
view_set_draw_callback(byte_input->view, byte_input_view_draw_callback);
|
||||
view_set_input_callback(byte_input->view, byte_input_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
model->header = "";
|
||||
model->input_callback = NULL;
|
||||
model->changed_callback = NULL;
|
||||
model->callback_context = NULL;
|
||||
byte_input_reset_model_input_data(model);
|
||||
return true;
|
||||
});
|
||||
|
||||
return byte_input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Deinitialize and free byte input
|
||||
*
|
||||
* @param byte_input Byte input instance
|
||||
*/
|
||||
void byte_input_free(ByteInput* byte_input) {
|
||||
furi_assert(byte_input);
|
||||
view_free(byte_input->view);
|
||||
free(byte_input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get byte input view
|
||||
*
|
||||
* @param byte_input byte input instance
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* byte_input_get_view(ByteInput* byte_input) {
|
||||
furi_assert(byte_input);
|
||||
return byte_input->view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Deinitialize and free byte input
|
||||
*
|
||||
* @param byte_input byte input instance
|
||||
* @param input_callback input callback fn
|
||||
* @param changed_callback changed callback fn
|
||||
* @param callback_context callback context
|
||||
* @param bytes buffer to use
|
||||
* @param bytes_count buffer length
|
||||
*/
|
||||
void byte_input_set_result_callback(
|
||||
ByteInput* byte_input,
|
||||
ByteInputCallback input_callback,
|
||||
ByteChangedCallback changed_callback,
|
||||
void* callback_context,
|
||||
uint8_t* bytes,
|
||||
uint8_t bytes_count) {
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
byte_input_reset_model_input_data(model);
|
||||
model->input_callback = input_callback;
|
||||
model->changed_callback = changed_callback;
|
||||
model->callback_context = callback_context;
|
||||
model->bytes = bytes;
|
||||
model->bytes_count = bytes_count;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set byte input header text
|
||||
*
|
||||
* @param byte_input byte input instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void byte_input_set_header_text(ByteInput* byte_input, const char* text) {
|
||||
with_view_model(
|
||||
byte_input->view, (ByteInputModel * model) {
|
||||
model->header = text;
|
||||
return true;
|
||||
});
|
||||
}
|
69
applications/services/gui/modules/byte_input.h
Normal file
69
applications/services/gui/modules/byte_input.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file byte_input.h
|
||||
* GUI: ByteInput keyboard view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Byte input anonymous structure */
|
||||
typedef struct ByteInput ByteInput;
|
||||
|
||||
/** callback that is executed on save button press */
|
||||
typedef void (*ByteInputCallback)(void* context);
|
||||
|
||||
/** callback that is executed when byte buffer is changed */
|
||||
typedef void (*ByteChangedCallback)(void* context);
|
||||
|
||||
/** Allocate and initialize byte input. This byte input is used to enter bytes.
|
||||
*
|
||||
* @return ByteInput instance pointer
|
||||
*/
|
||||
ByteInput* byte_input_alloc();
|
||||
|
||||
/** Deinitialize and free byte input
|
||||
*
|
||||
* @param byte_input Byte input instance
|
||||
*/
|
||||
void byte_input_free(ByteInput* byte_input);
|
||||
|
||||
/** Get byte input view
|
||||
*
|
||||
* @param byte_input byte input instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* byte_input_get_view(ByteInput* byte_input);
|
||||
|
||||
/** Set byte input result callback
|
||||
*
|
||||
* @param byte_input byte input instance
|
||||
* @param input_callback input callback fn
|
||||
* @param changed_callback changed callback fn
|
||||
* @param callback_context callback context
|
||||
* @param bytes buffer to use
|
||||
* @param bytes_count buffer length
|
||||
*/
|
||||
void byte_input_set_result_callback(
|
||||
ByteInput* byte_input,
|
||||
ByteInputCallback input_callback,
|
||||
ByteChangedCallback changed_callback,
|
||||
void* callback_context,
|
||||
uint8_t* bytes,
|
||||
uint8_t bytes_count);
|
||||
|
||||
/** Set byte input header text
|
||||
*
|
||||
* @param byte_input byte input instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void byte_input_set_header_text(ByteInput* byte_input, const char* text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
303
applications/services/gui/modules/dialog_ex.c
Normal file
303
applications/services/gui/modules/dialog_ex.c
Normal file
@@ -0,0 +1,303 @@
|
||||
#include "dialog_ex.h"
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
struct DialogEx {
|
||||
View* view;
|
||||
void* context;
|
||||
DialogExResultCallback callback;
|
||||
bool enable_extended_events;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* text;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
Align horizontal;
|
||||
Align vertical;
|
||||
} TextElement;
|
||||
|
||||
typedef struct {
|
||||
int8_t x;
|
||||
int8_t y;
|
||||
const Icon* icon;
|
||||
} IconElement;
|
||||
|
||||
typedef struct {
|
||||
TextElement header;
|
||||
TextElement text;
|
||||
IconElement icon;
|
||||
|
||||
const char* left_text;
|
||||
const char* center_text;
|
||||
const char* right_text;
|
||||
} DialogExModel;
|
||||
|
||||
static void dialog_ex_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
DialogExModel* model = _model;
|
||||
|
||||
// Prepare canvas
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(model->icon.icon != NULL) {
|
||||
canvas_draw_icon(canvas, model->icon.x, model->icon.y, model->icon.icon);
|
||||
}
|
||||
|
||||
// Draw header
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
if(model->header.text != NULL) {
|
||||
elements_multiline_text_aligned(
|
||||
canvas,
|
||||
model->header.x,
|
||||
model->header.y,
|
||||
model->header.horizontal,
|
||||
model->header.vertical,
|
||||
model->header.text);
|
||||
}
|
||||
|
||||
// Draw text
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
if(model->text.text != NULL) {
|
||||
elements_multiline_text_aligned(
|
||||
canvas,
|
||||
model->text.x,
|
||||
model->text.y,
|
||||
model->text.horizontal,
|
||||
model->text.vertical,
|
||||
model->text.text);
|
||||
}
|
||||
|
||||
// Draw buttons
|
||||
if(model->left_text != NULL) {
|
||||
elements_button_left(canvas, model->left_text);
|
||||
}
|
||||
|
||||
if(model->center_text != NULL) {
|
||||
elements_button_center(canvas, model->center_text);
|
||||
}
|
||||
|
||||
if(model->right_text != NULL) {
|
||||
elements_button_right(canvas, model->right_text);
|
||||
}
|
||||
}
|
||||
|
||||
static bool dialog_ex_view_input_callback(InputEvent* event, void* context) {
|
||||
DialogEx* dialog_ex = context;
|
||||
bool consumed = false;
|
||||
const char* left_text = NULL;
|
||||
const char* center_text = NULL;
|
||||
const char* right_text = NULL;
|
||||
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
left_text = model->left_text;
|
||||
center_text = model->center_text;
|
||||
right_text = model->right_text;
|
||||
return true;
|
||||
});
|
||||
|
||||
if(dialog_ex->callback) {
|
||||
if(event->type == InputTypeShort) {
|
||||
if(event->key == InputKeyLeft && left_text != NULL) {
|
||||
dialog_ex->callback(DialogExResultLeft, dialog_ex->context);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyOk && center_text != NULL) {
|
||||
dialog_ex->callback(DialogExResultCenter, dialog_ex->context);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyRight && right_text != NULL) {
|
||||
dialog_ex->callback(DialogExResultRight, dialog_ex->context);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(event->type == InputTypePress && dialog_ex->enable_extended_events) {
|
||||
if(event->key == InputKeyLeft && left_text != NULL) {
|
||||
dialog_ex->callback(DialogExPressLeft, dialog_ex->context);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyOk && center_text != NULL) {
|
||||
dialog_ex->callback(DialogExPressCenter, dialog_ex->context);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyRight && right_text != NULL) {
|
||||
dialog_ex->callback(DialogExPressRight, dialog_ex->context);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(event->type == InputTypeRelease && dialog_ex->enable_extended_events) {
|
||||
if(event->key == InputKeyLeft && left_text != NULL) {
|
||||
dialog_ex->callback(DialogExReleaseLeft, dialog_ex->context);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyOk && center_text != NULL) {
|
||||
dialog_ex->callback(DialogExReleaseCenter, dialog_ex->context);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyRight && right_text != NULL) {
|
||||
dialog_ex->callback(DialogExReleaseRight, dialog_ex->context);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
DialogEx* dialog_ex_alloc() {
|
||||
DialogEx* dialog_ex = malloc(sizeof(DialogEx));
|
||||
dialog_ex->view = view_alloc();
|
||||
view_set_context(dialog_ex->view, dialog_ex);
|
||||
view_allocate_model(dialog_ex->view, ViewModelTypeLockFree, sizeof(DialogExModel));
|
||||
view_set_draw_callback(dialog_ex->view, dialog_ex_view_draw_callback);
|
||||
view_set_input_callback(dialog_ex->view, dialog_ex_view_input_callback);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->header.text = NULL;
|
||||
model->header.x = 0;
|
||||
model->header.y = 0;
|
||||
model->header.horizontal = AlignLeft;
|
||||
model->header.vertical = AlignBottom;
|
||||
|
||||
model->text.text = NULL;
|
||||
model->text.x = 0;
|
||||
model->text.y = 0;
|
||||
model->text.horizontal = AlignLeft;
|
||||
model->text.vertical = AlignBottom;
|
||||
|
||||
model->icon.x = 0;
|
||||
model->icon.y = 0;
|
||||
model->icon.icon = NULL;
|
||||
|
||||
model->left_text = NULL;
|
||||
model->center_text = NULL;
|
||||
model->right_text = NULL;
|
||||
|
||||
return true;
|
||||
});
|
||||
dialog_ex->enable_extended_events = false;
|
||||
return dialog_ex;
|
||||
}
|
||||
|
||||
void dialog_ex_free(DialogEx* dialog_ex) {
|
||||
furi_assert(dialog_ex);
|
||||
view_free(dialog_ex->view);
|
||||
free(dialog_ex);
|
||||
}
|
||||
|
||||
View* dialog_ex_get_view(DialogEx* dialog_ex) {
|
||||
furi_assert(dialog_ex);
|
||||
return dialog_ex->view;
|
||||
}
|
||||
|
||||
void dialog_ex_set_result_callback(DialogEx* dialog_ex, DialogExResultCallback callback) {
|
||||
furi_assert(dialog_ex);
|
||||
dialog_ex->callback = callback;
|
||||
}
|
||||
|
||||
void dialog_ex_set_context(DialogEx* dialog_ex, void* context) {
|
||||
furi_assert(dialog_ex);
|
||||
dialog_ex->context = context;
|
||||
}
|
||||
|
||||
void dialog_ex_set_header(
|
||||
DialogEx* dialog_ex,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical) {
|
||||
furi_assert(dialog_ex);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->header.text = text;
|
||||
model->header.x = x;
|
||||
model->header.y = y;
|
||||
model->header.horizontal = horizontal;
|
||||
model->header.vertical = vertical;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void dialog_ex_set_text(
|
||||
DialogEx* dialog_ex,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical) {
|
||||
furi_assert(dialog_ex);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->text.text = text;
|
||||
model->text.x = x;
|
||||
model->text.y = y;
|
||||
model->text.horizontal = horizontal;
|
||||
model->text.vertical = vertical;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void dialog_ex_set_icon(DialogEx* dialog_ex, uint8_t x, uint8_t y, const Icon* icon) {
|
||||
furi_assert(dialog_ex);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->icon.x = x;
|
||||
model->icon.y = y;
|
||||
model->icon.icon = icon;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void dialog_ex_set_left_button_text(DialogEx* dialog_ex, const char* text) {
|
||||
furi_assert(dialog_ex);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->left_text = text;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void dialog_ex_set_center_button_text(DialogEx* dialog_ex, const char* text) {
|
||||
furi_assert(dialog_ex);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->center_text = text;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void dialog_ex_set_right_button_text(DialogEx* dialog_ex, const char* text) {
|
||||
furi_assert(dialog_ex);
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->right_text = text;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void dialog_ex_reset(DialogEx* dialog_ex) {
|
||||
furi_assert(dialog_ex);
|
||||
TextElement clean_text_el = {
|
||||
.text = NULL, .x = 0, .y = 0, .horizontal = AlignLeft, .vertical = AlignLeft};
|
||||
IconElement clean_icon_el = {.icon = NULL, .x = 0, .y = 0};
|
||||
with_view_model(
|
||||
dialog_ex->view, (DialogExModel * model) {
|
||||
model->header = clean_text_el;
|
||||
model->text = clean_text_el;
|
||||
model->icon = clean_icon_el;
|
||||
model->left_text = NULL;
|
||||
model->center_text = NULL;
|
||||
model->right_text = NULL;
|
||||
return true;
|
||||
});
|
||||
dialog_ex->context = NULL;
|
||||
dialog_ex->callback = NULL;
|
||||
}
|
||||
|
||||
void dialog_ex_enable_extended_events(DialogEx* dialog_ex) {
|
||||
furi_assert(dialog_ex);
|
||||
dialog_ex->enable_extended_events = true;
|
||||
}
|
||||
|
||||
void dialog_ex_disable_extended_events(DialogEx* dialog_ex) {
|
||||
furi_assert(dialog_ex);
|
||||
dialog_ex->enable_extended_events = false;
|
||||
}
|
168
applications/services/gui/modules/dialog_ex.h
Normal file
168
applications/services/gui/modules/dialog_ex.h
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @file dialog_ex.h
|
||||
* GUI: DialogEx view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Dialog anonymous structure */
|
||||
typedef struct DialogEx DialogEx;
|
||||
|
||||
/** DialogEx result */
|
||||
typedef enum {
|
||||
DialogExResultLeft,
|
||||
DialogExResultCenter,
|
||||
DialogExResultRight,
|
||||
DialogExPressLeft,
|
||||
DialogExPressCenter,
|
||||
DialogExPressRight,
|
||||
DialogExReleaseLeft,
|
||||
DialogExReleaseCenter,
|
||||
DialogExReleaseRight,
|
||||
} DialogExResult;
|
||||
|
||||
/** DialogEx result callback type
|
||||
* @warning comes from GUI thread
|
||||
*/
|
||||
typedef void (*DialogExResultCallback)(DialogExResult result, void* context);
|
||||
|
||||
/** Allocate and initialize dialog
|
||||
*
|
||||
* This dialog used to ask simple questions
|
||||
*
|
||||
* @return DialogEx instance
|
||||
*/
|
||||
DialogEx* dialog_ex_alloc();
|
||||
|
||||
/** Deinitialize and free dialog
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
*/
|
||||
void dialog_ex_free(DialogEx* dialog_ex);
|
||||
|
||||
/** Get dialog view
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* dialog_ex_get_view(DialogEx* dialog_ex);
|
||||
|
||||
/** Set dialog result callback
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param callback result callback function
|
||||
*/
|
||||
void dialog_ex_set_result_callback(DialogEx* dialog_ex, DialogExResultCallback callback);
|
||||
|
||||
/** Set dialog context
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param context context pointer, will be passed to result callback
|
||||
*/
|
||||
void dialog_ex_set_context(DialogEx* dialog_ex, void* context);
|
||||
|
||||
/** Set dialog header text
|
||||
*
|
||||
* If text is null, dialog header will not be rendered
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param text text to be shown, can be multiline
|
||||
* @param x x position
|
||||
* @param y y position
|
||||
* @param horizontal horizontal text aligment
|
||||
* @param vertical vertical text aligment
|
||||
*/
|
||||
void dialog_ex_set_header(
|
||||
DialogEx* dialog_ex,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical);
|
||||
|
||||
/** Set dialog text
|
||||
*
|
||||
* If text is null, dialog text will not be rendered
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param text text to be shown, can be multiline
|
||||
* @param x x position
|
||||
* @param y y position
|
||||
* @param horizontal horizontal text aligment
|
||||
* @param vertical vertical text aligment
|
||||
*/
|
||||
void dialog_ex_set_text(
|
||||
DialogEx* dialog_ex,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical);
|
||||
|
||||
/** Set dialog icon
|
||||
*
|
||||
* If x or y is negative, dialog icon will not be rendered
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param x x position
|
||||
* @param y y position
|
||||
* @param icon The icon
|
||||
* @param name icon to be shown
|
||||
*/
|
||||
void dialog_ex_set_icon(DialogEx* dialog_ex, uint8_t x, uint8_t y, const Icon* icon);
|
||||
|
||||
/** Set left button text
|
||||
*
|
||||
* If text is null, left button will not be rendered and processed
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void dialog_ex_set_left_button_text(DialogEx* dialog_ex, const char* text);
|
||||
|
||||
/** Set center button text
|
||||
*
|
||||
* If text is null, center button will not be rendered and processed
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void dialog_ex_set_center_button_text(DialogEx* dialog_ex, const char* text);
|
||||
|
||||
/** Set right button text
|
||||
*
|
||||
* If text is null, right button will not be rendered and processed
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void dialog_ex_set_right_button_text(DialogEx* dialog_ex, const char* text);
|
||||
|
||||
/** Clean dialog
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
*/
|
||||
void dialog_ex_reset(DialogEx* dialog_ex);
|
||||
|
||||
/** Enable press/release events
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
*/
|
||||
void dialog_ex_enable_extended_events(DialogEx* dialog_ex);
|
||||
|
||||
/** Disable press/release events
|
||||
*
|
||||
* @param dialog_ex DialogEx instance
|
||||
*/
|
||||
void dialog_ex_disable_extended_events(DialogEx* dialog_ex);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
37
applications/services/gui/modules/empty_screen.c
Normal file
37
applications/services/gui/modules/empty_screen.c
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "empty_screen.h"
|
||||
#include <furi.h>
|
||||
|
||||
struct EmptyScreen {
|
||||
View* view;
|
||||
};
|
||||
|
||||
static void empty_screen_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
UNUSED(_model);
|
||||
canvas_clear(canvas);
|
||||
}
|
||||
|
||||
static bool empty_screen_view_input_callback(InputEvent* event, void* context) {
|
||||
UNUSED(event);
|
||||
UNUSED(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
EmptyScreen* empty_screen_alloc() {
|
||||
EmptyScreen* empty_screen = malloc(sizeof(EmptyScreen));
|
||||
empty_screen->view = view_alloc();
|
||||
view_set_context(empty_screen->view, empty_screen);
|
||||
view_set_draw_callback(empty_screen->view, empty_screen_view_draw_callback);
|
||||
view_set_input_callback(empty_screen->view, empty_screen_view_input_callback);
|
||||
return empty_screen;
|
||||
}
|
||||
|
||||
void empty_screen_free(EmptyScreen* empty_screen) {
|
||||
furi_assert(empty_screen);
|
||||
view_free(empty_screen->view);
|
||||
free(empty_screen);
|
||||
}
|
||||
|
||||
View* empty_screen_get_view(EmptyScreen* empty_screen) {
|
||||
furi_assert(empty_screen);
|
||||
return empty_screen->view;
|
||||
}
|
41
applications/services/gui/modules/empty_screen.h
Normal file
41
applications/services/gui/modules/empty_screen.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file empty_screen.h
|
||||
* GUI: EmptyScreen view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Empty screen anonymous structure */
|
||||
typedef struct EmptyScreen EmptyScreen;
|
||||
|
||||
/** Allocate and initialize empty screen
|
||||
*
|
||||
* This empty screen used to ask simple questions like Yes/
|
||||
*
|
||||
* @return EmptyScreen instance
|
||||
*/
|
||||
EmptyScreen* empty_screen_alloc();
|
||||
|
||||
/** Deinitialize and free empty screen
|
||||
*
|
||||
* @param empty_screen Empty screen instance
|
||||
*/
|
||||
void empty_screen_free(EmptyScreen* empty_screen);
|
||||
|
||||
/** Get empty screen view
|
||||
*
|
||||
* @param empty_screen Empty screen instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* empty_screen_get_view(EmptyScreen* empty_screen);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
595
applications/services/gui/modules/file_browser.c
Normal file
595
applications/services/gui/modules/file_browser.c
Normal file
@@ -0,0 +1,595 @@
|
||||
#include "file_browser.h"
|
||||
#include "assets_icons.h"
|
||||
#include "file_browser_worker.h"
|
||||
#include <core/check.h>
|
||||
#include <core/common_defines.h>
|
||||
#include <core/log.h>
|
||||
#include "furi_hal_resources.h"
|
||||
#include "m-string.h"
|
||||
#include <m-array.h>
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
#include "toolbox/path.h"
|
||||
|
||||
#define LIST_ITEMS 5u
|
||||
#define MAX_LEN_PX 110
|
||||
#define FRAME_HEIGHT 12
|
||||
#define Y_OFFSET 3
|
||||
|
||||
#define ITEM_LIST_LEN_MAX 50
|
||||
|
||||
#define CUSTOM_ICON_MAX_SIZE 32
|
||||
|
||||
typedef enum {
|
||||
BrowserItemTypeLoading,
|
||||
BrowserItemTypeBack,
|
||||
BrowserItemTypeFolder,
|
||||
BrowserItemTypeFile,
|
||||
} BrowserItemType;
|
||||
|
||||
typedef struct {
|
||||
string_t path;
|
||||
BrowserItemType type;
|
||||
uint8_t* custom_icon_data;
|
||||
string_t display_name;
|
||||
} BrowserItem_t;
|
||||
|
||||
static void BrowserItem_t_init(BrowserItem_t* obj) {
|
||||
obj->type = BrowserItemTypeLoading;
|
||||
string_init(obj->path);
|
||||
string_init(obj->display_name);
|
||||
obj->custom_icon_data = NULL;
|
||||
}
|
||||
|
||||
static void BrowserItem_t_init_set(BrowserItem_t* obj, const BrowserItem_t* src) {
|
||||
obj->type = src->type;
|
||||
string_init_set(obj->path, src->path);
|
||||
string_init_set(obj->display_name, src->display_name);
|
||||
if(src->custom_icon_data) {
|
||||
obj->custom_icon_data = malloc(CUSTOM_ICON_MAX_SIZE);
|
||||
memcpy(obj->custom_icon_data, src->custom_icon_data, CUSTOM_ICON_MAX_SIZE);
|
||||
} else {
|
||||
obj->custom_icon_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void BrowserItem_t_set(BrowserItem_t* obj, const BrowserItem_t* src) {
|
||||
obj->type = src->type;
|
||||
string_set(obj->path, src->path);
|
||||
string_set(obj->display_name, src->display_name);
|
||||
if(src->custom_icon_data) {
|
||||
obj->custom_icon_data = malloc(CUSTOM_ICON_MAX_SIZE);
|
||||
memcpy(obj->custom_icon_data, src->custom_icon_data, CUSTOM_ICON_MAX_SIZE);
|
||||
} else {
|
||||
obj->custom_icon_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void BrowserItem_t_clear(BrowserItem_t* obj) {
|
||||
string_clear(obj->path);
|
||||
string_clear(obj->display_name);
|
||||
if(obj->custom_icon_data) {
|
||||
free(obj->custom_icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
ARRAY_DEF(
|
||||
items_array,
|
||||
BrowserItem_t,
|
||||
(INIT(API_2(BrowserItem_t_init)),
|
||||
SET(API_6(BrowserItem_t_set)),
|
||||
INIT_SET(API_6(BrowserItem_t_init_set)),
|
||||
CLEAR(API_2(BrowserItem_t_clear))))
|
||||
|
||||
struct FileBrowser {
|
||||
View* view;
|
||||
BrowserWorker* worker;
|
||||
const char* ext_filter;
|
||||
bool skip_assets;
|
||||
bool hide_ext;
|
||||
|
||||
FileBrowserCallback callback;
|
||||
void* context;
|
||||
|
||||
FileBrowserLoadItemCallback item_callback;
|
||||
void* item_context;
|
||||
|
||||
string_ptr result_path;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
items_array_t items;
|
||||
|
||||
bool is_root;
|
||||
bool folder_loading;
|
||||
bool list_loading;
|
||||
uint32_t item_cnt;
|
||||
int32_t item_idx;
|
||||
int32_t array_offset;
|
||||
int32_t list_offset;
|
||||
|
||||
const Icon* file_icon;
|
||||
bool hide_ext;
|
||||
} FileBrowserModel;
|
||||
|
||||
static const Icon* BrowserItemIcons[] = {
|
||||
[BrowserItemTypeLoading] = &I_loading_10px,
|
||||
[BrowserItemTypeBack] = &I_back_10px,
|
||||
[BrowserItemTypeFolder] = &I_dir_10px,
|
||||
[BrowserItemTypeFile] = &I_unknown_10px,
|
||||
};
|
||||
|
||||
static void file_browser_view_draw_callback(Canvas* canvas, void* _model);
|
||||
static bool file_browser_view_input_callback(InputEvent* event, void* context);
|
||||
|
||||
static void
|
||||
browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root);
|
||||
static void browser_list_load_cb(void* context, uint32_t list_load_offset);
|
||||
static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last);
|
||||
static void browser_long_load_cb(void* context);
|
||||
|
||||
FileBrowser* file_browser_alloc(string_ptr result_path) {
|
||||
furi_assert(result_path);
|
||||
FileBrowser* browser = malloc(sizeof(FileBrowser));
|
||||
browser->view = view_alloc();
|
||||
view_allocate_model(browser->view, ViewModelTypeLocking, sizeof(FileBrowserModel));
|
||||
view_set_context(browser->view, browser);
|
||||
view_set_draw_callback(browser->view, file_browser_view_draw_callback);
|
||||
view_set_input_callback(browser->view, file_browser_view_input_callback);
|
||||
|
||||
browser->result_path = result_path;
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
items_array_init(model->items);
|
||||
return false;
|
||||
});
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
void file_browser_free(FileBrowser* browser) {
|
||||
furi_assert(browser);
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
items_array_clear(model->items);
|
||||
return false;
|
||||
});
|
||||
|
||||
view_free(browser->view);
|
||||
free(browser);
|
||||
}
|
||||
|
||||
View* file_browser_get_view(FileBrowser* browser) {
|
||||
furi_assert(browser);
|
||||
return browser->view;
|
||||
}
|
||||
|
||||
void file_browser_configure(
|
||||
FileBrowser* browser,
|
||||
const char* extension,
|
||||
bool skip_assets,
|
||||
const Icon* file_icon,
|
||||
bool hide_ext) {
|
||||
furi_assert(browser);
|
||||
|
||||
browser->ext_filter = extension;
|
||||
browser->skip_assets = skip_assets;
|
||||
browser->hide_ext = hide_ext;
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
model->file_icon = file_icon;
|
||||
model->hide_ext = hide_ext;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void file_browser_start(FileBrowser* browser, string_t path) {
|
||||
furi_assert(browser);
|
||||
browser->worker = file_browser_worker_alloc(path, browser->ext_filter, browser->skip_assets);
|
||||
file_browser_worker_set_callback_context(browser->worker, browser);
|
||||
file_browser_worker_set_folder_callback(browser->worker, browser_folder_open_cb);
|
||||
file_browser_worker_set_list_callback(browser->worker, browser_list_load_cb);
|
||||
file_browser_worker_set_item_callback(browser->worker, browser_list_item_cb);
|
||||
file_browser_worker_set_long_load_callback(browser->worker, browser_long_load_cb);
|
||||
}
|
||||
|
||||
void file_browser_stop(FileBrowser* browser) {
|
||||
furi_assert(browser);
|
||||
file_browser_worker_free(browser->worker);
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
items_array_reset(model->items);
|
||||
model->item_cnt = 0;
|
||||
model->item_idx = 0;
|
||||
model->array_offset = 0;
|
||||
model->list_offset = 0;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context) {
|
||||
browser->context = context;
|
||||
browser->callback = callback;
|
||||
}
|
||||
|
||||
void file_browser_set_item_callback(
|
||||
FileBrowser* browser,
|
||||
FileBrowserLoadItemCallback callback,
|
||||
void* context) {
|
||||
browser->item_context = context;
|
||||
browser->item_callback = callback;
|
||||
}
|
||||
|
||||
static bool browser_is_item_in_array(FileBrowserModel* model, uint32_t idx) {
|
||||
size_t array_size = items_array_size(model->items);
|
||||
|
||||
if((idx >= (uint32_t)model->array_offset + array_size) ||
|
||||
(idx < (uint32_t)model->array_offset)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool browser_is_list_load_required(FileBrowserModel* model) {
|
||||
size_t array_size = items_array_size(model->items);
|
||||
uint32_t item_cnt = (model->is_root) ? model->item_cnt : model->item_cnt - 1;
|
||||
|
||||
if((model->list_loading) || (array_size >= item_cnt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if((model->array_offset > 0) &&
|
||||
(model->item_idx < (model->array_offset + ITEM_LIST_LEN_MAX / 4))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(((model->array_offset + array_size) < item_cnt) &&
|
||||
(model->item_idx > (int32_t)(model->array_offset + array_size - ITEM_LIST_LEN_MAX / 4))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void browser_update_offset(FileBrowser* browser) {
|
||||
furi_assert(browser);
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
uint16_t bounds = model->item_cnt > (LIST_ITEMS - 1) ? 2 : model->item_cnt;
|
||||
|
||||
if((model->item_cnt > (LIST_ITEMS - 1)) &&
|
||||
(model->item_idx >= ((int32_t)model->item_cnt - 1))) {
|
||||
model->list_offset = model->item_idx - (LIST_ITEMS - 1);
|
||||
} else if(model->list_offset < model->item_idx - bounds) {
|
||||
model->list_offset = CLAMP(
|
||||
model->item_idx - (int32_t)(LIST_ITEMS - 2),
|
||||
(int32_t)model->item_cnt - bounds,
|
||||
0);
|
||||
} else if(model->list_offset > model->item_idx - bounds) {
|
||||
model->list_offset =
|
||||
CLAMP(model->item_idx - 1, (int32_t)model->item_cnt - bounds, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
static void
|
||||
browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root) {
|
||||
furi_assert(context);
|
||||
FileBrowser* browser = (FileBrowser*)context;
|
||||
|
||||
int32_t load_offset = 0;
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
items_array_reset(model->items);
|
||||
if(is_root) {
|
||||
model->item_cnt = item_cnt;
|
||||
model->item_idx = (file_idx > 0) ? file_idx : 0;
|
||||
load_offset =
|
||||
CLAMP(model->item_idx - ITEM_LIST_LEN_MAX / 2, (int32_t)model->item_cnt, 0);
|
||||
} else {
|
||||
model->item_cnt = item_cnt + 1;
|
||||
model->item_idx = file_idx + 1;
|
||||
load_offset = CLAMP(
|
||||
model->item_idx - ITEM_LIST_LEN_MAX / 2 - 1, (int32_t)model->item_cnt - 1, 0);
|
||||
}
|
||||
model->array_offset = 0;
|
||||
model->list_offset = 0;
|
||||
model->is_root = is_root;
|
||||
model->list_loading = true;
|
||||
model->folder_loading = false;
|
||||
return true;
|
||||
});
|
||||
browser_update_offset(browser);
|
||||
|
||||
file_browser_worker_load(browser->worker, load_offset, ITEM_LIST_LEN_MAX);
|
||||
}
|
||||
|
||||
static void browser_list_load_cb(void* context, uint32_t list_load_offset) {
|
||||
furi_assert(context);
|
||||
FileBrowser* browser = (FileBrowser*)context;
|
||||
|
||||
BrowserItem_t back_item;
|
||||
BrowserItem_t_init(&back_item);
|
||||
back_item.type = BrowserItemTypeBack;
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
items_array_reset(model->items);
|
||||
model->array_offset = list_load_offset;
|
||||
if(!model->is_root) {
|
||||
if(list_load_offset == 0) {
|
||||
items_array_push_back(model->items, back_item);
|
||||
} else {
|
||||
model->array_offset += 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
BrowserItem_t_clear(&back_item);
|
||||
}
|
||||
|
||||
static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last) {
|
||||
furi_assert(context);
|
||||
FileBrowser* browser = (FileBrowser*)context;
|
||||
|
||||
BrowserItem_t item;
|
||||
item.custom_icon_data = NULL;
|
||||
|
||||
if(!is_last) {
|
||||
string_init_set(item.path, item_path);
|
||||
string_init(item.display_name);
|
||||
if(is_folder) {
|
||||
item.type = BrowserItemTypeFolder;
|
||||
} else {
|
||||
item.type = BrowserItemTypeFile;
|
||||
if(browser->item_callback) {
|
||||
item.custom_icon_data = malloc(CUSTOM_ICON_MAX_SIZE);
|
||||
if(!browser->item_callback(
|
||||
item_path,
|
||||
browser->item_context,
|
||||
&item.custom_icon_data,
|
||||
item.display_name)) {
|
||||
free(item.custom_icon_data);
|
||||
item.custom_icon_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(string_empty_p(item.display_name)) {
|
||||
path_extract_filename(
|
||||
item_path,
|
||||
item.display_name,
|
||||
(browser->hide_ext) && (item.type == BrowserItemTypeFile));
|
||||
}
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
items_array_push_back(model->items, item);
|
||||
// TODO: calculate if element is visible
|
||||
return true;
|
||||
});
|
||||
string_clear(item.display_name);
|
||||
string_clear(item.path);
|
||||
} else {
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
model->list_loading = false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void browser_long_load_cb(void* context) {
|
||||
furi_assert(context);
|
||||
FileBrowser* browser = (FileBrowser*)context;
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
model->folder_loading = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void browser_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(
|
||||
canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT, (scrollbar ? 122 : 127), FRAME_HEIGHT);
|
||||
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_dot(canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT);
|
||||
canvas_draw_dot(canvas, 1, Y_OFFSET + idx * FRAME_HEIGHT);
|
||||
canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + 1);
|
||||
|
||||
canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1));
|
||||
canvas_draw_dot(canvas, scrollbar ? 121 : 126, Y_OFFSET + idx * FRAME_HEIGHT);
|
||||
canvas_draw_dot(
|
||||
canvas, scrollbar ? 121 : 126, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1));
|
||||
}
|
||||
|
||||
static void browser_draw_loading(Canvas* canvas, FileBrowserModel* model) {
|
||||
UNUSED(model);
|
||||
|
||||
uint8_t x = 128 / 2 - 24 / 2;
|
||||
uint8_t y = 64 / 2 - 24 / 2;
|
||||
|
||||
canvas_draw_icon(canvas, x, y, &A_Loading_24);
|
||||
}
|
||||
|
||||
static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) {
|
||||
uint32_t array_size = items_array_size(model->items);
|
||||
bool show_scrollbar = model->item_cnt > LIST_ITEMS;
|
||||
|
||||
string_t filename;
|
||||
string_init(filename);
|
||||
|
||||
for(uint32_t i = 0; i < MIN(model->item_cnt, LIST_ITEMS); i++) {
|
||||
int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u);
|
||||
|
||||
BrowserItemType item_type = BrowserItemTypeLoading;
|
||||
uint8_t* custom_icon_data = NULL;
|
||||
|
||||
if(browser_is_item_in_array(model, idx)) {
|
||||
BrowserItem_t* item = items_array_get(
|
||||
model->items, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0));
|
||||
item_type = item->type;
|
||||
string_set(filename, item->display_name);
|
||||
if(item_type == BrowserItemTypeFile) {
|
||||
custom_icon_data = item->custom_icon_data;
|
||||
}
|
||||
} else {
|
||||
string_set_str(filename, "---");
|
||||
}
|
||||
|
||||
if(item_type == BrowserItemTypeBack) {
|
||||
string_set_str(filename, ". .");
|
||||
}
|
||||
|
||||
elements_string_fit_width(
|
||||
canvas, filename, (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX));
|
||||
|
||||
if(model->item_idx == idx) {
|
||||
browser_draw_frame(canvas, i, show_scrollbar);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
if(custom_icon_data) {
|
||||
// Currently only 10*10 icons are supported
|
||||
canvas_draw_bitmap(
|
||||
canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, 10, 10, custom_icon_data);
|
||||
} else if((item_type == BrowserItemTypeFile) && (model->file_icon)) {
|
||||
canvas_draw_icon(canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, model->file_icon);
|
||||
} else if(BrowserItemIcons[item_type] != NULL) {
|
||||
canvas_draw_icon(
|
||||
canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]);
|
||||
}
|
||||
canvas_draw_str(canvas, 15, Y_OFFSET + 9 + i * FRAME_HEIGHT, string_get_cstr(filename));
|
||||
}
|
||||
|
||||
if(show_scrollbar) {
|
||||
elements_scrollbar_pos(
|
||||
canvas,
|
||||
126,
|
||||
Y_OFFSET,
|
||||
canvas_height(canvas) - Y_OFFSET,
|
||||
model->item_idx,
|
||||
model->item_cnt);
|
||||
}
|
||||
|
||||
string_clear(filename);
|
||||
}
|
||||
|
||||
static void file_browser_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
FileBrowserModel* model = _model;
|
||||
|
||||
if(model->folder_loading) {
|
||||
browser_draw_loading(canvas, model);
|
||||
} else {
|
||||
browser_draw_list(canvas, model);
|
||||
}
|
||||
}
|
||||
|
||||
static bool file_browser_view_input_callback(InputEvent* event, void* context) {
|
||||
FileBrowser* browser = context;
|
||||
furi_assert(browser);
|
||||
bool consumed = false;
|
||||
bool is_loading = false;
|
||||
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
is_loading = model->folder_loading;
|
||||
return false;
|
||||
});
|
||||
|
||||
if(is_loading) {
|
||||
return false;
|
||||
} else if(event->key == InputKeyUp || event->key == InputKeyDown) {
|
||||
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
if(event->key == InputKeyUp) {
|
||||
model->item_idx =
|
||||
((model->item_idx - 1) + model->item_cnt) % model->item_cnt;
|
||||
if(browser_is_list_load_required(model)) {
|
||||
model->list_loading = true;
|
||||
int32_t load_offset = CLAMP(
|
||||
model->item_idx - ITEM_LIST_LEN_MAX / 4 * 3,
|
||||
(int32_t)model->item_cnt - ITEM_LIST_LEN_MAX,
|
||||
0);
|
||||
file_browser_worker_load(
|
||||
browser->worker, load_offset, ITEM_LIST_LEN_MAX);
|
||||
}
|
||||
} else if(event->key == InputKeyDown) {
|
||||
model->item_idx = (model->item_idx + 1) % model->item_cnt;
|
||||
if(browser_is_list_load_required(model)) {
|
||||
model->list_loading = true;
|
||||
int32_t load_offset = CLAMP(
|
||||
model->item_idx - ITEM_LIST_LEN_MAX / 4 * 1,
|
||||
(int32_t)model->item_cnt - ITEM_LIST_LEN_MAX,
|
||||
0);
|
||||
file_browser_worker_load(
|
||||
browser->worker, load_offset, ITEM_LIST_LEN_MAX);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
browser_update_offset(browser);
|
||||
consumed = true;
|
||||
}
|
||||
} else if(event->key == InputKeyOk) {
|
||||
if(event->type == InputTypeShort) {
|
||||
BrowserItem_t* selected_item = NULL;
|
||||
int32_t select_index = 0;
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
if(browser_is_item_in_array(model, model->item_idx)) {
|
||||
selected_item =
|
||||
items_array_get(model->items, model->item_idx - model->array_offset);
|
||||
select_index = model->item_idx;
|
||||
if((!model->is_root) && (select_index > 0)) {
|
||||
select_index -= 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if(selected_item) {
|
||||
if(selected_item->type == BrowserItemTypeBack) {
|
||||
file_browser_worker_folder_exit(browser->worker);
|
||||
} else if(selected_item->type == BrowserItemTypeFolder) {
|
||||
file_browser_worker_folder_enter(
|
||||
browser->worker, selected_item->path, select_index);
|
||||
} else if(selected_item->type == BrowserItemTypeFile) {
|
||||
string_set(browser->result_path, selected_item->path);
|
||||
if(browser->callback) {
|
||||
browser->callback(browser->context);
|
||||
}
|
||||
}
|
||||
}
|
||||
consumed = true;
|
||||
}
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
if(event->type == InputTypeShort) {
|
||||
bool is_root = false;
|
||||
with_view_model(
|
||||
browser->view, (FileBrowserModel * model) {
|
||||
is_root = model->is_root;
|
||||
return false;
|
||||
});
|
||||
if(!is_root) {
|
||||
file_browser_worker_folder_exit(browser->worker);
|
||||
}
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
47
applications/services/gui/modules/file_browser.h
Normal file
47
applications/services/gui/modules/file_browser.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @file file_browser.h
|
||||
* GUI: FileBrowser view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "m-string.h"
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct FileBrowser FileBrowser;
|
||||
typedef void (*FileBrowserCallback)(void* context);
|
||||
|
||||
typedef bool (
|
||||
*FileBrowserLoadItemCallback)(string_t path, void* context, uint8_t** icon, string_t item_name);
|
||||
|
||||
FileBrowser* file_browser_alloc(string_ptr result_path);
|
||||
|
||||
void file_browser_free(FileBrowser* browser);
|
||||
|
||||
View* file_browser_get_view(FileBrowser* browser);
|
||||
|
||||
void file_browser_configure(
|
||||
FileBrowser* browser,
|
||||
const char* extension,
|
||||
bool skip_assets,
|
||||
const Icon* file_icon,
|
||||
bool hide_ext);
|
||||
|
||||
void file_browser_start(FileBrowser* browser, string_t path);
|
||||
|
||||
void file_browser_stop(FileBrowser* browser);
|
||||
|
||||
void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context);
|
||||
|
||||
void file_browser_set_item_callback(
|
||||
FileBrowser* browser,
|
||||
FileBrowserLoadItemCallback callback,
|
||||
void* context);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
458
applications/services/gui/modules/file_browser_worker.c
Normal file
458
applications/services/gui/modules/file_browser_worker.c
Normal file
@@ -0,0 +1,458 @@
|
||||
#include "file_browser_worker.h"
|
||||
#include <core/check.h>
|
||||
#include <core/common_defines.h>
|
||||
#include "m-string.h"
|
||||
#include "storage/filesystem_api_defines.h"
|
||||
#include <m-array.h>
|
||||
#include <stdbool.h>
|
||||
#include <storage/storage.h>
|
||||
#include <furi.h>
|
||||
#include <stddef.h>
|
||||
#include "toolbox/path.h"
|
||||
|
||||
#define TAG "BrowserWorker"
|
||||
|
||||
#define ASSETS_DIR "assets"
|
||||
#define BROWSER_ROOT STORAGE_ANY_PATH_PREFIX
|
||||
#define FILE_NAME_LEN_MAX 256
|
||||
#define LONG_LOAD_THRESHOLD 100
|
||||
|
||||
typedef enum {
|
||||
WorkerEvtStop = (1 << 0),
|
||||
WorkerEvtLoad = (1 << 1),
|
||||
WorkerEvtFolderEnter = (1 << 2),
|
||||
WorkerEvtFolderExit = (1 << 3),
|
||||
WorkerEvtFolderRefresh = (1 << 4),
|
||||
WorkerEvtConfigChange = (1 << 5),
|
||||
} WorkerEvtFlags;
|
||||
|
||||
#define WORKER_FLAGS_ALL \
|
||||
(WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit | \
|
||||
WorkerEvtFolderRefresh | WorkerEvtConfigChange)
|
||||
|
||||
ARRAY_DEF(idx_last_array, int32_t)
|
||||
|
||||
struct BrowserWorker {
|
||||
FuriThread* thread;
|
||||
|
||||
string_t filter_extension;
|
||||
string_t path_next;
|
||||
int32_t item_sel_idx;
|
||||
uint32_t load_offset;
|
||||
uint32_t load_count;
|
||||
bool skip_assets;
|
||||
idx_last_array_t idx_last;
|
||||
|
||||
void* cb_ctx;
|
||||
BrowserWorkerFolderOpenCallback folder_cb;
|
||||
BrowserWorkerListLoadCallback list_load_cb;
|
||||
BrowserWorkerListItemCallback list_item_cb;
|
||||
BrowserWorkerLongLoadCallback long_load_cb;
|
||||
};
|
||||
|
||||
static bool browser_path_is_file(string_t path) {
|
||||
bool state = false;
|
||||
FileInfo file_info;
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) {
|
||||
if((file_info.flags & FSF_DIRECTORY) == 0) {
|
||||
state = true;
|
||||
}
|
||||
}
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return state;
|
||||
}
|
||||
|
||||
static bool browser_path_trim(string_t path) {
|
||||
bool is_root = false;
|
||||
size_t filename_start = string_search_rchar(path, '/');
|
||||
string_left(path, filename_start);
|
||||
if((string_empty_p(path)) || (filename_start == STRING_FAILURE)) {
|
||||
string_set_str(path, BROWSER_ROOT);
|
||||
is_root = true;
|
||||
}
|
||||
return is_root;
|
||||
}
|
||||
|
||||
static bool browser_filter_by_name(BrowserWorker* browser, string_t name, bool is_folder) {
|
||||
if(is_folder) {
|
||||
// Skip assets folders (if enabled)
|
||||
if(browser->skip_assets) {
|
||||
return ((string_cmp_str(name, ASSETS_DIR) == 0) ? (false) : (true));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Filter files by extension
|
||||
if((string_empty_p(browser->filter_extension)) ||
|
||||
(string_cmp_str(browser->filter_extension, "*") == 0)) {
|
||||
return true;
|
||||
}
|
||||
if(string_end_with_string_p(name, browser->filter_extension)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool browser_folder_check_and_switch(string_t path) {
|
||||
FileInfo file_info;
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
bool is_root = false;
|
||||
|
||||
if(string_search_rchar(path, '/') == 0) {
|
||||
is_root = true;
|
||||
}
|
||||
|
||||
while(1) {
|
||||
// Check if folder is existing and navigate back if not
|
||||
if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) {
|
||||
if(file_info.flags & FSF_DIRECTORY) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(is_root) {
|
||||
break;
|
||||
}
|
||||
is_root = browser_path_trim(path);
|
||||
}
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return is_root;
|
||||
}
|
||||
|
||||
static bool browser_folder_init(
|
||||
BrowserWorker* browser,
|
||||
string_t path,
|
||||
string_t filename,
|
||||
uint32_t* item_cnt,
|
||||
int32_t* file_idx) {
|
||||
bool state = false;
|
||||
FileInfo file_info;
|
||||
uint32_t total_files_cnt = 0;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* directory = storage_file_alloc(storage);
|
||||
|
||||
char name_temp[FILE_NAME_LEN_MAX];
|
||||
string_t name_str;
|
||||
string_init(name_str);
|
||||
|
||||
*item_cnt = 0;
|
||||
*file_idx = -1;
|
||||
|
||||
if(storage_dir_open(directory, string_get_cstr(path))) {
|
||||
state = true;
|
||||
while(1) {
|
||||
if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) {
|
||||
break;
|
||||
}
|
||||
if((storage_file_get_error(directory) == FSE_OK) && (name_temp[0] != '\0')) {
|
||||
total_files_cnt++;
|
||||
string_set_str(name_str, name_temp);
|
||||
if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) {
|
||||
if(!string_empty_p(filename)) {
|
||||
if(string_cmp(name_str, filename) == 0) {
|
||||
*file_idx = *item_cnt;
|
||||
}
|
||||
}
|
||||
(*item_cnt)++;
|
||||
}
|
||||
if(total_files_cnt == LONG_LOAD_THRESHOLD) {
|
||||
// There are too many files in folder and counting them will take some time - send callback to app
|
||||
if(browser->long_load_cb) {
|
||||
browser->long_load_cb(browser->cb_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string_clear(name_str);
|
||||
|
||||
storage_dir_close(directory);
|
||||
storage_file_free(directory);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static bool
|
||||
browser_folder_load(BrowserWorker* browser, string_t path, uint32_t offset, uint32_t count) {
|
||||
FileInfo file_info;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* directory = storage_file_alloc(storage);
|
||||
|
||||
char name_temp[FILE_NAME_LEN_MAX];
|
||||
string_t name_str;
|
||||
string_init(name_str);
|
||||
|
||||
uint32_t items_cnt = 0;
|
||||
|
||||
do {
|
||||
if(!storage_dir_open(directory, string_get_cstr(path))) {
|
||||
break;
|
||||
}
|
||||
|
||||
items_cnt = 0;
|
||||
while(items_cnt < offset) {
|
||||
if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) {
|
||||
break;
|
||||
}
|
||||
if(storage_file_get_error(directory) == FSE_OK) {
|
||||
string_set_str(name_str, name_temp);
|
||||
if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) {
|
||||
items_cnt++;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(items_cnt != offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(browser->list_load_cb) {
|
||||
browser->list_load_cb(browser->cb_ctx, offset);
|
||||
}
|
||||
|
||||
items_cnt = 0;
|
||||
while(items_cnt < count) {
|
||||
if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) {
|
||||
break;
|
||||
}
|
||||
if(storage_file_get_error(directory) == FSE_OK) {
|
||||
string_set_str(name_str, name_temp);
|
||||
if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) {
|
||||
string_printf(name_str, "%s/%s", string_get_cstr(path), name_temp);
|
||||
if(browser->list_item_cb) {
|
||||
browser->list_item_cb(
|
||||
browser->cb_ctx, name_str, (file_info.flags & FSF_DIRECTORY), false);
|
||||
}
|
||||
items_cnt++;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(browser->list_item_cb) {
|
||||
browser->list_item_cb(browser->cb_ctx, NULL, false, true);
|
||||
}
|
||||
} while(0);
|
||||
|
||||
string_clear(name_str);
|
||||
|
||||
storage_dir_close(directory);
|
||||
storage_file_free(directory);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return (items_cnt == count);
|
||||
}
|
||||
|
||||
static int32_t browser_worker(void* context) {
|
||||
BrowserWorker* browser = (BrowserWorker*)context;
|
||||
furi_assert(browser);
|
||||
FURI_LOG_D(TAG, "Start");
|
||||
|
||||
uint32_t items_cnt = 0;
|
||||
string_t path;
|
||||
string_init_set_str(path, BROWSER_ROOT);
|
||||
browser->item_sel_idx = -1;
|
||||
|
||||
string_t filename;
|
||||
string_init(filename);
|
||||
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtConfigChange);
|
||||
|
||||
while(1) {
|
||||
uint32_t flags =
|
||||
furi_thread_flags_wait(WORKER_FLAGS_ALL, FuriFlagWaitAny, FuriWaitForever);
|
||||
furi_assert((flags & FuriFlagError) == 0);
|
||||
|
||||
if(flags & WorkerEvtConfigChange) {
|
||||
// If start path is a path to the file - try finding index of this file in a folder
|
||||
if(browser_path_is_file(browser->path_next)) {
|
||||
path_extract_filename(browser->path_next, filename, false);
|
||||
}
|
||||
idx_last_array_reset(browser->idx_last);
|
||||
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderEnter);
|
||||
}
|
||||
|
||||
if(flags & WorkerEvtFolderEnter) {
|
||||
string_set(path, browser->path_next);
|
||||
bool is_root = browser_folder_check_and_switch(path);
|
||||
|
||||
// Push previous selected item index to history array
|
||||
idx_last_array_push_back(browser->idx_last, browser->item_sel_idx);
|
||||
|
||||
int32_t file_idx = 0;
|
||||
browser_folder_init(browser, path, filename, &items_cnt, &file_idx);
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"Enter folder: %s items: %u idx: %d",
|
||||
string_get_cstr(path),
|
||||
items_cnt,
|
||||
file_idx);
|
||||
if(browser->folder_cb) {
|
||||
browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root);
|
||||
}
|
||||
string_reset(filename);
|
||||
}
|
||||
|
||||
if(flags & WorkerEvtFolderExit) {
|
||||
browser_path_trim(path);
|
||||
bool is_root = browser_folder_check_and_switch(path);
|
||||
|
||||
int32_t file_idx = 0;
|
||||
browser_folder_init(browser, path, filename, &items_cnt, &file_idx);
|
||||
if(idx_last_array_size(browser->idx_last) > 0) {
|
||||
// Pop previous selected item index from history array
|
||||
idx_last_array_pop_back(&file_idx, browser->idx_last);
|
||||
}
|
||||
FURI_LOG_D(
|
||||
TAG, "Exit to: %s items: %u idx: %d", string_get_cstr(path), items_cnt, file_idx);
|
||||
if(browser->folder_cb) {
|
||||
browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root);
|
||||
}
|
||||
}
|
||||
|
||||
if(flags & WorkerEvtFolderRefresh) {
|
||||
bool is_root = browser_folder_check_and_switch(path);
|
||||
|
||||
int32_t file_idx = 0;
|
||||
string_reset(filename);
|
||||
browser_folder_init(browser, path, filename, &items_cnt, &file_idx);
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"Refresh folder: %s items: %u idx: %d",
|
||||
string_get_cstr(path),
|
||||
items_cnt,
|
||||
browser->item_sel_idx);
|
||||
if(browser->folder_cb) {
|
||||
browser->folder_cb(browser->cb_ctx, items_cnt, browser->item_sel_idx, is_root);
|
||||
}
|
||||
}
|
||||
|
||||
if(flags & WorkerEvtLoad) {
|
||||
FURI_LOG_D(TAG, "Load offset: %u cnt: %u", browser->load_offset, browser->load_count);
|
||||
browser_folder_load(browser, path, browser->load_offset, browser->load_count);
|
||||
}
|
||||
|
||||
if(flags & WorkerEvtStop) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string_clear(filename);
|
||||
string_clear(path);
|
||||
|
||||
FURI_LOG_D(TAG, "End");
|
||||
return 0;
|
||||
}
|
||||
|
||||
BrowserWorker* file_browser_worker_alloc(string_t path, const char* filter_ext, bool skip_assets) {
|
||||
BrowserWorker* browser = malloc(sizeof(BrowserWorker));
|
||||
|
||||
idx_last_array_init(browser->idx_last);
|
||||
|
||||
string_init_set_str(browser->filter_extension, filter_ext);
|
||||
browser->skip_assets = skip_assets;
|
||||
string_init_set(browser->path_next, path);
|
||||
|
||||
browser->thread = furi_thread_alloc();
|
||||
furi_thread_set_name(browser->thread, "BrowserWorker");
|
||||
furi_thread_set_stack_size(browser->thread, 2048);
|
||||
furi_thread_set_context(browser->thread, browser);
|
||||
furi_thread_set_callback(browser->thread, browser_worker);
|
||||
furi_thread_start(browser->thread);
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
void file_browser_worker_free(BrowserWorker* browser) {
|
||||
furi_assert(browser);
|
||||
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtStop);
|
||||
furi_thread_join(browser->thread);
|
||||
furi_thread_free(browser->thread);
|
||||
|
||||
string_clear(browser->filter_extension);
|
||||
string_clear(browser->path_next);
|
||||
|
||||
idx_last_array_clear(browser->idx_last);
|
||||
|
||||
free(browser);
|
||||
}
|
||||
|
||||
void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context) {
|
||||
furi_assert(browser);
|
||||
browser->cb_ctx = context;
|
||||
}
|
||||
|
||||
void file_browser_worker_set_folder_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerFolderOpenCallback cb) {
|
||||
furi_assert(browser);
|
||||
browser->folder_cb = cb;
|
||||
}
|
||||
|
||||
void file_browser_worker_set_list_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerListLoadCallback cb) {
|
||||
furi_assert(browser);
|
||||
browser->list_load_cb = cb;
|
||||
}
|
||||
|
||||
void file_browser_worker_set_item_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerListItemCallback cb) {
|
||||
furi_assert(browser);
|
||||
browser->list_item_cb = cb;
|
||||
}
|
||||
|
||||
void file_browser_worker_set_long_load_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerLongLoadCallback cb) {
|
||||
furi_assert(browser);
|
||||
browser->long_load_cb = cb;
|
||||
}
|
||||
|
||||
void file_browser_worker_set_config(
|
||||
BrowserWorker* browser,
|
||||
string_t path,
|
||||
const char* filter_ext,
|
||||
bool skip_assets) {
|
||||
furi_assert(browser);
|
||||
string_set(browser->path_next, path);
|
||||
string_set_str(browser->filter_extension, filter_ext);
|
||||
browser->skip_assets = skip_assets;
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtConfigChange);
|
||||
}
|
||||
|
||||
void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx) {
|
||||
furi_assert(browser);
|
||||
string_set(browser->path_next, path);
|
||||
browser->item_sel_idx = item_idx;
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderEnter);
|
||||
}
|
||||
|
||||
void file_browser_worker_folder_exit(BrowserWorker* browser) {
|
||||
furi_assert(browser);
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderExit);
|
||||
}
|
||||
|
||||
void file_browser_worker_folder_refresh(BrowserWorker* browser, int32_t item_idx) {
|
||||
furi_assert(browser);
|
||||
browser->item_sel_idx = item_idx;
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderRefresh);
|
||||
}
|
||||
|
||||
void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count) {
|
||||
furi_assert(browser);
|
||||
browser->load_offset = offset;
|
||||
browser->load_count = count;
|
||||
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtLoad);
|
||||
}
|
63
applications/services/gui/modules/file_browser_worker.h
Normal file
63
applications/services/gui/modules/file_browser_worker.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "m-string.h"
|
||||
#include <gui/view.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct BrowserWorker BrowserWorker;
|
||||
typedef void (*BrowserWorkerFolderOpenCallback)(
|
||||
void* context,
|
||||
uint32_t item_cnt,
|
||||
int32_t file_idx,
|
||||
bool is_root);
|
||||
typedef void (*BrowserWorkerListLoadCallback)(void* context, uint32_t list_load_offset);
|
||||
typedef void (*BrowserWorkerListItemCallback)(
|
||||
void* context,
|
||||
string_t item_path,
|
||||
bool is_folder,
|
||||
bool is_last);
|
||||
typedef void (*BrowserWorkerLongLoadCallback)(void* context);
|
||||
|
||||
BrowserWorker* file_browser_worker_alloc(string_t path, const char* filter_ext, bool skip_assets);
|
||||
|
||||
void file_browser_worker_free(BrowserWorker* browser);
|
||||
|
||||
void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context);
|
||||
|
||||
void file_browser_worker_set_folder_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerFolderOpenCallback cb);
|
||||
|
||||
void file_browser_worker_set_list_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerListLoadCallback cb);
|
||||
|
||||
void file_browser_worker_set_item_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerListItemCallback cb);
|
||||
|
||||
void file_browser_worker_set_long_load_callback(
|
||||
BrowserWorker* browser,
|
||||
BrowserWorkerLongLoadCallback cb);
|
||||
|
||||
void file_browser_worker_set_config(
|
||||
BrowserWorker* browser,
|
||||
string_t path,
|
||||
const char* filter_ext,
|
||||
bool skip_assets);
|
||||
|
||||
void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx);
|
||||
|
||||
void file_browser_worker_folder_exit(BrowserWorker* browser);
|
||||
|
||||
void file_browser_worker_folder_refresh(BrowserWorker* browser, int32_t item_idx);
|
||||
|
||||
void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
94
applications/services/gui/modules/loading.c
Normal file
94
applications/services/gui/modules/loading.c
Normal file
@@ -0,0 +1,94 @@
|
||||
#include <stdint.h>
|
||||
#include <furi.h>
|
||||
#include <assets_icons.h>
|
||||
#include <gui/icon_animation.h>
|
||||
#include <gui/elements.h>
|
||||
#include <gui/canvas.h>
|
||||
#include <gui/view.h>
|
||||
#include <input/input.h>
|
||||
|
||||
#include "loading.h"
|
||||
|
||||
struct Loading {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
IconAnimation* icon;
|
||||
} LoadingModel;
|
||||
|
||||
static void loading_draw_callback(Canvas* canvas, void* _model) {
|
||||
LoadingModel* model = (LoadingModel*)_model;
|
||||
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 0, 0, canvas_width(canvas), canvas_height(canvas));
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
uint8_t x = canvas_width(canvas) / 2 - 24 / 2;
|
||||
uint8_t y = canvas_height(canvas) / 2 - 24 / 2;
|
||||
|
||||
canvas_draw_icon(canvas, x, y, &A_Loading_24);
|
||||
|
||||
canvas_draw_icon_animation(canvas, x, y, model->icon);
|
||||
}
|
||||
|
||||
static bool loading_input_callback(InputEvent* event, void* context) {
|
||||
UNUSED(event);
|
||||
furi_assert(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void loading_enter_callback(void* context) {
|
||||
furi_assert(context);
|
||||
Loading* instance = context;
|
||||
LoadingModel* model = view_get_model(instance->view);
|
||||
/* using Loading View in conjunction with several
|
||||
* Stack View obligates to reassign
|
||||
* Update callback, as it can be rewritten
|
||||
*/
|
||||
view_tie_icon_animation(instance->view, model->icon);
|
||||
icon_animation_start(model->icon);
|
||||
view_commit_model(instance->view, false);
|
||||
}
|
||||
|
||||
static void loading_exit_callback(void* context) {
|
||||
furi_assert(context);
|
||||
Loading* instance = context;
|
||||
LoadingModel* model = view_get_model(instance->view);
|
||||
icon_animation_stop(model->icon);
|
||||
view_commit_model(instance->view, false);
|
||||
}
|
||||
|
||||
Loading* loading_alloc(void) {
|
||||
Loading* instance = malloc(sizeof(Loading));
|
||||
instance->view = view_alloc();
|
||||
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(LoadingModel));
|
||||
LoadingModel* model = view_get_model(instance->view);
|
||||
model->icon = icon_animation_alloc(&A_Loading_24);
|
||||
view_tie_icon_animation(instance->view, model->icon);
|
||||
view_commit_model(instance->view, false);
|
||||
|
||||
view_set_context(instance->view, instance);
|
||||
view_set_draw_callback(instance->view, loading_draw_callback);
|
||||
view_set_input_callback(instance->view, loading_input_callback);
|
||||
view_set_enter_callback(instance->view, loading_enter_callback);
|
||||
view_set_exit_callback(instance->view, loading_exit_callback);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void loading_free(Loading* instance) {
|
||||
LoadingModel* model = view_get_model(instance->view);
|
||||
icon_animation_free(model->icon);
|
||||
view_commit_model(instance->view, false);
|
||||
|
||||
furi_assert(instance);
|
||||
view_free(instance->view);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
View* loading_get_view(Loading* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->view);
|
||||
return instance->view;
|
||||
}
|
35
applications/services/gui/modules/loading.h
Normal file
35
applications/services/gui/modules/loading.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Loading anonymous structure */
|
||||
typedef struct Loading Loading;
|
||||
|
||||
/** Allocate and initialize
|
||||
*
|
||||
* This View used to show system is doing some processing
|
||||
*
|
||||
* @return Loading View instance
|
||||
*/
|
||||
Loading* loading_alloc();
|
||||
|
||||
/** Deinitialize and free Loading View
|
||||
*
|
||||
* @param instance Loading instance
|
||||
*/
|
||||
void loading_free(Loading* instance);
|
||||
|
||||
/** Get Loading view
|
||||
*
|
||||
* @param instance Loading instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* loading_get_view(Loading* instance);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
267
applications/services/gui/modules/menu.c
Normal file
267
applications/services/gui/modules/menu.c
Normal file
@@ -0,0 +1,267 @@
|
||||
#include "menu.h"
|
||||
|
||||
#include <m-array.h>
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
struct Menu {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* label;
|
||||
IconAnimation* icon;
|
||||
uint32_t index;
|
||||
MenuItemCallback callback;
|
||||
void* callback_context;
|
||||
} MenuItem;
|
||||
|
||||
ARRAY_DEF(MenuItemArray, MenuItem, M_POD_OPLIST);
|
||||
|
||||
#define M_OPL_MenuItemArray_t() ARRAY_OPLIST(MenuItemArray, M_POD_OPLIST)
|
||||
|
||||
typedef struct {
|
||||
MenuItemArray_t items;
|
||||
size_t position;
|
||||
} MenuModel;
|
||||
|
||||
static void menu_process_up(Menu* menu);
|
||||
static void menu_process_down(Menu* menu);
|
||||
static void menu_process_ok(Menu* menu);
|
||||
|
||||
static void menu_draw_callback(Canvas* canvas, void* _model) {
|
||||
MenuModel* model = _model;
|
||||
|
||||
canvas_clear(canvas);
|
||||
|
||||
size_t position = model->position;
|
||||
size_t items_count = MenuItemArray_size(model->items);
|
||||
if(items_count) {
|
||||
MenuItem* item;
|
||||
size_t shift_position;
|
||||
// First line
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
shift_position = (0 + position + items_count - 1) % items_count;
|
||||
item = MenuItemArray_get(model->items, shift_position);
|
||||
if(item->icon) {
|
||||
canvas_draw_icon_animation(canvas, 4, 3, item->icon);
|
||||
}
|
||||
canvas_draw_str(canvas, 22, 14, item->label);
|
||||
// Second line main
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
shift_position = (1 + position + items_count - 1) % items_count;
|
||||
item = MenuItemArray_get(model->items, shift_position);
|
||||
if(item->icon) {
|
||||
canvas_draw_icon_animation(canvas, 4, 25, item->icon);
|
||||
}
|
||||
canvas_draw_str(canvas, 22, 36, item->label);
|
||||
// Third line
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
shift_position = (2 + position + items_count - 1) % items_count;
|
||||
item = MenuItemArray_get(model->items, shift_position);
|
||||
if(item->icon) {
|
||||
canvas_draw_icon_animation(canvas, 4, 47, item->icon);
|
||||
}
|
||||
canvas_draw_str(canvas, 22, 58, item->label);
|
||||
// Frame and scrollbar
|
||||
elements_frame(canvas, 0, 21, 128 - 5, 21);
|
||||
elements_scrollbar(canvas, position, items_count);
|
||||
} else {
|
||||
canvas_draw_str(canvas, 2, 32, "Empty");
|
||||
elements_scrollbar(canvas, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static bool menu_input_callback(InputEvent* event, void* context) {
|
||||
Menu* menu = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeShort) {
|
||||
if(event->key == InputKeyUp) {
|
||||
consumed = true;
|
||||
menu_process_up(menu);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
consumed = true;
|
||||
menu_process_down(menu);
|
||||
} else if(event->key == InputKeyOk) {
|
||||
consumed = true;
|
||||
menu_process_ok(menu);
|
||||
}
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
if(event->key == InputKeyUp) {
|
||||
consumed = true;
|
||||
menu_process_up(menu);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
consumed = true;
|
||||
menu_process_down(menu);
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
static void menu_enter(void* context) {
|
||||
Menu* menu = context;
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
MenuItem* item = MenuItemArray_get(model->items, model->position);
|
||||
if(item && item->icon) {
|
||||
icon_animation_start(item->icon);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
static void menu_exit(void* context) {
|
||||
Menu* menu = context;
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
MenuItem* item = MenuItemArray_get(model->items, model->position);
|
||||
if(item && item->icon) {
|
||||
icon_animation_stop(item->icon);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Menu* menu_alloc() {
|
||||
Menu* menu = malloc(sizeof(Menu));
|
||||
menu->view = view_alloc(menu->view);
|
||||
view_set_context(menu->view, menu);
|
||||
view_allocate_model(menu->view, ViewModelTypeLocking, sizeof(MenuModel));
|
||||
view_set_draw_callback(menu->view, menu_draw_callback);
|
||||
view_set_input_callback(menu->view, menu_input_callback);
|
||||
view_set_enter_callback(menu->view, menu_enter);
|
||||
view_set_exit_callback(menu->view, menu_exit);
|
||||
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
MenuItemArray_init(model->items);
|
||||
model->position = 0;
|
||||
return true;
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
void menu_free(Menu* menu) {
|
||||
furi_assert(menu);
|
||||
menu_reset(menu);
|
||||
view_free(menu->view);
|
||||
free(menu);
|
||||
}
|
||||
|
||||
View* menu_get_view(Menu* menu) {
|
||||
furi_assert(menu);
|
||||
return (menu->view);
|
||||
}
|
||||
|
||||
void menu_add_item(
|
||||
Menu* menu,
|
||||
const char* label,
|
||||
const Icon* icon,
|
||||
uint32_t index,
|
||||
MenuItemCallback callback,
|
||||
void* context) {
|
||||
furi_assert(menu);
|
||||
furi_assert(label);
|
||||
|
||||
MenuItem* item = NULL;
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
item = MenuItemArray_push_new(model->items);
|
||||
item->label = label;
|
||||
item->icon = icon ? icon_animation_alloc(icon) : icon_animation_alloc(&A_Plugins_14);
|
||||
view_tie_icon_animation(menu->view, item->icon);
|
||||
item->index = index;
|
||||
item->callback = callback;
|
||||
item->callback_context = context;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void menu_reset(Menu* menu) {
|
||||
furi_assert(menu);
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
for
|
||||
M_EACH(item, model->items, MenuItemArray_t) {
|
||||
icon_animation_stop(item->icon);
|
||||
icon_animation_free(item->icon);
|
||||
}
|
||||
|
||||
MenuItemArray_reset(model->items);
|
||||
model->position = 0;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void menu_set_selected_item(Menu* menu, uint32_t index) {
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
if(index >= MenuItemArray_size(model->items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
model->position = index;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void menu_process_up(Menu* menu) {
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
MenuItem* item = MenuItemArray_get(model->items, model->position);
|
||||
if(item && item->icon) {
|
||||
icon_animation_stop(item->icon);
|
||||
}
|
||||
|
||||
if(model->position > 0) {
|
||||
model->position--;
|
||||
} else {
|
||||
model->position = MenuItemArray_size(model->items) - 1;
|
||||
}
|
||||
|
||||
item = MenuItemArray_get(model->items, model->position);
|
||||
if(item && item->icon) {
|
||||
icon_animation_start(item->icon);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void menu_process_down(Menu* menu) {
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
MenuItem* item = MenuItemArray_get(model->items, model->position);
|
||||
if(item && item->icon) {
|
||||
icon_animation_stop(item->icon);
|
||||
}
|
||||
|
||||
if(model->position < MenuItemArray_size(model->items) - 1) {
|
||||
model->position++;
|
||||
} else {
|
||||
model->position = 0;
|
||||
}
|
||||
|
||||
item = MenuItemArray_get(model->items, model->position);
|
||||
if(item && item->icon) {
|
||||
icon_animation_start(item->icon);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void menu_process_ok(Menu* menu) {
|
||||
MenuItem* item = NULL;
|
||||
with_view_model(
|
||||
menu->view, (MenuModel * model) {
|
||||
if(model->position < MenuItemArray_size(model->items)) {
|
||||
item = MenuItemArray_get(model->items, model->position);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if(item && item->callback) {
|
||||
item->callback(item->callback_context, item->index);
|
||||
}
|
||||
}
|
73
applications/services/gui/modules/menu.h
Normal file
73
applications/services/gui/modules/menu.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @file menu.h
|
||||
* GUI: Menu view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Menu anonymous structure */
|
||||
typedef struct Menu Menu;
|
||||
|
||||
/** Menu Item Callback */
|
||||
typedef void (*MenuItemCallback)(void* context, uint32_t index);
|
||||
|
||||
/** Menu allocation and initialization
|
||||
*
|
||||
* @return Menu instance
|
||||
*/
|
||||
Menu* menu_alloc();
|
||||
|
||||
/** Free menu
|
||||
*
|
||||
* @param menu Menu instance
|
||||
*/
|
||||
void menu_free(Menu* menu);
|
||||
|
||||
/** Get Menu view
|
||||
*
|
||||
* @param menu Menu instance
|
||||
*
|
||||
* @return View instance
|
||||
*/
|
||||
View* menu_get_view(Menu* menu);
|
||||
|
||||
/** Add item to menu
|
||||
*
|
||||
* @param menu Menu instance
|
||||
* @param label menu item string label
|
||||
* @param icon IconAnimation instance
|
||||
* @param index menu item index
|
||||
* @param callback MenuItemCallback instance
|
||||
* @param context pointer to context
|
||||
*/
|
||||
void menu_add_item(
|
||||
Menu* menu,
|
||||
const char* label,
|
||||
const Icon* icon,
|
||||
uint32_t index,
|
||||
MenuItemCallback callback,
|
||||
void* context);
|
||||
|
||||
/** Clean menu
|
||||
* @note this function does not free menu instance
|
||||
*
|
||||
* @param menu Menu instance
|
||||
*/
|
||||
void menu_reset(Menu* menu);
|
||||
|
||||
/** Set current menu item
|
||||
*
|
||||
* @param menu Menu instance
|
||||
* @param index The index
|
||||
*/
|
||||
void menu_set_selected_item(Menu* menu, uint32_t index);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
246
applications/services/gui/modules/popup.c
Normal file
246
applications/services/gui/modules/popup.c
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "popup.h"
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
struct Popup {
|
||||
View* view;
|
||||
void* context;
|
||||
PopupCallback callback;
|
||||
|
||||
FuriTimer* timer;
|
||||
uint32_t timer_period_in_ms;
|
||||
bool timer_enabled;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* text;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
Align horizontal;
|
||||
Align vertical;
|
||||
} TextElement;
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
const Icon* icon;
|
||||
} IconElement;
|
||||
|
||||
typedef struct {
|
||||
TextElement header;
|
||||
TextElement text;
|
||||
IconElement icon;
|
||||
} PopupModel;
|
||||
|
||||
static void popup_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
PopupModel* model = _model;
|
||||
|
||||
// Prepare canvas
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(model->icon.icon != NULL) {
|
||||
canvas_draw_icon(canvas, model->icon.x, model->icon.y, model->icon.icon);
|
||||
}
|
||||
|
||||
// Draw header
|
||||
if(model->header.text != NULL) {
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
elements_multiline_text_aligned(
|
||||
canvas,
|
||||
model->header.x,
|
||||
model->header.y,
|
||||
model->header.horizontal,
|
||||
model->header.vertical,
|
||||
model->header.text);
|
||||
}
|
||||
|
||||
// Draw text
|
||||
if(model->text.text != NULL) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
elements_multiline_text_aligned(
|
||||
canvas,
|
||||
model->text.x,
|
||||
model->text.y,
|
||||
model->text.horizontal,
|
||||
model->text.vertical,
|
||||
model->text.text);
|
||||
}
|
||||
}
|
||||
|
||||
static void popup_timer_callback(void* context) {
|
||||
furi_assert(context);
|
||||
Popup* popup = context;
|
||||
|
||||
if(popup->callback) {
|
||||
popup->callback(popup->context);
|
||||
}
|
||||
}
|
||||
|
||||
static bool popup_view_input_callback(InputEvent* event, void* context) {
|
||||
Popup* popup = context;
|
||||
bool consumed = false;
|
||||
|
||||
// Process key presses only
|
||||
if(event->type == InputTypeShort && popup->callback) {
|
||||
popup->callback(popup->context);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void popup_start_timer(void* context) {
|
||||
Popup* popup = context;
|
||||
if(popup->timer_enabled) {
|
||||
uint32_t timer_period =
|
||||
popup->timer_period_in_ms / (1000.0f / furi_kernel_get_tick_frequency());
|
||||
if(timer_period == 0) timer_period = 1;
|
||||
|
||||
if(furi_timer_start(popup->timer, timer_period) != FuriStatusOk) {
|
||||
furi_assert(0);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void popup_stop_timer(void* context) {
|
||||
Popup* popup = context;
|
||||
furi_timer_stop(popup->timer);
|
||||
}
|
||||
|
||||
Popup* popup_alloc() {
|
||||
Popup* popup = malloc(sizeof(Popup));
|
||||
popup->view = view_alloc();
|
||||
popup->timer = furi_timer_alloc(popup_timer_callback, FuriTimerTypeOnce, popup);
|
||||
furi_assert(popup->timer);
|
||||
popup->timer_period_in_ms = 1000;
|
||||
popup->timer_enabled = false;
|
||||
|
||||
view_set_context(popup->view, popup);
|
||||
view_allocate_model(popup->view, ViewModelTypeLockFree, sizeof(PopupModel));
|
||||
view_set_draw_callback(popup->view, popup_view_draw_callback);
|
||||
view_set_input_callback(popup->view, popup_view_input_callback);
|
||||
view_set_enter_callback(popup->view, popup_start_timer);
|
||||
view_set_exit_callback(popup->view, popup_stop_timer);
|
||||
|
||||
with_view_model(
|
||||
popup->view, (PopupModel * model) {
|
||||
model->header.text = NULL;
|
||||
model->header.x = 0;
|
||||
model->header.y = 0;
|
||||
model->header.horizontal = AlignLeft;
|
||||
model->header.vertical = AlignBottom;
|
||||
|
||||
model->text.text = NULL;
|
||||
model->text.x = 0;
|
||||
model->text.y = 0;
|
||||
model->text.horizontal = AlignLeft;
|
||||
model->text.vertical = AlignBottom;
|
||||
|
||||
model->icon.x = 0;
|
||||
model->icon.y = 0;
|
||||
model->icon.icon = NULL;
|
||||
return true;
|
||||
});
|
||||
return popup;
|
||||
}
|
||||
|
||||
void popup_free(Popup* popup) {
|
||||
furi_assert(popup);
|
||||
furi_timer_free(popup->timer);
|
||||
view_free(popup->view);
|
||||
free(popup);
|
||||
}
|
||||
|
||||
View* popup_get_view(Popup* popup) {
|
||||
furi_assert(popup);
|
||||
return popup->view;
|
||||
}
|
||||
|
||||
void popup_set_callback(Popup* popup, PopupCallback callback) {
|
||||
furi_assert(popup);
|
||||
popup->callback = callback;
|
||||
}
|
||||
|
||||
void popup_set_context(Popup* popup, void* context) {
|
||||
furi_assert(popup);
|
||||
popup->context = context;
|
||||
}
|
||||
|
||||
void popup_set_header(
|
||||
Popup* popup,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical) {
|
||||
furi_assert(popup);
|
||||
with_view_model(
|
||||
popup->view, (PopupModel * model) {
|
||||
model->header.text = text;
|
||||
model->header.x = x;
|
||||
model->header.y = y;
|
||||
model->header.horizontal = horizontal;
|
||||
model->header.vertical = vertical;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void popup_set_text(
|
||||
Popup* popup,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical) {
|
||||
furi_assert(popup);
|
||||
with_view_model(
|
||||
popup->view, (PopupModel * model) {
|
||||
model->text.text = text;
|
||||
model->text.x = x;
|
||||
model->text.y = y;
|
||||
model->text.horizontal = horizontal;
|
||||
model->text.vertical = vertical;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void popup_set_icon(Popup* popup, uint8_t x, uint8_t y, const Icon* icon) {
|
||||
furi_assert(popup);
|
||||
with_view_model(
|
||||
popup->view, (PopupModel * model) {
|
||||
model->icon.x = x;
|
||||
model->icon.y = y;
|
||||
model->icon.icon = icon;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void popup_set_timeout(Popup* popup, uint32_t timeout_in_ms) {
|
||||
furi_assert(popup);
|
||||
popup->timer_period_in_ms = timeout_in_ms;
|
||||
}
|
||||
|
||||
void popup_enable_timeout(Popup* popup) {
|
||||
popup->timer_enabled = true;
|
||||
}
|
||||
|
||||
void popup_disable_timeout(Popup* popup) {
|
||||
popup->timer_enabled = false;
|
||||
}
|
||||
|
||||
void popup_reset(Popup* popup) {
|
||||
furi_assert(popup);
|
||||
|
||||
with_view_model(
|
||||
popup->view, (PopupModel * model) {
|
||||
memset(&model->header, 0, sizeof(model->header));
|
||||
memset(&model->text, 0, sizeof(model->text));
|
||||
memset(&model->icon, 0, sizeof(model->icon));
|
||||
return false;
|
||||
});
|
||||
popup->callback = NULL;
|
||||
popup->context = NULL;
|
||||
popup->timer_enabled = false;
|
||||
popup->timer_period_in_ms = 0;
|
||||
}
|
134
applications/services/gui/modules/popup.h
Normal file
134
applications/services/gui/modules/popup.h
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file popup.h
|
||||
* GUI: Popup view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Popup anonymous structure */
|
||||
typedef struct Popup Popup;
|
||||
|
||||
/** Popup result callback type
|
||||
* @warning comes from GUI thread
|
||||
*/
|
||||
typedef void (*PopupCallback)(void* context);
|
||||
|
||||
/** Allocate and initialize popup
|
||||
*
|
||||
* This popup used to ask simple questions like Yes/
|
||||
*
|
||||
* @return Popup instance
|
||||
*/
|
||||
Popup* popup_alloc();
|
||||
|
||||
/** Deinitialize and free popup
|
||||
*
|
||||
* @param popup Popup instance
|
||||
*/
|
||||
void popup_free(Popup* popup);
|
||||
|
||||
/** Get popup view
|
||||
*
|
||||
* @param popup Popup instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* popup_get_view(Popup* popup);
|
||||
|
||||
/** Set popup header text
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param callback PopupCallback
|
||||
*/
|
||||
void popup_set_callback(Popup* popup, PopupCallback callback);
|
||||
|
||||
/** Set popup context
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param context context pointer, will be passed to result callback
|
||||
*/
|
||||
void popup_set_context(Popup* popup, void* context);
|
||||
|
||||
/** Set popup header text
|
||||
*
|
||||
* If text is null, popup header will not be rendered
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param text text to be shown, can be multiline
|
||||
* @param x x position
|
||||
* @param y y position
|
||||
* @param horizontal horizontal alignment
|
||||
* @param vertical vertical aligment
|
||||
*/
|
||||
void popup_set_header(
|
||||
Popup* popup,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical);
|
||||
|
||||
/** Set popup text
|
||||
*
|
||||
* If text is null, popup text will not be rendered
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param text text to be shown, can be multiline
|
||||
* @param x x position
|
||||
* @param y y position
|
||||
* @param horizontal horizontal alignment
|
||||
* @param vertical vertical aligment
|
||||
*/
|
||||
void popup_set_text(
|
||||
Popup* popup,
|
||||
const char* text,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical);
|
||||
|
||||
/** Set popup icon
|
||||
*
|
||||
* If icon position is negative, popup icon will not be rendered
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param x x position
|
||||
* @param y y position
|
||||
* @param icon pointer to Icon data
|
||||
*/
|
||||
void popup_set_icon(Popup* popup, uint8_t x, uint8_t y, const Icon* icon);
|
||||
|
||||
/** Set popup timeout
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param timeout_in_ms popup timeout value in milliseconds
|
||||
*/
|
||||
void popup_set_timeout(Popup* popup, uint32_t timeout_in_ms);
|
||||
|
||||
/** Enable popup timeout
|
||||
*
|
||||
* @param popup Popup instance
|
||||
*/
|
||||
void popup_enable_timeout(Popup* popup);
|
||||
|
||||
/** Disable popup timeout
|
||||
*
|
||||
* @param popup Popup instance
|
||||
*/
|
||||
void popup_disable_timeout(Popup* popup);
|
||||
|
||||
/** Reset popup instance state
|
||||
*
|
||||
* @param popup Popup instance
|
||||
*/
|
||||
void popup_reset(Popup* popup);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
296
applications/services/gui/modules/submenu.c
Normal file
296
applications/services/gui/modules/submenu.c
Normal file
@@ -0,0 +1,296 @@
|
||||
#include "submenu.h"
|
||||
|
||||
#include <m-array.h>
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
struct Submenu {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* label;
|
||||
uint32_t index;
|
||||
SubmenuItemCallback callback;
|
||||
void* callback_context;
|
||||
} SubmenuItem;
|
||||
|
||||
ARRAY_DEF(SubmenuItemArray, SubmenuItem, M_POD_OPLIST);
|
||||
|
||||
typedef struct {
|
||||
SubmenuItemArray_t items;
|
||||
const char* header;
|
||||
uint8_t position;
|
||||
uint8_t window_position;
|
||||
} SubmenuModel;
|
||||
|
||||
static void submenu_process_up(Submenu* submenu);
|
||||
static void submenu_process_down(Submenu* submenu);
|
||||
static void submenu_process_ok(Submenu* submenu);
|
||||
|
||||
static void submenu_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
SubmenuModel* model = _model;
|
||||
|
||||
const uint8_t item_height = 16;
|
||||
const uint8_t item_width = 123;
|
||||
|
||||
canvas_clear(canvas);
|
||||
|
||||
uint8_t position = 0;
|
||||
SubmenuItemArray_it_t it;
|
||||
|
||||
if(model->header) {
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 4, 11, model->header);
|
||||
}
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
|
||||
SubmenuItemArray_next(it)) {
|
||||
uint8_t item_position = position - model->window_position;
|
||||
uint8_t items_on_screen = model->header ? 3 : 4;
|
||||
uint8_t y_offset = model->header ? 16 : 0;
|
||||
|
||||
if(item_position < items_on_screen) {
|
||||
if(position == model->position) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
elements_slightly_rounded_box(
|
||||
canvas,
|
||||
0,
|
||||
y_offset + (item_position * item_height) + 1,
|
||||
item_width,
|
||||
item_height - 2);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
string_t disp_str;
|
||||
string_init_set_str(disp_str, SubmenuItemArray_cref(it)->label);
|
||||
elements_string_fit_width(canvas, disp_str, item_width - 20);
|
||||
|
||||
canvas_draw_str(
|
||||
canvas,
|
||||
6,
|
||||
y_offset + (item_position * item_height) + item_height - 4,
|
||||
string_get_cstr(disp_str));
|
||||
|
||||
string_clear(disp_str);
|
||||
}
|
||||
|
||||
position++;
|
||||
}
|
||||
|
||||
elements_scrollbar(canvas, model->position, SubmenuItemArray_size(model->items));
|
||||
}
|
||||
|
||||
static bool submenu_view_input_callback(InputEvent* event, void* context) {
|
||||
Submenu* submenu = context;
|
||||
furi_assert(submenu);
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeShort) {
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
consumed = true;
|
||||
submenu_process_up(submenu);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
consumed = true;
|
||||
submenu_process_down(submenu);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
consumed = true;
|
||||
submenu_process_ok(submenu);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
if(event->key == InputKeyUp) {
|
||||
consumed = true;
|
||||
submenu_process_up(submenu);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
consumed = true;
|
||||
submenu_process_down(submenu);
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
Submenu* submenu_alloc() {
|
||||
Submenu* submenu = malloc(sizeof(Submenu));
|
||||
submenu->view = view_alloc();
|
||||
view_set_context(submenu->view, submenu);
|
||||
view_allocate_model(submenu->view, ViewModelTypeLocking, sizeof(SubmenuModel));
|
||||
view_set_draw_callback(submenu->view, submenu_view_draw_callback);
|
||||
view_set_input_callback(submenu->view, submenu_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
SubmenuItemArray_init(model->items);
|
||||
model->position = 0;
|
||||
model->window_position = 0;
|
||||
model->header = NULL;
|
||||
return true;
|
||||
});
|
||||
|
||||
return submenu;
|
||||
}
|
||||
|
||||
void submenu_free(Submenu* submenu) {
|
||||
furi_assert(submenu);
|
||||
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
SubmenuItemArray_clear(model->items);
|
||||
return true;
|
||||
});
|
||||
view_free(submenu->view);
|
||||
free(submenu);
|
||||
}
|
||||
|
||||
View* submenu_get_view(Submenu* submenu) {
|
||||
furi_assert(submenu);
|
||||
return submenu->view;
|
||||
}
|
||||
|
||||
void submenu_add_item(
|
||||
Submenu* submenu,
|
||||
const char* label,
|
||||
uint32_t index,
|
||||
SubmenuItemCallback callback,
|
||||
void* callback_context) {
|
||||
SubmenuItem* item = NULL;
|
||||
furi_assert(label);
|
||||
furi_assert(submenu);
|
||||
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
item = SubmenuItemArray_push_new(model->items);
|
||||
item->label = label;
|
||||
item->index = index;
|
||||
item->callback = callback;
|
||||
item->callback_context = callback_context;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void submenu_reset(Submenu* submenu) {
|
||||
furi_assert(submenu);
|
||||
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
SubmenuItemArray_reset(model->items);
|
||||
model->position = 0;
|
||||
model->window_position = 0;
|
||||
model->header = NULL;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void submenu_set_selected_item(Submenu* submenu, uint32_t index) {
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
uint32_t position = 0;
|
||||
SubmenuItemArray_it_t it;
|
||||
for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
|
||||
SubmenuItemArray_next(it)) {
|
||||
if(index == SubmenuItemArray_cref(it)->index) {
|
||||
break;
|
||||
}
|
||||
position++;
|
||||
}
|
||||
|
||||
if(position >= SubmenuItemArray_size(model->items)) {
|
||||
position = 0;
|
||||
}
|
||||
|
||||
model->position = position;
|
||||
model->window_position = position;
|
||||
|
||||
if(model->window_position > 0) {
|
||||
model->window_position -= 1;
|
||||
}
|
||||
|
||||
uint8_t items_on_screen = model->header ? 3 : 4;
|
||||
|
||||
if(SubmenuItemArray_size(model->items) <= items_on_screen) {
|
||||
model->window_position = 0;
|
||||
} else {
|
||||
if(model->window_position >=
|
||||
(SubmenuItemArray_size(model->items) - items_on_screen)) {
|
||||
model->window_position =
|
||||
(SubmenuItemArray_size(model->items) - items_on_screen);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void submenu_process_up(Submenu* submenu) {
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
uint8_t items_on_screen = model->header ? 3 : 4;
|
||||
if(model->position > 0) {
|
||||
model->position--;
|
||||
if(((model->position - model->window_position) < 1) &&
|
||||
model->window_position > 0) {
|
||||
model->window_position--;
|
||||
}
|
||||
} else {
|
||||
model->position = SubmenuItemArray_size(model->items) - 1;
|
||||
if(model->position > (items_on_screen - 1)) {
|
||||
model->window_position = model->position - (items_on_screen - 1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void submenu_process_down(Submenu* submenu) {
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
uint8_t items_on_screen = model->header ? 3 : 4;
|
||||
if(model->position < (SubmenuItemArray_size(model->items) - 1)) {
|
||||
model->position++;
|
||||
if((model->position - model->window_position) > (items_on_screen - 2) &&
|
||||
model->window_position <
|
||||
(SubmenuItemArray_size(model->items) - items_on_screen)) {
|
||||
model->window_position++;
|
||||
}
|
||||
} else {
|
||||
model->position = 0;
|
||||
model->window_position = 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void submenu_process_ok(Submenu* submenu) {
|
||||
SubmenuItem* item = NULL;
|
||||
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
if(model->position < (SubmenuItemArray_size(model->items))) {
|
||||
item = SubmenuItemArray_get(model->items, model->position);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if(item && item->callback) {
|
||||
item->callback(item->callback_context, item->index);
|
||||
}
|
||||
}
|
||||
|
||||
void submenu_set_header(Submenu* submenu, const char* header) {
|
||||
furi_assert(submenu);
|
||||
|
||||
with_view_model(
|
||||
submenu->view, (SubmenuModel * model) {
|
||||
model->header = header;
|
||||
return true;
|
||||
});
|
||||
}
|
78
applications/services/gui/modules/submenu.h
Normal file
78
applications/services/gui/modules/submenu.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @file submenu.h
|
||||
* GUI: SubMenu view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Submenu anonymous structure */
|
||||
typedef struct Submenu Submenu;
|
||||
typedef void (*SubmenuItemCallback)(void* context, uint32_t index);
|
||||
|
||||
/** Allocate and initialize submenu
|
||||
*
|
||||
* This submenu is used to select one option
|
||||
*
|
||||
* @return Submenu instance
|
||||
*/
|
||||
Submenu* submenu_alloc();
|
||||
|
||||
/** Deinitialize and free submenu
|
||||
*
|
||||
* @param submenu Submenu instance
|
||||
*/
|
||||
void submenu_free(Submenu* submenu);
|
||||
|
||||
/** Get submenu view
|
||||
*
|
||||
* @param submenu Submenu instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* submenu_get_view(Submenu* submenu);
|
||||
|
||||
/** Add item to submenu
|
||||
*
|
||||
* @param submenu Submenu instance
|
||||
* @param label menu item label
|
||||
* @param index menu item index, used for callback, may be
|
||||
* the same with other items
|
||||
* @param callback menu item callback
|
||||
* @param callback_context menu item callback context
|
||||
*/
|
||||
void submenu_add_item(
|
||||
Submenu* submenu,
|
||||
const char* label,
|
||||
uint32_t index,
|
||||
SubmenuItemCallback callback,
|
||||
void* callback_context);
|
||||
|
||||
/** Remove all items from submenu
|
||||
*
|
||||
* @param submenu Submenu instance
|
||||
*/
|
||||
void submenu_reset(Submenu* submenu);
|
||||
|
||||
/** Set submenu item selector
|
||||
*
|
||||
* @param submenu Submenu instance
|
||||
* @param index The index
|
||||
*/
|
||||
void submenu_set_selected_item(Submenu* submenu, uint32_t index);
|
||||
|
||||
/** Set optional header for submenu
|
||||
*
|
||||
* @param submenu Submenu instance
|
||||
* @param header header to set
|
||||
*/
|
||||
void submenu_set_header(Submenu* submenu, const char* header);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
214
applications/services/gui/modules/text_box.c
Normal file
214
applications/services/gui/modules/text_box.c
Normal file
@@ -0,0 +1,214 @@
|
||||
#include "text_box.h"
|
||||
#include "gui/canvas.h"
|
||||
#include <m-string.h>
|
||||
#include <furi.h>
|
||||
#include <gui/elements.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct TextBox {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* text;
|
||||
char* text_pos;
|
||||
string_t text_formatted;
|
||||
int32_t scroll_pos;
|
||||
int32_t scroll_num;
|
||||
TextBoxFont font;
|
||||
TextBoxFocus focus;
|
||||
bool formatted;
|
||||
} TextBoxModel;
|
||||
|
||||
static void text_box_process_down(TextBox* text_box) {
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
if(model->scroll_pos < model->scroll_num - 1) {
|
||||
model->scroll_pos++;
|
||||
// Search next line start
|
||||
while(*model->text_pos++ != '\n')
|
||||
;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void text_box_process_up(TextBox* text_box) {
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
if(model->scroll_pos > 0) {
|
||||
model->scroll_pos--;
|
||||
// Reach last symbol of previous line
|
||||
model->text_pos--;
|
||||
// Search prevous line start
|
||||
while((model->text_pos != model->text) && (*(--model->text_pos) != '\n'))
|
||||
;
|
||||
if(*model->text_pos == '\n') {
|
||||
model->text_pos++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void text_box_insert_endline(Canvas* canvas, TextBoxModel* model) {
|
||||
size_t i = 0;
|
||||
size_t line_width = 0;
|
||||
const char* str = model->text;
|
||||
size_t line_num = 0;
|
||||
|
||||
const size_t text_width = 120;
|
||||
|
||||
while(str[i] != '\0') {
|
||||
char symb = str[i++];
|
||||
if(symb != '\n') {
|
||||
size_t glyph_width = canvas_glyph_width(canvas, symb);
|
||||
if(line_width + glyph_width > text_width) {
|
||||
line_num++;
|
||||
line_width = 0;
|
||||
string_push_back(model->text_formatted, '\n');
|
||||
}
|
||||
line_width += glyph_width;
|
||||
} else {
|
||||
line_num++;
|
||||
line_width = 0;
|
||||
}
|
||||
string_push_back(model->text_formatted, symb);
|
||||
}
|
||||
line_num++;
|
||||
model->text = string_get_cstr(model->text_formatted);
|
||||
model->text_pos = (char*)model->text;
|
||||
if(model->focus == TextBoxFocusEnd && line_num > 5) {
|
||||
// Set text position to 5th line from the end
|
||||
for(uint8_t i = 0; i < line_num - 5; i++) {
|
||||
while(*model->text_pos++ != '\n') {
|
||||
};
|
||||
}
|
||||
model->scroll_num = line_num - 4;
|
||||
model->scroll_pos = line_num - 5;
|
||||
} else {
|
||||
model->scroll_num = MAX(line_num - 4, 0u);
|
||||
model->scroll_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_box_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
TextBoxModel* model = _model;
|
||||
|
||||
canvas_clear(canvas);
|
||||
if(model->font == TextBoxFontText) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
} else if(model->font == TextBoxFontHex) {
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
}
|
||||
|
||||
if(!model->formatted) {
|
||||
text_box_insert_endline(canvas, model);
|
||||
model->formatted = true;
|
||||
}
|
||||
|
||||
elements_slightly_rounded_frame(canvas, 0, 0, 124, 64);
|
||||
elements_multiline_text(canvas, 3, 11, model->text_pos);
|
||||
elements_scrollbar(canvas, model->scroll_pos, model->scroll_num);
|
||||
}
|
||||
|
||||
static bool text_box_view_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
|
||||
TextBox* text_box = context;
|
||||
bool consumed = false;
|
||||
if(event->type == InputTypeShort) {
|
||||
if(event->key == InputKeyDown) {
|
||||
text_box_process_down(text_box);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyUp) {
|
||||
text_box_process_up(text_box);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
TextBox* text_box_alloc() {
|
||||
TextBox* text_box = malloc(sizeof(TextBox));
|
||||
text_box->view = view_alloc();
|
||||
view_set_context(text_box->view, text_box);
|
||||
view_allocate_model(text_box->view, ViewModelTypeLocking, sizeof(TextBoxModel));
|
||||
view_set_draw_callback(text_box->view, text_box_view_draw_callback);
|
||||
view_set_input_callback(text_box->view, text_box_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
model->text = NULL;
|
||||
string_init_set_str(model->text_formatted, "");
|
||||
model->formatted = false;
|
||||
model->font = TextBoxFontText;
|
||||
return true;
|
||||
});
|
||||
|
||||
return text_box;
|
||||
}
|
||||
|
||||
void text_box_free(TextBox* text_box) {
|
||||
furi_assert(text_box);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
string_clear(model->text_formatted);
|
||||
return true;
|
||||
});
|
||||
view_free(text_box->view);
|
||||
free(text_box);
|
||||
}
|
||||
|
||||
View* text_box_get_view(TextBox* text_box) {
|
||||
furi_assert(text_box);
|
||||
return text_box->view;
|
||||
}
|
||||
|
||||
void text_box_reset(TextBox* text_box) {
|
||||
furi_assert(text_box);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
model->text = NULL;
|
||||
string_set_str(model->text_formatted, "");
|
||||
model->font = TextBoxFontText;
|
||||
model->focus = TextBoxFocusStart;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void text_box_set_text(TextBox* text_box, const char* text) {
|
||||
furi_assert(text_box);
|
||||
furi_assert(text);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
model->text = text;
|
||||
string_reset(model->text_formatted);
|
||||
string_reserve(model->text_formatted, strlen(text));
|
||||
model->formatted = false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void text_box_set_font(TextBox* text_box, TextBoxFont font) {
|
||||
furi_assert(text_box);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
model->font = font;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void text_box_set_focus(TextBox* text_box, TextBoxFocus focus) {
|
||||
furi_assert(text_box);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, (TextBoxModel * model) {
|
||||
model->focus = focus;
|
||||
return true;
|
||||
});
|
||||
}
|
77
applications/services/gui/modules/text_box.h
Normal file
77
applications/services/gui/modules/text_box.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @file text_box.h
|
||||
* GUI: TextBox view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** TextBox anonymous structure */
|
||||
typedef struct TextBox TextBox;
|
||||
|
||||
typedef enum {
|
||||
TextBoxFontText,
|
||||
TextBoxFontHex,
|
||||
} TextBoxFont;
|
||||
|
||||
typedef enum {
|
||||
TextBoxFocusStart,
|
||||
TextBoxFocusEnd,
|
||||
} TextBoxFocus;
|
||||
|
||||
/** Allocate and initialize text_box
|
||||
*
|
||||
* @return TextBox instance
|
||||
*/
|
||||
TextBox* text_box_alloc();
|
||||
|
||||
/** Deinitialize and free text_box
|
||||
*
|
||||
* @param text_box text_box instance
|
||||
*/
|
||||
void text_box_free(TextBox* text_box);
|
||||
|
||||
/** Get text_box view
|
||||
*
|
||||
* @param text_box TextBox instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* text_box_get_view(TextBox* text_box);
|
||||
|
||||
/** Clean text_box
|
||||
*
|
||||
* @param text_box TextBox instance
|
||||
*/
|
||||
void text_box_reset(TextBox* text_box);
|
||||
|
||||
/** Set text for text_box
|
||||
*
|
||||
* @param text_box TextBox instance
|
||||
* @param text text to set
|
||||
*/
|
||||
void text_box_set_text(TextBox* text_box, const char* text);
|
||||
|
||||
/** Set TextBox font
|
||||
*
|
||||
* @param text_box TextBox instance
|
||||
* @param font TextBoxFont instance
|
||||
*/
|
||||
void text_box_set_font(TextBox* text_box, TextBoxFont font);
|
||||
|
||||
/** Set TextBox focus
|
||||
* @note Use to display from start or from end
|
||||
*
|
||||
* @param text_box TextBox instance
|
||||
* @param focus TextBoxFocus instance
|
||||
*/
|
||||
void text_box_set_focus(TextBox* text_box, TextBoxFocus focus);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
561
applications/services/gui/modules/text_input.c
Normal file
561
applications/services/gui/modules/text_input.c
Normal file
@@ -0,0 +1,561 @@
|
||||
#include "text_input.h"
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
struct TextInput {
|
||||
View* view;
|
||||
FuriTimer* timer;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char text;
|
||||
const uint8_t x;
|
||||
const uint8_t y;
|
||||
} TextInputKey;
|
||||
|
||||
typedef struct {
|
||||
const char* header;
|
||||
char* text_buffer;
|
||||
size_t text_buffer_size;
|
||||
bool clear_default_text;
|
||||
|
||||
TextInputCallback callback;
|
||||
void* callback_context;
|
||||
|
||||
uint8_t selected_row;
|
||||
uint8_t selected_column;
|
||||
|
||||
TextInputValidatorCallback validator_callback;
|
||||
void* validator_callback_context;
|
||||
string_t validator_text;
|
||||
bool valadator_message_visible;
|
||||
} TextInputModel;
|
||||
|
||||
static const uint8_t keyboard_origin_x = 1;
|
||||
static const uint8_t keyboard_origin_y = 29;
|
||||
static const uint8_t keyboard_row_count = 3;
|
||||
|
||||
#define ENTER_KEY '\r'
|
||||
#define BACKSPACE_KEY '\b'
|
||||
|
||||
static const TextInputKey keyboard_keys_row_1[] = {
|
||||
{'q', 1, 8},
|
||||
{'w', 10, 8},
|
||||
{'e', 19, 8},
|
||||
{'r', 28, 8},
|
||||
{'t', 37, 8},
|
||||
{'y', 46, 8},
|
||||
{'u', 55, 8},
|
||||
{'i', 64, 8},
|
||||
{'o', 73, 8},
|
||||
{'p', 82, 8},
|
||||
{'0', 91, 8},
|
||||
{'1', 100, 8},
|
||||
{'2', 110, 8},
|
||||
{'3', 120, 8},
|
||||
};
|
||||
|
||||
static const TextInputKey keyboard_keys_row_2[] = {
|
||||
{'a', 1, 20},
|
||||
{'s', 10, 20},
|
||||
{'d', 19, 20},
|
||||
{'f', 28, 20},
|
||||
{'g', 37, 20},
|
||||
{'h', 46, 20},
|
||||
{'j', 55, 20},
|
||||
{'k', 64, 20},
|
||||
{'l', 73, 20},
|
||||
{BACKSPACE_KEY, 82, 12},
|
||||
{'4', 100, 20},
|
||||
{'5', 110, 20},
|
||||
{'6', 120, 20},
|
||||
};
|
||||
|
||||
static const TextInputKey keyboard_keys_row_3[] = {
|
||||
{'z', 1, 32},
|
||||
{'x', 10, 32},
|
||||
{'c', 19, 32},
|
||||
{'v', 28, 32},
|
||||
{'b', 37, 32},
|
||||
{'n', 46, 32},
|
||||
{'m', 55, 32},
|
||||
{'_', 64, 32},
|
||||
{ENTER_KEY, 74, 23},
|
||||
{'7', 100, 32},
|
||||
{'8', 110, 32},
|
||||
{'9', 120, 32},
|
||||
};
|
||||
|
||||
static uint8_t get_row_size(uint8_t row_index) {
|
||||
uint8_t row_size = 0;
|
||||
|
||||
switch(row_index + 1) {
|
||||
case 1:
|
||||
row_size = sizeof(keyboard_keys_row_1) / sizeof(TextInputKey);
|
||||
break;
|
||||
case 2:
|
||||
row_size = sizeof(keyboard_keys_row_2) / sizeof(TextInputKey);
|
||||
break;
|
||||
case 3:
|
||||
row_size = sizeof(keyboard_keys_row_3) / sizeof(TextInputKey);
|
||||
break;
|
||||
}
|
||||
|
||||
return row_size;
|
||||
}
|
||||
|
||||
static const TextInputKey* get_row(uint8_t row_index) {
|
||||
const TextInputKey* row = NULL;
|
||||
|
||||
switch(row_index + 1) {
|
||||
case 1:
|
||||
row = keyboard_keys_row_1;
|
||||
break;
|
||||
case 2:
|
||||
row = keyboard_keys_row_2;
|
||||
break;
|
||||
case 3:
|
||||
row = keyboard_keys_row_3;
|
||||
break;
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static char get_selected_char(TextInputModel* model) {
|
||||
return get_row(model->selected_row)[model->selected_column].text;
|
||||
}
|
||||
|
||||
static bool char_is_lowercase(char letter) {
|
||||
return (letter >= 0x61 && letter <= 0x7A);
|
||||
}
|
||||
|
||||
static char char_to_uppercase(const char letter) {
|
||||
if(letter == '_') {
|
||||
return 0x20;
|
||||
} else if(isalpha(letter)) {
|
||||
return (letter - 0x20);
|
||||
} else {
|
||||
return letter;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_backspace_cb(TextInputModel* model) {
|
||||
uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
|
||||
if(text_length > 0) {
|
||||
model->text_buffer[text_length - 1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
TextInputModel* model = _model;
|
||||
uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
|
||||
uint8_t needed_string_width = canvas_width(canvas) - 8;
|
||||
uint8_t start_pos = 4;
|
||||
|
||||
const char* text = model->text_buffer;
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
canvas_draw_str(canvas, 2, 8, model->header);
|
||||
elements_slightly_rounded_frame(canvas, 1, 12, 126, 15);
|
||||
|
||||
if(canvas_string_width(canvas, text) > needed_string_width) {
|
||||
canvas_draw_str(canvas, start_pos, 22, "...");
|
||||
start_pos += 6;
|
||||
needed_string_width -= 8;
|
||||
}
|
||||
|
||||
while(text != 0 && canvas_string_width(canvas, text) > needed_string_width) {
|
||||
text++;
|
||||
}
|
||||
|
||||
if(model->clear_default_text) {
|
||||
elements_slightly_rounded_box(
|
||||
canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 22, "|");
|
||||
canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 22, "|");
|
||||
}
|
||||
canvas_draw_str(canvas, start_pos, 22, text);
|
||||
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
|
||||
for(uint8_t row = 0; row <= keyboard_row_count; row++) {
|
||||
const uint8_t column_count = get_row_size(row);
|
||||
const TextInputKey* keys = get_row(row);
|
||||
|
||||
for(size_t column = 0; column < column_count; column++) {
|
||||
if(keys[column].text == ENTER_KEY) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySaveSelected_24x11);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySave_24x11);
|
||||
}
|
||||
} else if(keys[column].text == BACKSPACE_KEY) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeyBackspaceSelected_16x9);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeyBackspace_16x9);
|
||||
}
|
||||
} else {
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x - 1,
|
||||
keyboard_origin_y + keys[column].y - 8,
|
||||
7,
|
||||
10);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
if(text_length == 0 && char_is_lowercase(keys[column].text)) {
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
char_to_uppercase(keys[column].text));
|
||||
} else {
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
keys[column].text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(model->valadator_message_visible) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 8, 10, 110, 48);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42);
|
||||
canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
|
||||
canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
|
||||
elements_multiline_text(canvas, 62, 20, string_get_cstr(model->validator_text));
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_handle_up(TextInput* text_input, TextInputModel* model) {
|
||||
UNUSED(text_input);
|
||||
if(model->selected_row > 0) {
|
||||
model->selected_row--;
|
||||
if(model->selected_column > get_row_size(model->selected_row) - 6) {
|
||||
model->selected_column = model->selected_column + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_handle_down(TextInput* text_input, TextInputModel* model) {
|
||||
UNUSED(text_input);
|
||||
if(model->selected_row < keyboard_row_count - 1) {
|
||||
model->selected_row++;
|
||||
if(model->selected_column > get_row_size(model->selected_row) - 4) {
|
||||
model->selected_column = model->selected_column - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_handle_left(TextInput* text_input, TextInputModel* model) {
|
||||
UNUSED(text_input);
|
||||
if(model->selected_column > 0) {
|
||||
model->selected_column--;
|
||||
} else {
|
||||
model->selected_column = get_row_size(model->selected_row) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_handle_right(TextInput* text_input, TextInputModel* model) {
|
||||
UNUSED(text_input);
|
||||
if(model->selected_column < get_row_size(model->selected_row) - 1) {
|
||||
model->selected_column++;
|
||||
} else {
|
||||
model->selected_column = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_input_handle_ok(TextInput* text_input, TextInputModel* model, bool shift) {
|
||||
char selected = get_selected_char(model);
|
||||
uint8_t text_length = strlen(model->text_buffer);
|
||||
|
||||
if(shift) {
|
||||
selected = char_to_uppercase(selected);
|
||||
}
|
||||
|
||||
if(selected == ENTER_KEY) {
|
||||
if(model->validator_callback &&
|
||||
(!model->validator_callback(
|
||||
model->text_buffer, model->validator_text, model->validator_callback_context))) {
|
||||
model->valadator_message_visible = true;
|
||||
furi_timer_start(text_input->timer, furi_kernel_get_tick_frequency() * 4);
|
||||
} else if(model->callback != 0 && text_length > 0) {
|
||||
model->callback(model->callback_context);
|
||||
}
|
||||
} else if(selected == BACKSPACE_KEY) {
|
||||
text_input_backspace_cb(model);
|
||||
} else if(text_length < (model->text_buffer_size - 1)) {
|
||||
if(model->clear_default_text) {
|
||||
text_length = 0;
|
||||
}
|
||||
if(text_length == 0 && char_is_lowercase(selected)) {
|
||||
selected = char_to_uppercase(selected);
|
||||
}
|
||||
model->text_buffer[text_length] = selected;
|
||||
model->text_buffer[text_length + 1] = 0;
|
||||
}
|
||||
model->clear_default_text = false;
|
||||
}
|
||||
|
||||
static bool text_input_view_input_callback(InputEvent* event, void* context) {
|
||||
TextInput* text_input = context;
|
||||
furi_assert(text_input);
|
||||
|
||||
bool consumed = false;
|
||||
|
||||
// Acquire model
|
||||
TextInputModel* model = view_get_model(text_input->view);
|
||||
|
||||
if((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) &&
|
||||
model->valadator_message_visible) {
|
||||
model->valadator_message_visible = false;
|
||||
consumed = true;
|
||||
} else if(event->type == InputTypeShort) {
|
||||
consumed = true;
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
text_input_handle_up(text_input, model);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
text_input_handle_down(text_input, model);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
text_input_handle_left(text_input, model);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
text_input_handle_right(text_input, model);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
text_input_handle_ok(text_input, model, false);
|
||||
break;
|
||||
default:
|
||||
consumed = false;
|
||||
break;
|
||||
}
|
||||
} else if(event->type == InputTypeLong) {
|
||||
consumed = true;
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
text_input_handle_up(text_input, model);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
text_input_handle_down(text_input, model);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
text_input_handle_left(text_input, model);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
text_input_handle_right(text_input, model);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
text_input_handle_ok(text_input, model, true);
|
||||
break;
|
||||
case InputKeyBack:
|
||||
text_input_backspace_cb(model);
|
||||
break;
|
||||
default:
|
||||
consumed = false;
|
||||
break;
|
||||
}
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
consumed = true;
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
text_input_handle_up(text_input, model);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
text_input_handle_down(text_input, model);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
text_input_handle_left(text_input, model);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
text_input_handle_right(text_input, model);
|
||||
break;
|
||||
case InputKeyBack:
|
||||
text_input_backspace_cb(model);
|
||||
break;
|
||||
default:
|
||||
consumed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit model
|
||||
view_commit_model(text_input->view, consumed);
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void text_input_timer_callback(void* context) {
|
||||
furi_assert(context);
|
||||
TextInput* text_input = context;
|
||||
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
model->valadator_message_visible = false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
TextInput* text_input_alloc() {
|
||||
TextInput* text_input = malloc(sizeof(TextInput));
|
||||
text_input->view = view_alloc();
|
||||
view_set_context(text_input->view, text_input);
|
||||
view_allocate_model(text_input->view, ViewModelTypeLocking, sizeof(TextInputModel));
|
||||
view_set_draw_callback(text_input->view, text_input_view_draw_callback);
|
||||
view_set_input_callback(text_input->view, text_input_view_input_callback);
|
||||
|
||||
text_input->timer = furi_timer_alloc(text_input_timer_callback, FuriTimerTypeOnce, text_input);
|
||||
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
string_init(model->validator_text);
|
||||
return false;
|
||||
});
|
||||
|
||||
text_input_reset(text_input);
|
||||
|
||||
return text_input;
|
||||
}
|
||||
|
||||
void text_input_free(TextInput* text_input) {
|
||||
furi_assert(text_input);
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
string_clear(model->validator_text);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Send stop command
|
||||
furi_timer_stop(text_input->timer);
|
||||
// Release allocated memory
|
||||
furi_timer_free(text_input->timer);
|
||||
|
||||
view_free(text_input->view);
|
||||
|
||||
free(text_input);
|
||||
}
|
||||
|
||||
void text_input_reset(TextInput* text_input) {
|
||||
furi_assert(text_input);
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
model->text_buffer_size = 0;
|
||||
model->header = "";
|
||||
model->selected_row = 0;
|
||||
model->selected_column = 0;
|
||||
model->clear_default_text = false;
|
||||
model->text_buffer = NULL;
|
||||
model->text_buffer_size = 0;
|
||||
model->callback = NULL;
|
||||
model->callback_context = NULL;
|
||||
model->validator_callback = NULL;
|
||||
model->validator_callback_context = NULL;
|
||||
string_reset(model->validator_text);
|
||||
model->valadator_message_visible = false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
View* text_input_get_view(TextInput* text_input) {
|
||||
furi_assert(text_input);
|
||||
return text_input->view;
|
||||
}
|
||||
|
||||
void text_input_set_result_callback(
|
||||
TextInput* text_input,
|
||||
TextInputCallback callback,
|
||||
void* callback_context,
|
||||
char* text_buffer,
|
||||
size_t text_buffer_size,
|
||||
bool clear_default_text) {
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
model->callback = callback;
|
||||
model->callback_context = callback_context;
|
||||
model->text_buffer = text_buffer;
|
||||
model->text_buffer_size = text_buffer_size;
|
||||
model->clear_default_text = clear_default_text;
|
||||
if(text_buffer && text_buffer[0] != '\0') {
|
||||
// Set focus on Save
|
||||
model->selected_row = 2;
|
||||
model->selected_column = 8;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void text_input_set_validator(
|
||||
TextInput* text_input,
|
||||
TextInputValidatorCallback callback,
|
||||
void* callback_context) {
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
model->validator_callback = callback;
|
||||
model->validator_callback_context = callback_context;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
TextInputValidatorCallback text_input_get_validator_callback(TextInput* text_input) {
|
||||
TextInputValidatorCallback validator_callback = NULL;
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
validator_callback = model->validator_callback;
|
||||
return false;
|
||||
});
|
||||
return validator_callback;
|
||||
}
|
||||
|
||||
void* text_input_get_validator_callback_context(TextInput* text_input) {
|
||||
void* validator_callback_context = NULL;
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
validator_callback_context = model->validator_callback_context;
|
||||
return false;
|
||||
});
|
||||
return validator_callback_context;
|
||||
}
|
||||
|
||||
void text_input_set_header_text(TextInput* text_input, const char* text) {
|
||||
with_view_model(
|
||||
text_input->view, (TextInputModel * model) {
|
||||
model->header = text;
|
||||
return true;
|
||||
});
|
||||
}
|
87
applications/services/gui/modules/text_input.h
Normal file
87
applications/services/gui/modules/text_input.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file text_input.h
|
||||
* GUI: TextInput keybord view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
#include "validators.h"
|
||||
#include <m-string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Text input anonymous structure */
|
||||
typedef struct TextInput TextInput;
|
||||
typedef void (*TextInputCallback)(void* context);
|
||||
typedef bool (*TextInputValidatorCallback)(const char* text, string_t error, void* context);
|
||||
|
||||
/** Allocate and initialize text input
|
||||
*
|
||||
* This text input is used to enter string
|
||||
*
|
||||
* @return TextInput instance
|
||||
*/
|
||||
TextInput* text_input_alloc();
|
||||
|
||||
/** Deinitialize and free text input
|
||||
*
|
||||
* @param text_input TextInput instance
|
||||
*/
|
||||
void text_input_free(TextInput* text_input);
|
||||
|
||||
/** Clean text input view Note: this function does not free memory
|
||||
*
|
||||
* @param text_input Text input instance
|
||||
*/
|
||||
void text_input_reset(TextInput* text_input);
|
||||
|
||||
/** Get text input view
|
||||
*
|
||||
* @param text_input TextInput instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* text_input_get_view(TextInput* text_input);
|
||||
|
||||
/** Set text input result callback
|
||||
*
|
||||
* @param text_input TextInput instance
|
||||
* @param callback callback fn
|
||||
* @param callback_context callback context
|
||||
* @param text_buffer pointer to YOUR text buffer, that we going
|
||||
* to modify
|
||||
* @param text_buffer_size YOUR text buffer size in bytes. Max string
|
||||
* length will be text_buffer_size-1.
|
||||
* @param clear_default_text clear text from text_buffer on first OK
|
||||
* event
|
||||
*/
|
||||
void text_input_set_result_callback(
|
||||
TextInput* text_input,
|
||||
TextInputCallback callback,
|
||||
void* callback_context,
|
||||
char* text_buffer,
|
||||
size_t text_buffer_size,
|
||||
bool clear_default_text);
|
||||
|
||||
void text_input_set_validator(
|
||||
TextInput* text_input,
|
||||
TextInputValidatorCallback callback,
|
||||
void* callback_context);
|
||||
|
||||
TextInputValidatorCallback text_input_get_validator_callback(TextInput* text_input);
|
||||
|
||||
void* text_input_get_validator_callback_context(TextInput* text_input);
|
||||
|
||||
/** Set text input header text
|
||||
*
|
||||
* @param text_input TextInput instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void text_input_set_header_text(TextInput* text_input, const char* text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
57
applications/services/gui/modules/validators.c
Normal file
57
applications/services/gui/modules/validators.c
Normal file
@@ -0,0 +1,57 @@
|
||||
#include <furi.h>
|
||||
#include "validators.h"
|
||||
#include <storage/storage.h>
|
||||
|
||||
struct ValidatorIsFile {
|
||||
char* app_path_folder;
|
||||
const char* app_extension;
|
||||
char* current_name;
|
||||
};
|
||||
|
||||
bool validator_is_file_callback(const char* text, string_t error, void* context) {
|
||||
furi_assert(context);
|
||||
ValidatorIsFile* instance = context;
|
||||
|
||||
if(instance->current_name != NULL) {
|
||||
if(strcmp(instance->current_name, text) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ret = true;
|
||||
string_t path;
|
||||
string_init_printf(path, "%s/%s%s", instance->app_path_folder, text, instance->app_extension);
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
if(storage_common_stat(storage, string_get_cstr(path), NULL) == FSE_OK) {
|
||||
ret = false;
|
||||
string_printf(error, "This name\nexists!\nChoose\nanother one.");
|
||||
} else {
|
||||
ret = true;
|
||||
}
|
||||
string_clear(path);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ValidatorIsFile* validator_is_file_alloc_init(
|
||||
const char* app_path_folder,
|
||||
const char* app_extension,
|
||||
const char* current_name) {
|
||||
ValidatorIsFile* instance = malloc(sizeof(ValidatorIsFile));
|
||||
|
||||
instance->app_path_folder = strdup(app_path_folder);
|
||||
instance->app_extension = app_extension;
|
||||
if(current_name != NULL) {
|
||||
instance->current_name = strdup(current_name);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void validator_is_file_free(ValidatorIsFile* instance) {
|
||||
furi_assert(instance);
|
||||
free(instance->app_path_folder);
|
||||
free(instance->current_name);
|
||||
free(instance);
|
||||
}
|
22
applications/services/gui/modules/validators.h
Normal file
22
applications/services/gui/modules/validators.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <m-string.h>
|
||||
#include <core/common_defines.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef struct ValidatorIsFile ValidatorIsFile;
|
||||
|
||||
ValidatorIsFile* validator_is_file_alloc_init(
|
||||
const char* app_path_folder,
|
||||
const char* app_extension,
|
||||
const char* current_name);
|
||||
|
||||
void validator_is_file_free(ValidatorIsFile* instance);
|
||||
|
||||
bool validator_is_file_callback(const char* text, string_t error, void* context);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
388
applications/services/gui/modules/variable_item_list.c
Normal file
388
applications/services/gui/modules/variable_item_list.c
Normal file
@@ -0,0 +1,388 @@
|
||||
#include "variable_item_list.h"
|
||||
#include "gui/canvas.h"
|
||||
#include <m-array.h>
|
||||
#include <furi.h>
|
||||
#include <gui/elements.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct VariableItem {
|
||||
const char* label;
|
||||
uint8_t current_value_index;
|
||||
string_t current_value_text;
|
||||
uint8_t values_count;
|
||||
VariableItemChangeCallback change_callback;
|
||||
void* context;
|
||||
};
|
||||
|
||||
ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST);
|
||||
|
||||
struct VariableItemList {
|
||||
View* view;
|
||||
VariableItemListEnterCallback callback;
|
||||
void* context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
VariableItemArray_t items;
|
||||
uint8_t position;
|
||||
uint8_t window_position;
|
||||
} VariableItemListModel;
|
||||
|
||||
static void variable_item_list_process_up(VariableItemList* variable_item_list);
|
||||
static void variable_item_list_process_down(VariableItemList* variable_item_list);
|
||||
static void variable_item_list_process_left(VariableItemList* variable_item_list);
|
||||
static void variable_item_list_process_right(VariableItemList* variable_item_list);
|
||||
static void variable_item_list_process_ok(VariableItemList* variable_item_list);
|
||||
|
||||
static void variable_item_list_draw_callback(Canvas* canvas, void* _model) {
|
||||
VariableItemListModel* model = _model;
|
||||
|
||||
const uint8_t item_height = 16;
|
||||
const uint8_t item_width = 123;
|
||||
|
||||
canvas_clear(canvas);
|
||||
|
||||
uint8_t position = 0;
|
||||
VariableItemArray_it_t it;
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
|
||||
VariableItemArray_next(it)) {
|
||||
uint8_t item_position = position - model->window_position;
|
||||
uint8_t items_on_screen = 4;
|
||||
uint8_t y_offset = 0;
|
||||
|
||||
if(item_position < items_on_screen) {
|
||||
const VariableItem* item = VariableItemArray_cref(it);
|
||||
uint8_t item_y = y_offset + (item_position * item_height);
|
||||
uint8_t item_text_y = item_y + item_height - 4;
|
||||
|
||||
if(position == model->position) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
elements_slightly_rounded_box(canvas, 0, item_y + 1, item_width, item_height - 2);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
canvas_draw_str(canvas, 6, item_text_y, item->label);
|
||||
|
||||
if(item->current_value_index > 0) {
|
||||
canvas_draw_str(canvas, 73, item_text_y, "<");
|
||||
}
|
||||
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
(115 + 73) / 2 + 1,
|
||||
item_text_y,
|
||||
AlignCenter,
|
||||
AlignBottom,
|
||||
string_get_cstr(item->current_value_text));
|
||||
|
||||
if(item->current_value_index < (item->values_count - 1)) {
|
||||
canvas_draw_str(canvas, 115, item_text_y, ">");
|
||||
}
|
||||
}
|
||||
|
||||
position++;
|
||||
}
|
||||
|
||||
elements_scrollbar(canvas, model->position, VariableItemArray_size(model->items));
|
||||
}
|
||||
|
||||
void variable_item_list_set_selected_item(VariableItemList* variable_item_list, uint8_t index) {
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
uint8_t position = index;
|
||||
if(position >= VariableItemArray_size(model->items)) {
|
||||
position = 0;
|
||||
}
|
||||
|
||||
model->position = position;
|
||||
model->window_position = position;
|
||||
|
||||
if(model->window_position > 0) {
|
||||
model->window_position -= 1;
|
||||
}
|
||||
|
||||
if(VariableItemArray_size(model->items) <= 4) {
|
||||
model->window_position = 0;
|
||||
} else {
|
||||
if(model->window_position >= (VariableItemArray_size(model->items) - 4)) {
|
||||
model->window_position = (VariableItemArray_size(model->items) - 4);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
uint8_t variable_item_list_get_selected_item_index(VariableItemList* variable_item_list) {
|
||||
VariableItemListModel* model = view_get_model(variable_item_list->view);
|
||||
uint8_t idx = model->position;
|
||||
view_commit_model(variable_item_list->view, false);
|
||||
return idx;
|
||||
}
|
||||
|
||||
static bool variable_item_list_input_callback(InputEvent* event, void* context) {
|
||||
VariableItemList* variable_item_list = context;
|
||||
furi_assert(variable_item_list);
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeShort) {
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
consumed = true;
|
||||
variable_item_list_process_up(variable_item_list);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
consumed = true;
|
||||
variable_item_list_process_down(variable_item_list);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
consumed = true;
|
||||
variable_item_list_process_left(variable_item_list);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
consumed = true;
|
||||
variable_item_list_process_right(variable_item_list);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
variable_item_list_process_ok(variable_item_list);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
consumed = true;
|
||||
variable_item_list_process_up(variable_item_list);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
consumed = true;
|
||||
variable_item_list_process_down(variable_item_list);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
consumed = true;
|
||||
variable_item_list_process_left(variable_item_list);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
consumed = true;
|
||||
variable_item_list_process_right(variable_item_list);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void variable_item_list_process_up(VariableItemList* variable_item_list) {
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
uint8_t items_on_screen = 4;
|
||||
if(model->position > 0) {
|
||||
model->position--;
|
||||
if(((model->position - model->window_position) < 1) &&
|
||||
model->window_position > 0) {
|
||||
model->window_position--;
|
||||
}
|
||||
} else {
|
||||
model->position = VariableItemArray_size(model->items) - 1;
|
||||
if(model->position > (items_on_screen - 1)) {
|
||||
model->window_position = model->position - (items_on_screen - 1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void variable_item_list_process_down(VariableItemList* variable_item_list) {
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
uint8_t items_on_screen = 4;
|
||||
if(model->position < (VariableItemArray_size(model->items) - 1)) {
|
||||
model->position++;
|
||||
if((model->position - model->window_position) > (items_on_screen - 2) &&
|
||||
model->window_position <
|
||||
(VariableItemArray_size(model->items) - items_on_screen)) {
|
||||
model->window_position++;
|
||||
}
|
||||
} else {
|
||||
model->position = 0;
|
||||
model->window_position = 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
VariableItem* variable_item_list_get_selected_item(VariableItemListModel* model) {
|
||||
VariableItem* item = NULL;
|
||||
|
||||
VariableItemArray_it_t it;
|
||||
uint8_t position = 0;
|
||||
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
|
||||
VariableItemArray_next(it)) {
|
||||
if(position == model->position) {
|
||||
break;
|
||||
}
|
||||
position++;
|
||||
}
|
||||
|
||||
item = VariableItemArray_ref(it);
|
||||
|
||||
furi_assert(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
void variable_item_list_process_left(VariableItemList* variable_item_list) {
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
VariableItem* item = variable_item_list_get_selected_item(model);
|
||||
if(item->current_value_index > 0) {
|
||||
item->current_value_index--;
|
||||
if(item->change_callback) {
|
||||
item->change_callback(item);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void variable_item_list_process_right(VariableItemList* variable_item_list) {
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
VariableItem* item = variable_item_list_get_selected_item(model);
|
||||
if(item->current_value_index < (item->values_count - 1)) {
|
||||
item->current_value_index++;
|
||||
if(item->change_callback) {
|
||||
item->change_callback(item);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void variable_item_list_process_ok(VariableItemList* variable_item_list) {
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
if(variable_item_list->callback) {
|
||||
variable_item_list->callback(variable_item_list->context, model->position);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
VariableItemList* variable_item_list_alloc() {
|
||||
VariableItemList* variable_item_list = malloc(sizeof(VariableItemList));
|
||||
variable_item_list->view = view_alloc();
|
||||
view_set_context(variable_item_list->view, variable_item_list);
|
||||
view_allocate_model(
|
||||
variable_item_list->view, ViewModelTypeLocking, sizeof(VariableItemListModel));
|
||||
view_set_draw_callback(variable_item_list->view, variable_item_list_draw_callback);
|
||||
view_set_input_callback(variable_item_list->view, variable_item_list_input_callback);
|
||||
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
VariableItemArray_init(model->items);
|
||||
model->position = 0;
|
||||
model->window_position = 0;
|
||||
return true;
|
||||
});
|
||||
|
||||
return variable_item_list;
|
||||
}
|
||||
|
||||
void variable_item_list_free(VariableItemList* variable_item_list) {
|
||||
furi_assert(variable_item_list);
|
||||
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
VariableItemArray_it_t it;
|
||||
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
|
||||
VariableItemArray_next(it)) {
|
||||
string_clear(VariableItemArray_ref(it)->current_value_text);
|
||||
}
|
||||
VariableItemArray_clear(model->items);
|
||||
return false;
|
||||
});
|
||||
view_free(variable_item_list->view);
|
||||
free(variable_item_list);
|
||||
}
|
||||
|
||||
void variable_item_list_reset(VariableItemList* variable_item_list) {
|
||||
furi_assert(variable_item_list);
|
||||
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
VariableItemArray_it_t it;
|
||||
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
|
||||
VariableItemArray_next(it)) {
|
||||
string_clear(VariableItemArray_ref(it)->current_value_text);
|
||||
}
|
||||
VariableItemArray_reset(model->items);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
View* variable_item_list_get_view(VariableItemList* variable_item_list) {
|
||||
furi_assert(variable_item_list);
|
||||
return variable_item_list->view;
|
||||
}
|
||||
|
||||
VariableItem* variable_item_list_add(
|
||||
VariableItemList* variable_item_list,
|
||||
const char* label,
|
||||
uint8_t values_count,
|
||||
VariableItemChangeCallback change_callback,
|
||||
void* context) {
|
||||
VariableItem* item = NULL;
|
||||
furi_assert(label);
|
||||
furi_assert(variable_item_list);
|
||||
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
item = VariableItemArray_push_new(model->items);
|
||||
item->label = label;
|
||||
item->values_count = values_count;
|
||||
item->change_callback = change_callback;
|
||||
item->context = context;
|
||||
item->current_value_index = 0;
|
||||
string_init(item->current_value_text);
|
||||
return true;
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
void variable_item_list_set_enter_callback(
|
||||
VariableItemList* variable_item_list,
|
||||
VariableItemListEnterCallback callback,
|
||||
void* context) {
|
||||
furi_assert(callback);
|
||||
with_view_model(
|
||||
variable_item_list->view, (VariableItemListModel * model) {
|
||||
UNUSED(model);
|
||||
variable_item_list->callback = callback;
|
||||
variable_item_list->context = context;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index) {
|
||||
item->current_value_index = current_value_index;
|
||||
}
|
||||
|
||||
void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text) {
|
||||
string_set_str(item->current_value_text, current_value_text);
|
||||
}
|
||||
|
||||
uint8_t variable_item_get_current_value_index(VariableItem* item) {
|
||||
return item->current_value_index;
|
||||
}
|
||||
|
||||
void* variable_item_get_context(VariableItem* item) {
|
||||
return item->context;
|
||||
}
|
109
applications/services/gui/modules/variable_item_list.h
Normal file
109
applications/services/gui/modules/variable_item_list.h
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file variable_item_list.h
|
||||
* GUI: VariableItemList view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct VariableItemList VariableItemList;
|
||||
typedef struct VariableItem VariableItem;
|
||||
typedef void (*VariableItemChangeCallback)(VariableItem* item);
|
||||
typedef void (*VariableItemListEnterCallback)(void* context, uint32_t index);
|
||||
|
||||
/** Allocate and initialize VariableItemList
|
||||
*
|
||||
* @return VariableItemList*
|
||||
*/
|
||||
VariableItemList* variable_item_list_alloc();
|
||||
|
||||
/** Deinitialize and free VariableItemList
|
||||
*
|
||||
* @param variable_item_list VariableItemList instance
|
||||
*/
|
||||
void variable_item_list_free(VariableItemList* variable_item_list);
|
||||
|
||||
/** Clear all elements from list
|
||||
*
|
||||
* @param variable_item_list VariableItemList instance
|
||||
*/
|
||||
void variable_item_list_reset(VariableItemList* variable_item_list);
|
||||
|
||||
/** Get VariableItemList View instance
|
||||
*
|
||||
* @param variable_item_list VariableItemList instance
|
||||
*
|
||||
* @return View instance
|
||||
*/
|
||||
View* variable_item_list_get_view(VariableItemList* variable_item_list);
|
||||
|
||||
/** Add item to VariableItemList
|
||||
*
|
||||
* @param variable_item_list VariableItemList instance
|
||||
* @param label item name
|
||||
* @param values_count item values count
|
||||
* @param change_callback called on value change in gui
|
||||
* @param context item context
|
||||
*
|
||||
* @return VariableItem* item instance
|
||||
*/
|
||||
VariableItem* variable_item_list_add(
|
||||
VariableItemList* variable_item_list,
|
||||
const char* label,
|
||||
uint8_t values_count,
|
||||
VariableItemChangeCallback change_callback,
|
||||
void* context);
|
||||
|
||||
/** Set enter callback
|
||||
*
|
||||
* @param variable_item_list VariableItemList instance
|
||||
* @param callback VariableItemListEnterCallback instance
|
||||
* @param context pointer to context
|
||||
*/
|
||||
void variable_item_list_set_enter_callback(
|
||||
VariableItemList* variable_item_list,
|
||||
VariableItemListEnterCallback callback,
|
||||
void* context);
|
||||
|
||||
void variable_item_list_set_selected_item(VariableItemList* variable_item_list, uint8_t index);
|
||||
|
||||
uint8_t variable_item_list_get_selected_item_index(VariableItemList* variable_item_list);
|
||||
|
||||
/** Set item current selected index
|
||||
*
|
||||
* @param item VariableItem* instance
|
||||
* @param current_value_index The current value index
|
||||
*/
|
||||
void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index);
|
||||
|
||||
/** Set item current selected text
|
||||
*
|
||||
* @param item VariableItem* instance
|
||||
* @param current_value_text The current value text
|
||||
*/
|
||||
void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text);
|
||||
|
||||
/** Get item current selected index
|
||||
*
|
||||
* @param item VariableItem* instance
|
||||
*
|
||||
* @return uint8_t current selected index
|
||||
*/
|
||||
uint8_t variable_item_get_current_value_index(VariableItem* item);
|
||||
|
||||
/** Get item context
|
||||
*
|
||||
* @param item VariableItem* instance
|
||||
*
|
||||
* @return void* item context
|
||||
*/
|
||||
void* variable_item_get_context(VariableItem* item);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
208
applications/services/gui/modules/widget.c
Normal file
208
applications/services/gui/modules/widget.c
Normal file
@@ -0,0 +1,208 @@
|
||||
#include <furi.h>
|
||||
#include "widget.h"
|
||||
#include <m-array.h>
|
||||
#include "widget_elements/widget_element_i.h"
|
||||
|
||||
ARRAY_DEF(ElementArray, WidgetElement*, M_PTR_OPLIST);
|
||||
|
||||
struct Widget {
|
||||
View* view;
|
||||
void* context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
ElementArray_t element;
|
||||
} GuiWidgetModel;
|
||||
|
||||
static void gui_widget_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
GuiWidgetModel* model = _model;
|
||||
canvas_clear(canvas);
|
||||
|
||||
// Draw all elements
|
||||
ElementArray_it_t it;
|
||||
ElementArray_it(it, model->element);
|
||||
while(!ElementArray_end_p(it)) {
|
||||
WidgetElement* element = *ElementArray_ref(it);
|
||||
if(element->draw != NULL) {
|
||||
element->draw(canvas, element);
|
||||
}
|
||||
ElementArray_next(it);
|
||||
}
|
||||
}
|
||||
|
||||
static bool gui_widget_view_input_callback(InputEvent* event, void* context) {
|
||||
Widget* widget = context;
|
||||
bool consumed = false;
|
||||
|
||||
// Call all Widget Elements input handlers
|
||||
with_view_model(
|
||||
widget->view, (GuiWidgetModel * model) {
|
||||
ElementArray_it_t it;
|
||||
ElementArray_it(it, model->element);
|
||||
while(!ElementArray_end_p(it)) {
|
||||
WidgetElement* element = *ElementArray_ref(it);
|
||||
if(element->input != NULL) {
|
||||
consumed |= element->input(event, element);
|
||||
}
|
||||
ElementArray_next(it);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
Widget* widget_alloc() {
|
||||
Widget* widget = malloc(sizeof(Widget));
|
||||
widget->view = view_alloc();
|
||||
view_set_context(widget->view, widget);
|
||||
view_allocate_model(widget->view, ViewModelTypeLocking, sizeof(GuiWidgetModel));
|
||||
view_set_draw_callback(widget->view, gui_widget_view_draw_callback);
|
||||
view_set_input_callback(widget->view, gui_widget_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
widget->view, (GuiWidgetModel * model) {
|
||||
ElementArray_init(model->element);
|
||||
return true;
|
||||
});
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
void widget_reset(Widget* widget) {
|
||||
furi_assert(widget);
|
||||
|
||||
with_view_model(
|
||||
widget->view, (GuiWidgetModel * model) {
|
||||
ElementArray_it_t it;
|
||||
ElementArray_it(it, model->element);
|
||||
while(!ElementArray_end_p(it)) {
|
||||
WidgetElement* element = *ElementArray_ref(it);
|
||||
furi_assert(element->free);
|
||||
element->free(element);
|
||||
ElementArray_next(it);
|
||||
}
|
||||
ElementArray_reset(model->element);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void widget_free(Widget* widget) {
|
||||
furi_assert(widget);
|
||||
// Free all elements
|
||||
widget_reset(widget);
|
||||
// Free elements container
|
||||
with_view_model(
|
||||
widget->view, (GuiWidgetModel * model) {
|
||||
ElementArray_clear(model->element);
|
||||
return true;
|
||||
});
|
||||
|
||||
view_free(widget->view);
|
||||
free(widget);
|
||||
}
|
||||
|
||||
View* widget_get_view(Widget* widget) {
|
||||
furi_assert(widget);
|
||||
return widget->view;
|
||||
}
|
||||
|
||||
static void widget_add_element(Widget* widget, WidgetElement* element) {
|
||||
furi_assert(widget);
|
||||
furi_assert(element);
|
||||
|
||||
with_view_model(
|
||||
widget->view, (GuiWidgetModel * model) {
|
||||
element->parent = widget;
|
||||
ElementArray_push_back(model->element, element);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void widget_add_string_multiline_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text) {
|
||||
furi_assert(widget);
|
||||
WidgetElement* string_multiline_element =
|
||||
widget_element_string_multiline_create(x, y, horizontal, vertical, font, text);
|
||||
widget_add_element(widget, string_multiline_element);
|
||||
}
|
||||
|
||||
void widget_add_string_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text) {
|
||||
furi_assert(widget);
|
||||
WidgetElement* string_element =
|
||||
widget_element_string_create(x, y, horizontal, vertical, font, text);
|
||||
widget_add_element(widget, string_element);
|
||||
}
|
||||
|
||||
void widget_add_text_box_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
const char* text,
|
||||
bool strip_to_dots) {
|
||||
furi_assert(widget);
|
||||
WidgetElement* text_box_element = widget_element_text_box_create(
|
||||
x, y, width, height, horizontal, vertical, text, strip_to_dots);
|
||||
widget_add_element(widget, text_box_element);
|
||||
}
|
||||
|
||||
void widget_add_text_scroll_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
const char* text) {
|
||||
furi_assert(widget);
|
||||
WidgetElement* text_scroll_element =
|
||||
widget_element_text_scroll_create(x, y, width, height, text);
|
||||
widget_add_element(widget, text_scroll_element);
|
||||
}
|
||||
|
||||
void widget_add_button_element(
|
||||
Widget* widget,
|
||||
GuiButtonType button_type,
|
||||
const char* text,
|
||||
ButtonCallback callback,
|
||||
void* context) {
|
||||
furi_assert(widget);
|
||||
WidgetElement* button_element =
|
||||
widget_element_button_create(button_type, text, callback, context);
|
||||
widget_add_element(widget, button_element);
|
||||
}
|
||||
|
||||
void widget_add_icon_element(Widget* widget, uint8_t x, uint8_t y, const Icon* icon) {
|
||||
furi_assert(widget);
|
||||
furi_assert(icon);
|
||||
WidgetElement* icon_element = widget_element_icon_create(x, y, icon);
|
||||
widget_add_element(widget, icon_element);
|
||||
}
|
||||
|
||||
void widget_add_frame_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
uint8_t radius) {
|
||||
furi_assert(widget);
|
||||
WidgetElement* frame_element = widget_element_frame_create(x, y, width, height, radius);
|
||||
widget_add_element(widget, frame_element);
|
||||
}
|
172
applications/services/gui/modules/widget.h
Normal file
172
applications/services/gui/modules/widget.h
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* @file widget.h
|
||||
* GUI: Widget view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <gui/view.h>
|
||||
#include "widget_elements/widget_element.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct Widget Widget;
|
||||
typedef struct WidgetElement WidgetElement;
|
||||
|
||||
/** Allocate Widget that holds Widget Elements
|
||||
*
|
||||
* @return Widget instance
|
||||
*/
|
||||
Widget* widget_alloc();
|
||||
|
||||
/** Free Widget
|
||||
* @note this function free allocated Widget Elements
|
||||
*
|
||||
* @param widget Widget instance
|
||||
*/
|
||||
void widget_free(Widget* widget);
|
||||
|
||||
/** Reset Widget
|
||||
*
|
||||
* @param widget Widget instance
|
||||
*/
|
||||
void widget_reset(Widget* widget);
|
||||
|
||||
/** Get Widget view
|
||||
*
|
||||
* @param widget Widget instance
|
||||
*
|
||||
* @return View instance
|
||||
*/
|
||||
View* widget_get_view(Widget* widget);
|
||||
|
||||
/** Add Multi String Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param horizontal Align instance
|
||||
* @param vertical Align instance
|
||||
* @param font Font instance
|
||||
* @param[in] text The text
|
||||
*/
|
||||
void widget_add_string_multiline_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text);
|
||||
|
||||
/** Add String Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param horizontal Align instance
|
||||
* @param vertical Align instance
|
||||
* @param font Font instance
|
||||
* @param[in] text The text
|
||||
*/
|
||||
void widget_add_string_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text);
|
||||
|
||||
/** Add Text Box Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param width width to fit text
|
||||
* @param height height to fit text
|
||||
* @param horizontal Align instance
|
||||
* @param vertical Align instance
|
||||
* @param[in] text Formatted text. The following formats are available:
|
||||
* "\e#Bold text\e#" - bold font is used
|
||||
* "\e*Monospaced text\e*" - monospaced font is used
|
||||
* "\e#Inversed text\e#" - white text on black background
|
||||
* @param strip_to_dots Strip text to ... if does not fit to width
|
||||
*/
|
||||
void widget_add_text_box_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
const char* text,
|
||||
bool strip_to_dots);
|
||||
|
||||
/** Add Text Scroll Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param width width to fit text
|
||||
* @param height height to fit text
|
||||
* @param[in] text Formatted text. Default format: align left, Secondary font.
|
||||
* The following formats are available:
|
||||
* "\e#Bold text" - sets bold font before until next '\n' symbol
|
||||
* "\ecCenter-aligned text" - sets center horizontal align until the next '\n' symbol
|
||||
* "\erRight-aligned text" - sets right horizontal align until the next '\n' symbol
|
||||
*/
|
||||
void widget_add_text_scroll_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
const char* text);
|
||||
|
||||
/** Add Button Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param button_type GuiButtonType instance
|
||||
* @param text text on allocated button
|
||||
* @param callback ButtonCallback instance
|
||||
* @param context pointer to context
|
||||
*/
|
||||
void widget_add_button_element(
|
||||
Widget* widget,
|
||||
GuiButtonType button_type,
|
||||
const char* text,
|
||||
ButtonCallback callback,
|
||||
void* context);
|
||||
|
||||
/** Add Icon Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param x top left x coordinate
|
||||
* @param y top left y coordinate
|
||||
* @param icon Icon instance
|
||||
*/
|
||||
void widget_add_icon_element(Widget* widget, uint8_t x, uint8_t y, const Icon* icon);
|
||||
|
||||
/** Add Frame Element
|
||||
*
|
||||
* @param widget Widget instance
|
||||
* @param x top left x coordinate
|
||||
* @param y top left y coordinate
|
||||
* @param width frame width
|
||||
* @param height frame height
|
||||
* @param radius frame radius
|
||||
*/
|
||||
void widget_add_frame_element(
|
||||
Widget* widget,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
uint8_t radius);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @file widget_element_i.h
|
||||
* GUI: internal Widget Element API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
GuiButtonTypeLeft,
|
||||
GuiButtonTypeCenter,
|
||||
GuiButtonTypeRight,
|
||||
} GuiButtonType;
|
||||
|
||||
typedef void (*ButtonCallback)(GuiButtonType result, InputType type, void* context);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,79 @@
|
||||
#include "widget_element_i.h"
|
||||
#include <gui/elements.h>
|
||||
#include <m-string.h>
|
||||
|
||||
typedef struct {
|
||||
GuiButtonType button_type;
|
||||
string_t text;
|
||||
ButtonCallback callback;
|
||||
void* context;
|
||||
} GuiButtonModel;
|
||||
|
||||
static void gui_button_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
GuiButtonModel* model = element->model;
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
if(model->button_type == GuiButtonTypeLeft) {
|
||||
elements_button_left(canvas, string_get_cstr(model->text));
|
||||
} else if(model->button_type == GuiButtonTypeRight) {
|
||||
elements_button_right(canvas, string_get_cstr(model->text));
|
||||
} else if(model->button_type == GuiButtonTypeCenter) {
|
||||
elements_button_center(canvas, string_get_cstr(model->text));
|
||||
}
|
||||
}
|
||||
|
||||
static bool gui_button_input(InputEvent* event, WidgetElement* element) {
|
||||
GuiButtonModel* model = element->model;
|
||||
bool consumed = false;
|
||||
|
||||
if(model->callback == NULL) return consumed;
|
||||
|
||||
if((model->button_type == GuiButtonTypeLeft) && (event->key == InputKeyLeft)) {
|
||||
model->callback(model->button_type, event->type, model->context);
|
||||
consumed = true;
|
||||
} else if((model->button_type == GuiButtonTypeRight) && (event->key == InputKeyRight)) {
|
||||
model->callback(model->button_type, event->type, model->context);
|
||||
consumed = true;
|
||||
} else if((model->button_type == GuiButtonTypeCenter) && (event->key == InputKeyOk)) {
|
||||
model->callback(model->button_type, event->type, model->context);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
static void gui_button_free(WidgetElement* gui_button) {
|
||||
furi_assert(gui_button);
|
||||
|
||||
GuiButtonModel* model = gui_button->model;
|
||||
string_clear(model->text);
|
||||
free(gui_button->model);
|
||||
free(gui_button);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_button_create(
|
||||
GuiButtonType button_type,
|
||||
const char* text,
|
||||
ButtonCallback callback,
|
||||
void* context) {
|
||||
// Allocate and init model
|
||||
GuiButtonModel* model = malloc(sizeof(GuiButtonModel));
|
||||
model->button_type = button_type;
|
||||
model->callback = callback;
|
||||
model->context = context;
|
||||
string_init_set_str(model->text, text);
|
||||
|
||||
// Allocate and init Element
|
||||
WidgetElement* gui_button = malloc(sizeof(WidgetElement));
|
||||
gui_button->parent = NULL;
|
||||
gui_button->input = gui_button_input;
|
||||
gui_button->draw = gui_button_draw;
|
||||
gui_button->free = gui_button_free;
|
||||
gui_button->model = model;
|
||||
|
||||
return gui_button;
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
#include "widget_element_i.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
uint8_t radius;
|
||||
} GuiFrameModel;
|
||||
|
||||
static void gui_frame_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
GuiFrameModel* model = element->model;
|
||||
canvas_draw_rframe(canvas, model->x, model->y, model->width, model->height, model->radius);
|
||||
}
|
||||
|
||||
static void gui_frame_free(WidgetElement* gui_frame) {
|
||||
furi_assert(gui_frame);
|
||||
|
||||
free(gui_frame->model);
|
||||
free(gui_frame);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_frame_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
uint8_t radius) {
|
||||
// Allocate and init model
|
||||
GuiFrameModel* model = malloc(sizeof(GuiFrameModel));
|
||||
model->x = x;
|
||||
model->y = y;
|
||||
model->width = width;
|
||||
model->height = height;
|
||||
model->radius = radius;
|
||||
|
||||
// Allocate and init Element
|
||||
WidgetElement* gui_frame = malloc(sizeof(WidgetElement));
|
||||
gui_frame->parent = NULL;
|
||||
gui_frame->input = NULL;
|
||||
gui_frame->draw = gui_frame_draw;
|
||||
gui_frame->free = gui_frame_free;
|
||||
gui_frame->model = model;
|
||||
|
||||
return gui_frame;
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @file widget_element_i.h
|
||||
* GUI: internal Widget Element API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <furi.h>
|
||||
#include <gui/view.h>
|
||||
#include <input/input.h>
|
||||
#include "widget_element.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct WidgetElement WidgetElement;
|
||||
typedef struct Widget Widget;
|
||||
|
||||
struct WidgetElement {
|
||||
// generic draw and input callbacks
|
||||
void (*draw)(Canvas* canvas, WidgetElement* element);
|
||||
bool (*input)(InputEvent* event, WidgetElement* element);
|
||||
|
||||
// free callback
|
||||
void (*free)(WidgetElement* element);
|
||||
|
||||
// generic model holder
|
||||
void* model;
|
||||
FuriMutex* model_mutex;
|
||||
|
||||
// pointer to widget that hold our element
|
||||
Widget* parent;
|
||||
};
|
||||
|
||||
/** Create multi string element */
|
||||
WidgetElement* widget_element_string_multiline_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text);
|
||||
|
||||
/** Create string element */
|
||||
WidgetElement* widget_element_string_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text);
|
||||
|
||||
/** Create text box element */
|
||||
WidgetElement* widget_element_text_box_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
const char* text,
|
||||
bool strip_to_dots);
|
||||
|
||||
/** Create button element */
|
||||
WidgetElement* widget_element_button_create(
|
||||
GuiButtonType button_type,
|
||||
const char* text,
|
||||
ButtonCallback callback,
|
||||
void* context);
|
||||
|
||||
/** Create icon element */
|
||||
WidgetElement* widget_element_icon_create(uint8_t x, uint8_t y, const Icon* icon);
|
||||
|
||||
/** Create frame element */
|
||||
WidgetElement* widget_element_frame_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
uint8_t radius);
|
||||
|
||||
WidgetElement* widget_element_text_scroll_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
const char* text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,44 @@
|
||||
#include "widget_element_i.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
const Icon* icon;
|
||||
} GuiIconModel;
|
||||
|
||||
static void gui_icon_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
GuiIconModel* model = element->model;
|
||||
|
||||
if(model->icon) {
|
||||
canvas_draw_icon(canvas, model->x, model->y, model->icon);
|
||||
}
|
||||
}
|
||||
|
||||
static void gui_icon_free(WidgetElement* gui_icon) {
|
||||
furi_assert(gui_icon);
|
||||
|
||||
free(gui_icon->model);
|
||||
free(gui_icon);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_icon_create(uint8_t x, uint8_t y, const Icon* icon) {
|
||||
furi_assert(icon);
|
||||
|
||||
// Allocate and init model
|
||||
GuiIconModel* model = malloc(sizeof(GuiIconModel));
|
||||
model->x = x;
|
||||
model->y = y;
|
||||
model->icon = icon;
|
||||
|
||||
// Allocate and init Element
|
||||
WidgetElement* gui_icon = malloc(sizeof(WidgetElement));
|
||||
gui_icon->parent = NULL;
|
||||
gui_icon->input = NULL;
|
||||
gui_icon->draw = gui_icon_draw;
|
||||
gui_icon->free = gui_icon_free;
|
||||
gui_icon->model = model;
|
||||
|
||||
return gui_icon;
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
#include "widget_element_i.h"
|
||||
#include <m-string.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
Align horizontal;
|
||||
Align vertical;
|
||||
Font font;
|
||||
string_t text;
|
||||
} GuiStringModel;
|
||||
|
||||
static void gui_string_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
GuiStringModel* model = element->model;
|
||||
|
||||
if(string_size(model->text)) {
|
||||
canvas_set_font(canvas, model->font);
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
model->x,
|
||||
model->y,
|
||||
model->horizontal,
|
||||
model->vertical,
|
||||
string_get_cstr(model->text));
|
||||
}
|
||||
}
|
||||
|
||||
static void gui_string_free(WidgetElement* gui_string) {
|
||||
furi_assert(gui_string);
|
||||
|
||||
GuiStringModel* model = gui_string->model;
|
||||
string_clear(model->text);
|
||||
free(gui_string->model);
|
||||
free(gui_string);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_string_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text) {
|
||||
furi_assert(text);
|
||||
|
||||
// Allocate and init model
|
||||
GuiStringModel* model = malloc(sizeof(GuiStringModel));
|
||||
model->x = x;
|
||||
model->y = y;
|
||||
model->horizontal = horizontal;
|
||||
model->vertical = vertical;
|
||||
model->font = font;
|
||||
string_init_set_str(model->text, text);
|
||||
|
||||
// Allocate and init Element
|
||||
WidgetElement* gui_string = malloc(sizeof(WidgetElement));
|
||||
gui_string->parent = NULL;
|
||||
gui_string->input = NULL;
|
||||
gui_string->draw = gui_string_draw;
|
||||
gui_string->free = gui_string_free;
|
||||
gui_string->model = model;
|
||||
|
||||
return gui_string;
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
#include "widget_element_i.h"
|
||||
#include <m-string.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
Align horizontal;
|
||||
Align vertical;
|
||||
Font font;
|
||||
string_t text;
|
||||
} GuiStringMultiLineModel;
|
||||
|
||||
static void gui_string_multiline_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
GuiStringMultiLineModel* model = element->model;
|
||||
|
||||
if(string_size(model->text)) {
|
||||
canvas_set_font(canvas, model->font);
|
||||
elements_multiline_text_aligned(
|
||||
canvas,
|
||||
model->x,
|
||||
model->y,
|
||||
model->horizontal,
|
||||
model->vertical,
|
||||
string_get_cstr(model->text));
|
||||
}
|
||||
}
|
||||
|
||||
static void gui_string_multiline_free(WidgetElement* gui_string) {
|
||||
furi_assert(gui_string);
|
||||
|
||||
GuiStringMultiLineModel* model = gui_string->model;
|
||||
string_clear(model->text);
|
||||
free(gui_string->model);
|
||||
free(gui_string);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_string_multiline_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
Font font,
|
||||
const char* text) {
|
||||
furi_assert(text);
|
||||
|
||||
// Allocate and init model
|
||||
GuiStringMultiLineModel* model = malloc(sizeof(GuiStringMultiLineModel));
|
||||
model->x = x;
|
||||
model->y = y;
|
||||
model->horizontal = horizontal;
|
||||
model->vertical = vertical;
|
||||
model->font = font;
|
||||
string_init_set_str(model->text, text);
|
||||
|
||||
// Allocate and init Element
|
||||
WidgetElement* gui_string = malloc(sizeof(WidgetElement));
|
||||
gui_string->parent = NULL;
|
||||
gui_string->input = NULL;
|
||||
gui_string->draw = gui_string_multiline_draw;
|
||||
gui_string->free = gui_string_multiline_free;
|
||||
gui_string->model = model;
|
||||
|
||||
return gui_string;
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
#include "widget_element_i.h"
|
||||
#include <m-string.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
Align horizontal;
|
||||
Align vertical;
|
||||
string_t text;
|
||||
bool strip_to_dots;
|
||||
} GuiTextBoxModel;
|
||||
|
||||
static void gui_text_box_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
GuiTextBoxModel* model = element->model;
|
||||
|
||||
if(string_size(model->text)) {
|
||||
elements_text_box(
|
||||
canvas,
|
||||
model->x,
|
||||
model->y,
|
||||
model->width,
|
||||
model->height,
|
||||
model->horizontal,
|
||||
model->vertical,
|
||||
string_get_cstr(model->text),
|
||||
model->strip_to_dots);
|
||||
}
|
||||
}
|
||||
|
||||
static void gui_text_box_free(WidgetElement* gui_string) {
|
||||
furi_assert(gui_string);
|
||||
|
||||
GuiTextBoxModel* model = gui_string->model;
|
||||
string_clear(model->text);
|
||||
free(gui_string->model);
|
||||
free(gui_string);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_text_box_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
Align horizontal,
|
||||
Align vertical,
|
||||
const char* text,
|
||||
bool strip_to_dots) {
|
||||
furi_assert(text);
|
||||
|
||||
// Allocate and init model
|
||||
GuiTextBoxModel* model = malloc(sizeof(GuiTextBoxModel));
|
||||
model->x = x;
|
||||
model->y = y;
|
||||
model->width = width;
|
||||
model->height = height;
|
||||
model->horizontal = horizontal;
|
||||
model->vertical = vertical;
|
||||
string_init_set_str(model->text, text);
|
||||
model->strip_to_dots = strip_to_dots;
|
||||
|
||||
// Allocate and init Element
|
||||
WidgetElement* gui_string = malloc(sizeof(WidgetElement));
|
||||
gui_string->parent = NULL;
|
||||
gui_string->input = NULL;
|
||||
gui_string->draw = gui_text_box_draw;
|
||||
gui_string->free = gui_text_box_free;
|
||||
gui_string->model = model;
|
||||
|
||||
return gui_string;
|
||||
}
|
@@ -0,0 +1,245 @@
|
||||
#include "widget_element_i.h"
|
||||
#include <m-string.h>
|
||||
#include <gui/elements.h>
|
||||
#include <m-array.h>
|
||||
|
||||
#define WIDGET_ELEMENT_TEXT_SCROLL_BAR_OFFSET (4)
|
||||
|
||||
typedef struct {
|
||||
Font font;
|
||||
Align horizontal;
|
||||
string_t text;
|
||||
} TextScrollLineArray;
|
||||
|
||||
ARRAY_DEF(TextScrollLineArray, TextScrollLineArray, M_POD_OPLIST)
|
||||
|
||||
typedef struct {
|
||||
TextScrollLineArray_t line_array;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
string_t text;
|
||||
uint8_t scroll_pos_total;
|
||||
uint8_t scroll_pos_current;
|
||||
bool text_formatted;
|
||||
} WidgetElementTextScrollModel;
|
||||
|
||||
static bool
|
||||
widget_element_text_scroll_process_ctrl_symbols(TextScrollLineArray* line, string_t text) {
|
||||
bool processed = false;
|
||||
|
||||
do {
|
||||
if(string_get_char(text, 0) != '\e') break;
|
||||
char ctrl_symbol = string_get_char(text, 1);
|
||||
if(ctrl_symbol == 'c') {
|
||||
line->horizontal = AlignCenter;
|
||||
} else if(ctrl_symbol == 'r') {
|
||||
line->horizontal = AlignRight;
|
||||
} else if(ctrl_symbol == '#') {
|
||||
line->font = FontPrimary;
|
||||
}
|
||||
string_right(text, 2);
|
||||
processed = true;
|
||||
} while(false);
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
void widget_element_text_scroll_add_line(WidgetElement* element, TextScrollLineArray* line) {
|
||||
WidgetElementTextScrollModel* model = element->model;
|
||||
TextScrollLineArray new_line;
|
||||
new_line.font = line->font;
|
||||
new_line.horizontal = line->horizontal;
|
||||
string_init_set(new_line.text, line->text);
|
||||
TextScrollLineArray_push_back(model->line_array, new_line);
|
||||
}
|
||||
|
||||
static void widget_element_text_scroll_fill_lines(Canvas* canvas, WidgetElement* element) {
|
||||
WidgetElementTextScrollModel* model = element->model;
|
||||
TextScrollLineArray line_tmp;
|
||||
bool all_text_processed = false;
|
||||
string_init(line_tmp.text);
|
||||
bool reached_new_line = true;
|
||||
uint16_t total_height = 0;
|
||||
|
||||
while(!all_text_processed) {
|
||||
if(reached_new_line) {
|
||||
// Set default line properties
|
||||
line_tmp.font = FontSecondary;
|
||||
line_tmp.horizontal = AlignLeft;
|
||||
string_reset(line_tmp.text);
|
||||
// Process control symbols
|
||||
while(widget_element_text_scroll_process_ctrl_symbols(&line_tmp, model->text))
|
||||
;
|
||||
}
|
||||
// Set canvas font
|
||||
canvas_set_font(canvas, line_tmp.font);
|
||||
CanvasFontParameters* params = canvas_get_font_params(canvas, line_tmp.font);
|
||||
total_height += params->height;
|
||||
if(total_height > model->height) {
|
||||
model->scroll_pos_total++;
|
||||
}
|
||||
|
||||
uint8_t line_width = 0;
|
||||
uint16_t char_i = 0;
|
||||
while(true) {
|
||||
char next_char = string_get_char(model->text, char_i++);
|
||||
if(next_char == '\0') {
|
||||
string_push_back(line_tmp.text, '\0');
|
||||
widget_element_text_scroll_add_line(element, &line_tmp);
|
||||
total_height += params->leading_default - params->height;
|
||||
all_text_processed = true;
|
||||
break;
|
||||
} else if(next_char == '\n') {
|
||||
string_push_back(line_tmp.text, '\0');
|
||||
widget_element_text_scroll_add_line(element, &line_tmp);
|
||||
string_right(model->text, char_i);
|
||||
total_height += params->leading_default - params->height;
|
||||
reached_new_line = true;
|
||||
break;
|
||||
} else {
|
||||
line_width += canvas_glyph_width(canvas, next_char);
|
||||
if(line_width > model->width) {
|
||||
string_push_back(line_tmp.text, '\0');
|
||||
widget_element_text_scroll_add_line(element, &line_tmp);
|
||||
string_right(model->text, char_i - 1);
|
||||
string_reset(line_tmp.text);
|
||||
total_height += params->leading_default - params->height;
|
||||
reached_new_line = false;
|
||||
break;
|
||||
} else {
|
||||
string_push_back(line_tmp.text, next_char);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string_clear(line_tmp.text);
|
||||
}
|
||||
|
||||
static void widget_element_text_scroll_draw(Canvas* canvas, WidgetElement* element) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(element);
|
||||
|
||||
furi_mutex_acquire(element->model_mutex, FuriWaitForever);
|
||||
|
||||
WidgetElementTextScrollModel* model = element->model;
|
||||
if(!model->text_formatted) {
|
||||
widget_element_text_scroll_fill_lines(canvas, element);
|
||||
model->text_formatted = true;
|
||||
}
|
||||
|
||||
uint8_t y = model->y;
|
||||
uint8_t x = model->x;
|
||||
uint16_t curr_line = 0;
|
||||
if(TextScrollLineArray_size(model->line_array)) {
|
||||
TextScrollLineArray_it_t it;
|
||||
for(TextScrollLineArray_it(it, model->line_array); !TextScrollLineArray_end_p(it);
|
||||
TextScrollLineArray_next(it), curr_line++) {
|
||||
if(curr_line < model->scroll_pos_current) continue;
|
||||
TextScrollLineArray* line = TextScrollLineArray_ref(it);
|
||||
CanvasFontParameters* params = canvas_get_font_params(canvas, line->font);
|
||||
if(y + params->descender > model->y + model->height) break;
|
||||
canvas_set_font(canvas, line->font);
|
||||
if(line->horizontal == AlignLeft) {
|
||||
x = model->x;
|
||||
} else if(line->horizontal == AlignCenter) {
|
||||
x = (model->x + model->width) / 2;
|
||||
} else if(line->horizontal == AlignRight) {
|
||||
x = model->x + model->width;
|
||||
}
|
||||
canvas_draw_str_aligned(
|
||||
canvas, x, y, line->horizontal, AlignTop, string_get_cstr(line->text));
|
||||
y += params->leading_default;
|
||||
}
|
||||
// Draw scroll bar
|
||||
if(model->scroll_pos_total > 1) {
|
||||
elements_scrollbar_pos(
|
||||
canvas,
|
||||
model->x + model->width + WIDGET_ELEMENT_TEXT_SCROLL_BAR_OFFSET,
|
||||
model->y,
|
||||
model->height,
|
||||
model->scroll_pos_current,
|
||||
model->scroll_pos_total);
|
||||
}
|
||||
}
|
||||
|
||||
furi_mutex_release(element->model_mutex);
|
||||
}
|
||||
|
||||
static bool widget_element_text_scroll_input(InputEvent* event, WidgetElement* element) {
|
||||
furi_assert(event);
|
||||
furi_assert(element);
|
||||
|
||||
furi_mutex_acquire(element->model_mutex, FuriWaitForever);
|
||||
|
||||
WidgetElementTextScrollModel* model = element->model;
|
||||
bool consumed = false;
|
||||
|
||||
if((event->type == InputTypeShort) || (event->type == InputTypeRepeat)) {
|
||||
if(event->key == InputKeyUp) {
|
||||
if(model->scroll_pos_current > 0) {
|
||||
model->scroll_pos_current--;
|
||||
}
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyDown) {
|
||||
if((model->scroll_pos_total > 1) &&
|
||||
(model->scroll_pos_current < model->scroll_pos_total - 1)) {
|
||||
model->scroll_pos_current++;
|
||||
}
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
furi_mutex_release(element->model_mutex);
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
static void widget_element_text_scroll_free(WidgetElement* text_scroll) {
|
||||
furi_assert(text_scroll);
|
||||
|
||||
WidgetElementTextScrollModel* model = text_scroll->model;
|
||||
TextScrollLineArray_it_t it;
|
||||
for(TextScrollLineArray_it(it, model->line_array); !TextScrollLineArray_end_p(it);
|
||||
TextScrollLineArray_next(it)) {
|
||||
TextScrollLineArray* line = TextScrollLineArray_ref(it);
|
||||
string_clear(line->text);
|
||||
}
|
||||
TextScrollLineArray_clear(model->line_array);
|
||||
string_clear(model->text);
|
||||
free(text_scroll->model);
|
||||
furi_mutex_free(text_scroll->model_mutex);
|
||||
free(text_scroll);
|
||||
}
|
||||
|
||||
WidgetElement* widget_element_text_scroll_create(
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t width,
|
||||
uint8_t height,
|
||||
const char* text) {
|
||||
furi_assert(text);
|
||||
|
||||
// Allocate and init model
|
||||
WidgetElementTextScrollModel* model = malloc(sizeof(WidgetElementTextScrollModel));
|
||||
model->x = x;
|
||||
model->y = y;
|
||||
model->width = width - WIDGET_ELEMENT_TEXT_SCROLL_BAR_OFFSET;
|
||||
model->height = height;
|
||||
model->scroll_pos_current = 0;
|
||||
model->scroll_pos_total = 1;
|
||||
TextScrollLineArray_init(model->line_array);
|
||||
string_init_set_str(model->text, text);
|
||||
|
||||
WidgetElement* text_scroll = malloc(sizeof(WidgetElement));
|
||||
text_scroll->parent = NULL;
|
||||
text_scroll->draw = widget_element_text_scroll_draw;
|
||||
text_scroll->input = widget_element_text_scroll_input;
|
||||
text_scroll->free = widget_element_text_scroll_free;
|
||||
text_scroll->model = model;
|
||||
text_scroll->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
|
||||
return text_scroll;
|
||||
}
|
Reference in New Issue
Block a user