[lvgl] Add lvgl.widget.focus action and related triggers. (#7315)

This commit is contained in:
Clyde Stubbs 2024-08-28 14:29:41 +10:00 committed by GitHub
parent 458a8970b6
commit d6df466237
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 253 additions and 27 deletions

View File

@ -21,8 +21,8 @@ from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, update_to_code
from .defines import CONF_SKIP
from .automation import disp_update, focused_widgets, update_to_code
from .defines import CONF_ADJUSTABLE, CONF_SKIP
from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
@ -67,7 +67,7 @@ from .widgets.lv_bar import bar_spec
from .widgets.meter import meter_spec
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
from .widgets.obj import obj_spec
from .widgets.page import add_pages, page_spec
from .widgets.page import add_pages, generate_page_triggers, page_spec
from .widgets.roller import roller_spec
from .widgets.slider import slider_spec
from .widgets.spinbox import spinbox_spec
@ -182,6 +182,14 @@ def final_validation(config):
raise cv.Invalid(
"Using RGBA or RGB24 in image config not compatible with LVGL", path
)
for w in focused_widgets:
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
if CONF_ADJUSTABLE in widget_conf and not widget_conf[CONF_ADJUSTABLE]:
raise cv.Invalid(
"A non adjustable arc may not be focused",
path,
)
async def to_code(config):
@ -271,6 +279,7 @@ async def to_code(config):
Widget.set_completed()
async with LvContext(lv_component):
await generate_triggers(lv_component)
await generate_page_triggers(lv_component, config)
for conf in config.get(CONF_ON_IDLE, ()):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)

View File

@ -4,13 +4,15 @@ from typing import Callable
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TIMEOUT
from esphome.cpp_generator import RawExpression
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
from esphome.cpp_generator import RawExpression, get_variable
from esphome.cpp_types import nullptr
from .defines import (
CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE,
CONF_EDITING,
CONF_FREEZE,
CONF_LVGL_ID,
CONF_SHOW_SNOW,
literal,
@ -30,6 +32,7 @@ from .lvcode import (
lv_expr,
lv_obj,
lvgl_comp,
static_cast,
)
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
from .types import (
@ -38,7 +41,9 @@ from .types import (
LvglCondition,
ObjUpdateAction,
lv_disp_t,
lv_group_t,
lv_obj_t,
lv_pseudo_button_t,
)
from .widgets import (
Widget,
@ -48,6 +53,9 @@ from .widgets import (
wait_for_widgets,
)
# Record widgets that are used in a focused action here
focused_widgets = set()
async def action_to_code(
widgets: list[Widget],
@ -234,3 +242,72 @@ async def obj_show_to_code(config, action_id, template_arg, args):
return await action_to_code(
await get_widgets(config), do_show, action_id, template_arg, args
)
def focused_id(value):
value = cv.use_id(lv_pseudo_button_t)(value)
focused_widgets.add(value)
return value
@automation.register_action(
"lvgl.widget.focus",
ObjUpdateAction,
cv.Any(
cv.maybe_simple_value(
{
cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
cv.Required(CONF_ACTION): cv.one_of(
"MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True
),
cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent),
cv.Optional(CONF_FREEZE, default=False): cv.boolean,
},
key=CONF_ACTION,
),
cv.maybe_simple_value(
{
cv.Required(CONF_ID): focused_id,
cv.Optional(CONF_FREEZE, default=False): cv.boolean,
cv.Optional(CONF_EDITING, default=False): cv.boolean,
},
key=CONF_ID,
),
),
)
async def widget_focus(config, action_id, template_arg, args):
widget = await get_widgets(config)
if widget:
widget = widget[0]
group = static_cast(
lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj)
)
elif group := config.get(CONF_GROUP):
group = await get_variable(group)
else:
group = lv_expr.group_get_default()
async with LambdaContext(parameters=args, where=action_id) as context:
if widget:
lv.group_focus_freeze(group, False)
lv.group_focus_obj(widget.obj)
if config[CONF_EDITING]:
lv.group_set_editing(group, True)
else:
action = config[CONF_ACTION]
lv_comp = await get_variable(config[CONF_LVGL_ID])
if action == "MARK":
context.add(lv_comp.set_focus_mark(group))
else:
lv.group_focus_freeze(group, False)
if action == "RESTORE":
context.add(lv_comp.restore_focus_mark(group))
elif action == "NEXT":
lv.group_focus_next(group)
else:
lv.group_focus_prev(group)
if config[CONF_FREEZE]:
lv.group_focus_freeze(group, True)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
return var

View File

@ -148,6 +148,7 @@ LV_EVENT_MAP = {
"DEFOCUS": "DEFOCUSED",
"READY": "READY",
"CANCEL": "CANCEL",
"ALL_EVENTS": "ALL",
}
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
@ -390,6 +391,7 @@ CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_GROUP = "default_group"
CONF_DIR = "dir"
CONF_DISPLAYS = "displays"
CONF_EDITING = "editing"
CONF_ENCODERS = "encoders"
CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value"
@ -401,6 +403,7 @@ CONF_FLEX_ALIGN_MAIN = "flex_align_main"
CONF_FLEX_ALIGN_CROSS = "flex_align_cross"
CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow"
CONF_FREEZE = "freeze"
CONF_FULL_REFRESH = "full_refresh"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
@ -428,9 +431,9 @@ CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next"
CONF_PAD_ROW = "pad_row"
CONF_PAD_COLUMN = "pad_column"

View File

@ -28,7 +28,7 @@ LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp()
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)]
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
EVENT_ARG = [(lv_event_t_ptr, "ev")]
EVENT_ARG = [(lv_event_t_ptr, "event")]
# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction;
# UPDATE_EVENT is fired when an entity is programmatically updated locally.
# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction.
@ -291,6 +291,10 @@ class LvExpr(MockLv):
pass
def static_cast(type, value):
return literal(f"static_cast<{type}>({value})")
# Top level mock for generic lv_ calls to be recorded
lv = MockLv("lv_")
# Just generate an expression

