studio
studio copied to clipboard
[LVGL] Support groups
Please add support for groups (needed for keys and encoders):
- [x] Have a tab with the list of groups (like Global vars). The list of objects assigned to a group should be orderable (like a struct or an enum), maybe per screen?
- [x] Add a group property to objects: Select the groups the object belongs to (multiple selections possible?)
- [x] On screen load event assigns objects to the selected groups in the right order.
I'm finally ready to implement this feature. In the meantime a new feature request, similar to this one, has been created: #487.
I'm taking into consideration what you proposed in this issue and what is proposed in #487. I want this to be as simple as possible.
Have a tab with the list of groups (like Global vars). The list of objects assigned to a group should be orderable (like a struct or an enum), maybe per screen?
Do we need another ordering? We already have an ordered list of widgets inside the page. So, we can use this also for the groups.
And can be added a input device (maybe also in the settings of the created group) like so lv_indev_set_group(encoder_indev, group).
I'm thinking how this should be implemented. To call lv_indev_set_group
we need input_device parameter. We can have an optional property called "Target input devices" for the group definition in the project. With this property we can specify one or more input device names. Input device name is arbitrary name chosen by the user. If specified then a new variable in the screens.h/cpp will be added. For example if for some screen we defined group named group1
and for the "Target input devices" we specified encoder_indev keyboard_indev
then following code will be generated:
In screens.h:
lv_group_t *group1;
lv_indev_t *encoder_indev;
lv_indev_t *keyboard_indev;
Note: User should assign to encoder_indev
and keyboard_indev
variables before ui_init
is called. If not assigned then this variables will be NULL and no lv_indev_set_group
will be called.
In screens.cpp, I will generate this code:
// ...
group1 = lv_group_create();
lv_group_add_obj(group1, objects.obj1);
lv_group_add_obj(group1, objects.obj2);
lv_group_add_obj(group1, objects.obj3);
// ...
And in the screen load event handler I will generate this code:
if (encoder_indev) {
lv_indev_set_group(encoder_indev, group1);
}
if (keyboard_indev) {
lv_indev_set_group(keyboard_indev, group1);
}
Some clarifications:
- we can have multiple groups per single page/screen
- each widget can be member of multiple groups
- so, for example, we can have one group for encoder and another for the keyboard and some widgets can be in both groups, only in encoder group, only in keyboard group or in no group
Another way to implement this is to leave to the user to call lv_indev_set_group
for each input device. In that case, screen load event will look like this:
lv_group_remove_all_objs(encoder_group);
lv_group_add_obj(encoder_group, objects.obj1);
lv_group_add_obj(encoder_group, objects.obj2);
lv_group_add_obj(encoder_group, objects.obj3);
So, we first remove all the objects from the group from previous screen and add a new set of object for the current screen.
This means that groups are global not per page. I think this is simpler model and maybe what you originally proposed.
Do we need another ordering? We already have an ordered list of widgets inside the page. So, we can use this also for the groups.
I do think we need a different ordering: The order in the group dictates the order in which widgets are selected when moving between them with keys. For example: pressing the button of an encoder moves to the next widget (slider for example). It also determines the widget that is selected when the page is entered.
The order in the page determines also the 'Z axis' on the screen. Combining this into a single ordering might cause impossible situations?
The generated code should probably also call lv_group_focus_obj()
with the first object when entering a page.
Yes, probably there are cases when you want different order.
The generated code should probably also call lv_group_focus_obj() with the first object when entering a page.
This is not default behaviour for LVGL. If encoder is not used then why should any widget be in focus?
You are correct. It deviates from LVGL default so it should not be default setting. I'm using it, but it should be a separate action.
This means that groups are global not per page. I think this is simpler model and maybe what you originally proposed.
Looks fine to me: Groups are global and for each group there is a ordered list for each screen: On a screen load only the objects in the ordered list of this specific screen of this specific group are loaded into the group. This is done for every group. Eh, I'm still making sense?
I just found that specific object can be only inside one group at the time. Here is the relevant code from the LVGL:
This is surprise to me, I didn't expect that. Is that makes sense to you?
And there is an API call lv_obj_get_group(obj) to see which group an object belongs to. From this it can also be concluded that the same object can only be in one group.
I was already implemented this feature with the premise that the same object can be in multiple groups:
Now, I need to change that.
BTW this is the code that is auto generated and the code that I needed to add manually:
I also didn’t realize this. It might be logical from a UI perspective: how can you visualize which group (and which inputs) are active for a widget.
BTW I might have missed the ‘default group’ setting for a screen?
BTW I might have missed the ‘default group’ setting for a screen?
I think it is not very useful feature because it doesn't take care which screen is active.
I mean, the object will remain in the default group even if its parent screen is not active anymore.
Clear, default is not very useful for us.
Your code example looks fine.
I compared it to my manual implementation and noticed groups has a configurable property that might be useful:
lv_group_set_wrap
Other properties don’t seem relevant for studio use cases I can think of.
Some functions might be relevant as events you can trigger via the LVGL action. This would allow use cases where pressing a specific button (or other event) selects a certain widget for your encoder or changes the group state:
lv_group_focus_obj
lv_group_focus_next
lv_group_focus_prev
lv_group_focus_freeze
lv_group_set_editing
The _obj function requires an object (widget). The _ next and _prev skip hidden widgets
A good complex example is a device with a row of button next to the screen and 1 generic encoder with ‘<‘ and ‘>’ keys:
- Pressing one of the buttons next to the screen will select a specific widget for the encoder group. This highly depends on the screen state you are in.
- Pressing the ‘<‘ key (or a button on screen) will call _prev, same for >
- Freeze/set edit can be used when you create some kind of pop-up/overlay to lock you into this input.
- I think LVGL doesn’t focus on any object if you load a screen and add objects to a group. The above functions allow a user to determine the status when a screen is entered. For example: In my device on entering the main screen I want the encoder focus to be on the spinbox in edit mode. I now handle this in code.
Group ordering can be defined with the "Group index" property of the widget:
This is similar to tabindex in HTML:
- if "Group index" is 0 then group order is the same as in Widgets Structure
- if "Group index" is > 0 then widget is added to the group before any widget with "Group index" 0 and before any widget with the greater "Group index" value. That is, "Group index"=4 is added before "Group index"=5 and "Group index"=0, but after "Group index"=3. If multiple widgets share the same "Group index" value, their order relative to each other follows their position in the Widgets Structure.
We can select one group to be used for the encoder input in simulator:
and one group for the keyboard:
The same group can be used for both.
Here you can see which widgets are in the group for the selected group and page:
Here you can see which widgets are in the group for the selected group and page:
Looks great! Can’t wait to use this and remove some code. Are the widgets in the widget group panel ordered by the given index number? In that way you can see the expected order of focus with next/previous.
Are the widgets in the widget group panel ordered by the given index number? In that way you can see the expected order of focus with next/previous.
Yes.
I gave it a try on real hardware with an encoder and it is working great!
--delete limitation with user actions, tested it not good enough --
User widgets are currently not supported. I'm adding group actions in LVGL action and after that I will see how to support user widgets.
FYI: My test use case for a user action looks like this:
It is an overlay with 2 buttons. It might be to complicated.
Added LVGL group actions:
Also, I changed how group variables are declared. I needed to put it inside structure (similar to objects), so I can access it sequentially by the index, i.e. to get group object from its index (this is required in the implementation of the group actions in LVGL action):
Groups are now supported for the user widgets too.
I'm having an issue with the group support in user widgets. I can add widgets from a user widget to a group but the group membership is not loaded upon screen load.
This is my user widget:
(Yes and No button are member of encoder group, but this is not shown in the encoder group panel here)
This is my screen (with the above widget in it):
(note the Yes and No button are not shown in the group panel)
The source code generated is (yes no button group adds are missing):
static void event_handler_cb_ranges_ranges(lv_event_t *e) {
lv_event_code_t event = lv_event_get_code(e);
void *flowState = lv_event_get_user_data(e);
if (event == LV_EVENT_SCREEN_LOAD_START) {
e->user_data = (void *)0;
flowPropagateValueLVGLEvent(flowState, 9, 0, e);
}
if (event == LV_EVENT_SCREEN_LOADED) {
// group: encoder_group
lv_group_remove_all_objs(groups.encoder_group);
lv_group_add_obj(groups.encoder_group, objects.current_range);
lv_group_add_obj(groups.encoder_group, objects.volt_range);
lv_group_add_obj(groups.encoder_group, objects.sense);
lv_group_add_obj(groups.encoder_group, objects.nlpc_back);
lv_group_add_obj(groups.encoder_group, objects.nlpc_ok);
lv_group_add_obj(groups.encoder_group, objects.nlpc_cancel);
}
}
Fixing it manually solves it:
static void event_handler_cb_ranges_ranges(lv_event_t *e) {
lv_event_code_t event = lv_event_get_code(e);
void *flowState = lv_event_get_user_data(e);
if (event == LV_EVENT_SCREEN_LOAD_START) {
e->user_data = (void *)0;
flowPropagateValueLVGLEvent(flowState, 9, 0, e);
}
if (event == LV_EVENT_SCREEN_LOADED) {
// group: encoder_group
lv_group_remove_all_objs(groups.encoder_group);
lv_group_add_obj(groups.encoder_group, objects.current_range);
lv_group_add_obj(groups.encoder_group, objects.volt_range);
lv_group_add_obj(groups.encoder_group, objects.sense);
lv_group_add_obj(groups.encoder_group, objects.nlpc_back);
lv_group_add_obj(groups.encoder_group, objects.nlpc_ok);
lv_group_add_obj(groups.encoder_group, objects.nlpc_cancel);
lv_group_add_obj(groups.encoder_group, objects.obj82__sure_but_no);
lv_group_add_obj(groups.encoder_group, objects.obj82__sure_but_yes);
}
}
Are you sure you have the latest version?
I missed a few commits. It is working now in HW and simulator!
I've added ui_create_groups
function in screens.h
, so groups can be created before ui_init
, like this:
#include "src/ui.h"
#include "src/screens.h"
// ...
ui_create_groups();
lv_indev_set_group(enc_indev, groups.encoder_group);
lv_indev_set_group(kb_indev, groups.keyboard_group);
ui_init();
If ui_create_groups
is not called explicitly then groups will be still created in ui_init
. For example, this also works:
ui_init();
lv_indev_set_group(enc_indev, groups.encoder_group);
lv_indev_set_group(kb_indev, groups.keyboard_group);
Apparently, the first way is required on LVGL 9.x otherwise focus on first group member by default will not work.