View File

@ -15,6 +15,60 @@ static void log_cb(const char *buf) {
}
#endif // LV_USE_LOG
static const char *const EVENT_NAMES[] = {
"NONE",
"PRESSED",
"PRESSING",
"PRESS_LOST",
"SHORT_CLICKED",
"LONG_PRESSED",
"LONG_PRESSED_REPEAT",
"CLICKED",
"RELEASED",
"SCROLL_BEGIN",
"SCROLL_END",
"SCROLL",
"GESTURE",
"KEY",
"FOCUSED",
"DEFOCUSED",
"LEAVE",
"HIT_TEST",
"COVER_CHECK",
"REFR_EXT_DRAW_SIZE",
"DRAW_MAIN_BEGIN",
"DRAW_MAIN",
"DRAW_MAIN_END",
"DRAW_POST_BEGIN",
"DRAW_POST",
"DRAW_POST_END",
"DRAW_PART_BEGIN",
"DRAW_PART_END",
"VALUE_CHANGED",
"INSERT",
"REFRESH",
"READY",
"CANCEL",
"DELETE",
"CHILD_CHANGED",
"CHILD_CREATED",
"CHILD_DELETED",
"SCREEN_UNLOAD_START",
"SCREEN_LOAD_START",
"SCREEN_LOADED",
"SCREEN_UNLOADED",
"SIZE_CHANGED",
"STYLE_CHANGED",
"LAYOUT_CHANGED",
"GET_SELF_SIZE",
};
std::string lv_event_code_name_for(uint8_t event_code) {
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
return EVENT_NAMES[event_code];
}
return str_sprintf("%2d", event_code);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
// make sure all coordinates are even
if (area->x1 & 1)

View File

@ -40,6 +40,7 @@ namespace lvgl {
extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT
extern std::string lv_event_code_name_for(uint8_t event_code);
extern bool lv_is_pre_initialise();
#ifdef USE_LVGL_COLOR
inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
@ -143,6 +144,13 @@ class LvglComponent : public PollingComponent {
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
void restore_focus_mark(lv_group_t *group) {
auto *mark = this->focus_marks_[group];
if (mark != nullptr) {
lv_group_focus_obj(mark);
}
}
protected:
void write_random_();
@ -158,6 +166,7 @@ class LvglComponent : public PollingComponent {
bool show_snow_{};
lv_coord_t snow_line_{};
bool page_wrap_{true};
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{};

View File

@ -20,7 +20,7 @@ from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_image
from .lvcode import LvglComponent
from .lvcode import LvglComponent, lv_event_t_ptr
from .types import (
LVEncoderListener,
LvType,
@ -215,14 +215,12 @@ def automation_schema(typ: LvType):
events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,)
else:
events = df.LV_EVENT_TRIGGERS
if isinstance(typ, LvType):
template = Trigger.template(typ.get_arg_type())
else:
template = Trigger.template()
args = [typ.get_arg_type()] if isinstance(typ, LvType) else []
args.append(lv_event_t_ptr)
return {
cv.Optional(event): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(template),
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template(*args)),
}
)
for event in events

View File

@ -19,6 +19,7 @@ from .lvcode import (
LvConditional,
lv,
lv_add,
lv_event_t_ptr,
)
from .types import LV_EVENT
from .widgets import widget_map
@ -65,10 +66,10 @@ async def generate_triggers(lv_component):
async def add_trigger(conf, lv_component, w, *events):
tid = conf[CONF_TRIGGER_ID]
trigger = cg.new_Pvariable(tid)
args = w.get_args()
args = w.get_args() + [(lv_event_t_ptr, "event")]
value = w.get_value()
await automation.build_automation(trigger, args, conf)
async with LambdaContext(EVENT_ARG, where=tid) as context:
with LvConditional(w.is_selected()):
lv_add(trigger.trigger(value))
lv_add(trigger.trigger(value, literal("event")))
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events))

View File

@ -57,7 +57,7 @@ lv_group_t = cg.global_ns.struct("lv_group_t")
LVTouchListener = lvgl_ns.class_("LVTouchListener")
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t")
lv_page_t = cg.global_ns.class_("LvPageType", LvCompound)
lv_page_t = LvType("LvPageType", parents=(LvCompound,))
lv_img_t = LvType("lv_img_t")
LV_EVENT = MockObj(base="LV_EVENT_", op="")

View File

@ -225,7 +225,7 @@ def get_widget_generator(wid):
yield
async def get_widget_(wid: Widget):
async def get_widget_(wid):
if obj := widget_map.get(wid):
return obj
return await FakeAwaitable(get_widget_generator(wid))
@ -348,8 +348,6 @@ async def set_obj_properties(w: Widget, config):
if group := config.get(CONF_GROUP):
group = await cg.get_variable(group)
lv.group_add_obj(group, w.obj)
flag_clr = set()
flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT]
lambs = {}
flag_set = set()

View File

@ -1,5 +1,6 @@
import esphome.config_validation as cv
from esphome.const import (
CONF_GROUP,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_MODE,
@ -20,7 +21,7 @@ from ..defines import (
literal,
)
from ..lv_validation import angle, get_start_value, lv_float
from ..lvcode import lv, lv_obj
from ..lvcode import lv, lv_expr, lv_obj
from ..types import LvNumber, NumberType
from . import Widget
@ -69,6 +70,9 @@ class ArcType(NumberType):
if config.get(CONF_ADJUSTABLE) is False:
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
elif CONF_GROUP not in config:
# For some reason arc does not get automatically added to the default group
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
value = await get_start_value(config)
if value is not None:

View File

@ -1,6 +1,7 @@
from esphome import automation, codegen as cg
from esphome.automation import Trigger
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
from ..defines import (
CONF_ANIMATION,
@ -9,12 +10,39 @@ from ..defines import (
CONF_PAGE_WRAP,
CONF_SKIP,
LV_ANIM,
literal,
)
from ..lv_validation import lv_bool, lv_milliseconds
from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
from ..lvcode import (
EVENT_ARG,
LVGL_COMP_ARG,
LambdaContext,
add_line_marks,
lv_add,
lvgl_comp,
)
from ..schemas import LVGL_SCHEMA
from ..types import LvglAction, lv_page_t
from . import Widget, WidgetType, add_widgets, set_obj_properties
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
CONF_ON_LOAD = "on_load"
CONF_ON_UNLOAD = "on_unload"
PAGE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SKIP, default=False): lv_bool,
cv.Optional(CONF_ON_LOAD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()),
}
),
cv.Optional(CONF_ON_UNLOAD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()),
}
),
}
)
class PageType(WidgetType):
@ -23,9 +51,8 @@ class PageType(WidgetType):
CONF_PAGE,
lv_page_t,
(),
{
cv.Optional(CONF_SKIP, default=False): lv_bool,
},
PAGE_SCHEMA,
modify_schema={},
)
async def to_code(self, w: Widget, config: dict):
@ -39,7 +66,6 @@ SHOW_SCHEMA = LVGL_SCHEMA.extend(
}
)
page_spec = PageType()
@ -111,3 +137,21 @@ async def add_pages(lv_component, config):
await set_obj_properties(page, config)
await set_obj_properties(page, pconf)
await add_widgets(page, pconf)
async def generate_page_triggers(lv_component, config):
for pconf in config.get(CONF_PAGES, ()):
page = (await get_widgets(pconf))[0]
for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD):
for loaded in pconf.get(ev, ()):
trigger = cg.new_Pvariable(loaded[CONF_TRIGGER_ID])
await automation.build_automation(trigger, [], loaded)
async with LambdaContext(EVENT_ARG, where=id) as context:
lv_add(trigger.trigger())
lv_add(
lv_component.add_event_cb(
page.obj,
await context.get_lambda(),
literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"),
)
)

View File

@ -54,6 +54,17 @@ lvgl:
long_press_time: 500ms
pages:
- id: page1
on_load:
- logger.log: page loaded
- lvgl.widget.focus:
action: restore
on_unload:
- logger.log: page unloaded
- lvgl.widget.focus: mark
on_all_events:
logger.log:
format: "Event %s"
args: ['lv_event_code_name_for(event->code).c_str()']
skip: true
layout:
type: flex
@ -70,6 +81,10 @@ lvgl:
repeat_count: 10
duration: 1s
auto_start: true
on_all_events:
logger.log:
format: "Event %s"
args: ['lv_event_code_name_for(event->code).c_str()']
- label:
id: hello_label
text: Hello world
@ -229,6 +244,16 @@ lvgl:
- label:
text: Button
on_click:
- lvgl.widget.focus: spin_up
- lvgl.widget.focus: next
- lvgl.widget.focus: previous
- lvgl.widget.focus:
action: previous
freeze: true
- lvgl.widget.focus:
id: spin_up
freeze: true
editing: true
- lvgl.label.update:
id: hello_label
bg_color: 0x123456