Files
edgetx-gx12/radio/src/lua/lua_widget.cpp
T

539 lines
14 KiB
C++

/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "lua_widget.h"
#include "lua_api.h"
#include "lua_event.h"
#include "lua_widget_factory.h"
#include "touch.h"
#include "view_main.h"
#define MAX_INSTRUCTIONS (20000 / 100)
#if defined(HARDWARE_TOUCH)
uint32_t LuaEventHandler::downTime = 0;
uint32_t LuaEventHandler::tapTime = 0;
uint32_t LuaEventHandler::tapCount = 0;
tmr10ms_t LuaEventHandler::swipeTimeOut = 0;
coord_t LuaEventHandler::_startX;
coord_t LuaEventHandler::_startY;
bool LuaEventHandler::_sliding = false;
#endif
void LuaEventHandler::event_cb(lv_event_t* e)
{
auto obj = lv_event_get_target(e);
auto win = (Window*)lv_obj_get_user_data(obj);
if (!win) return;
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_KEY) {
uint32_t key = *(uint32_t*)lv_event_get_param(e);
if (key == LV_KEY_LEFT) {
win->onEvent(EVT_ROTARY_LEFT);
} else if (key == LV_KEY_RIGHT) {
win->onEvent(EVT_ROTARY_RIGHT);
}
} else if (code == LV_EVENT_LONG_PRESSED) {
lv_indev_type_t indev_type = lv_indev_get_type(lv_indev_get_act());
if (indev_type != LV_INDEV_TYPE_POINTER) {
// Do not use `lv_indev_wait_release()` as some LUA scripts
// rely on EVT_KEY_BREAK(KEY_ENTER) being generated on key release
luaPushEvent(EVT_KEY_LONG(KEY_ENTER));
}
}
#if defined(HARDWARE_TOUCH)
else if (code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING) {
lv_area_t obj_coords;
lv_obj_get_coords(obj, &obj_coords);
lv_point_t point_act;
lv_indev_t* click_source = (lv_indev_t*)lv_event_get_param(e);
lv_indev_get_point(click_source, &point_act);
// Ignore event from keypad
if (point_act.x < 0 || point_act.y < 0) return;
lv_point_t rel_pos;
rel_pos.x = point_act.x - obj_coords.x1;
rel_pos.y = point_act.y - obj_coords.y1;
if (code == LV_EVENT_PRESSING) {
lv_point_t vect_act;
lv_indev_get_vect(click_source, &vect_act);
if ((abs(vect_act.x) >= SLIDE_RANGE) || (abs(vect_act.y) >= SLIDE_RANGE))
_sliding = true;
if (!_sliding) return;
TRACE_WINDOWS("LuaWidget touch slide");
// If we already have a SLIDE going, then accumulate slide values instead
// of allocating an empty slot
LuaEventData* es = luaGetEventSlot(EVT_TOUCH_SLIDE);
if (es) {
es->event = EVT_TOUCH_SLIDE;
es->touchX = rel_pos.x;
es->touchY = rel_pos.y;
es->startX = _startX;
es->startY = _startY;
es->slideX += vect_act.x;
es->slideY += vect_act.y;
TRACE("EVT_TOUCH_SLIDE [%d,%d]", rel_pos.x, rel_pos.y);
}
} else {
LuaEventData* es = luaGetEventSlot();
if (es) {
es->event = EVT_TOUCH_FIRST;
es->touchX = rel_pos.x;
es->touchY = rel_pos.y;
TRACE("EVT_TOUCH_FIRST [%d,%d]", rel_pos.x, rel_pos.y);
}
_startX = rel_pos.x;
_startY = rel_pos.y;
downTime = RTOS_GET_MS();
}
} else if (code == LV_EVENT_RELEASED) {
// tap count handling
uint32_t now = RTOS_GET_MS();
if (now - downTime <= LUA_TAP_TIME) {
if (now - tapTime > LUA_TAP_TIME) {
tapCount = 1;
} else {
tapCount++;
}
tapTime = now;
} else {
tapCount = 0;
}
}
#endif
}
void LuaEventHandler::onClicked()
{
#if defined(HARDWARE_TOUCH)
auto click_source = lv_indev_get_act();
bool is_pointer = lv_indev_get_type(click_source) == LV_INDEV_TYPE_POINTER;
if (is_pointer) {
lv_point_t point_act;
lv_indev_get_point(click_source, &point_act);
LuaEventData* es = luaGetEventSlot();
if (!es) return;
if (tapCount > 0) {
es->event = EVT_TOUCH_TAP;
es->tapCount = tapCount;
} else {
es->event = EVT_TOUCH_BREAK;
}
es->touchX = point_act.x;
es->touchY = point_act.y;
_sliding = false;
return;
}
#endif
luaPushEvent(EVT_KEY_BREAK(KEY_ENTER));
}
void LuaEventHandler::onCancel() {
luaPushEvent(EVT_KEY_BREAK(KEY_EXIT));
}
void LuaEventHandler::onEvent(event_t event)
{
luaPushEvent(event);
}
void LuaEventHandler::setupHandler(Window* w)
{
lv_obj_t* obj = w->getLvObj();
lv_obj_add_event_cb(obj, LuaEventHandler::event_cb, LV_EVENT_ALL, nullptr);
}
void LuaEventHandler::removeHandler(Window* w)
{
lv_obj_t* obj = w->getLvObj();
lv_obj_remove_event_cb(obj, LuaEventHandler::event_cb);
}
void LuaWidget::redraw_cb(lv_event_t* e)
{
lv_obj_t* target = lv_event_get_target(e);
if (lv_obj_has_flag(target, LV_OBJ_FLAG_HIDDEN)) return;
LuaWidget* widget = (LuaWidget*)lv_obj_get_user_data(target);
if (widget) {
if (widget->useLvglLayout()) {
if (!widget->isCreated()) {
widget->update();
widget->setCreated();
}
} else {
lv_draw_ctx_t* draw_ctx = lv_event_get_draw_ctx(e);
auto w = draw_ctx->buf_area->x2 - draw_ctx->buf_area->x1 + 1;
auto h = draw_ctx->buf_area->y2 - draw_ctx->buf_area->y1 + 1;
TRACE_WINDOWS("Draw %s", widget->getWindowDebugString().c_str());
BitmapBuffer buf = {BMP_RGB565, (uint16_t)w, (uint16_t)h,
(uint16_t*)draw_ctx->buf};
buf.setDrawCtx(draw_ctx);
buf.setOffset(draw_ctx->clip_area->x1 - draw_ctx->buf_area->x1, draw_ctx->clip_area->y1 - draw_ctx->buf_area->y1);
buf.setClippingRect(draw_ctx->clip_area->x1 - draw_ctx->buf_area->x1, draw_ctx->clip_area->x2 - draw_ctx->buf_area->x1 + 1,
draw_ctx->clip_area->y1 - draw_ctx->buf_area->y1, draw_ctx->clip_area->y2 - draw_ctx->buf_area->y1 + 1);
widget->refresh(&buf);
}
}
}
LuaWidget::LuaWidget(const WidgetFactory* factory, Window* parent,
const rect_t& rect, WidgetPersistentData* persistentData,
int luaWidgetDataRef, int zoneRectDataRef) :
Widget(factory, parent, rect, persistentData),
zoneRectDataRef(zoneRectDataRef),
errorMessage(nullptr)
{
this->luaWidgetDataRef = luaWidgetDataRef;
lv_obj_add_event_cb(lvobj, LuaWidget::redraw_cb, LV_EVENT_DRAW_MAIN,
nullptr);
}
LuaWidget::~LuaWidget()
{
luaL_unref(lsWidgets, LUA_REGISTRYINDEX, luaWidgetDataRef);
luaL_unref(lsWidgets, LUA_REGISTRYINDEX, zoneRectDataRef);
free(errorMessage);
}
void LuaWidget::onClicked()
{
if (!fullscreen) {
ButtonBase::onClicked();
return;
}
LuaEventHandler::onClicked();
}
void LuaWidget::onCancel()
{
if (!fullscreen) {
ButtonBase::onCancel();
return;
}
LuaEventHandler::onCancel();
}
void LuaWidget::checkEvents()
{
Widget::checkEvents();
// paint has not been called
if (!refreshed) {
if (isCreated())
background();
refreshed = true;
}
refreshed = false;
if (useLvglLayout()) {
if (!lv_obj_has_flag(lvobj, LV_OBJ_FLAG_HIDDEN) && isCreated()) {
PROTECT_LUA() {
luaLvglManager = this;
refresh(nullptr);
if (!errorMessage) {
if (!callRefs(lsWidgets)) {
setErrorMessage("callRefs()");
}
}
refreshInstructionsPercent = instructionsPercent;
} else {
// TODO: error handling
}
luaLvglManager = nullptr;
UNPROTECT_LUA();
}
} else {
invalidate();
}
#if defined(DEBUG_WINDOWS)
TRACE_WINDOWS("# refresh: %s", getWindowDebugString().c_str());
#endif
}
static void l_pushtableint(const char* key, int value)
{
lua_pushstring(lsWidgets, key);
lua_pushinteger(lsWidgets, value);
lua_settable(lsWidgets, -3);
}
void LuaWidget::update()
{
Widget::update();
if (lsWidgets == 0 || errorMessage) return;
luaSetInstructionsLimit(lsWidgets, MAX_INSTRUCTIONS);
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, luaFactory()->updateFunction);
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, luaWidgetDataRef);
lua_newtable(lsWidgets);
int i = 0;
for (const ZoneOption* option = getOptions(); option->name; option++, i++) {
if (option->type == ZoneOption::String) {
lua_pushstring(lsWidgets, option->name);
char str[LEN_ZONE_OPTION_STRING + 1] = {
0}; // Zero-terminated string for Lua
strncpy(str, persistentData->options[i].value.stringValue,
LEN_ZONE_OPTION_STRING);
lua_pushstring(lsWidgets, &str[0]);
lua_settable(lsWidgets, -3);
} else if (option->type == ZoneOption::Color) {
int32_t value = persistentData->options[i].value.unsignedValue;
l_pushtableint(option->name, value);
} else {
int32_t value = persistentData->options[i].value.signedValue;
l_pushtableint(option->name, value);
}
}
if (useLvglLayout()) luaLvglManager = this;
if (lua_pcall(lsWidgets, 2, 0, 0) != 0)
setErrorMessage("update()");
luaLvglManager = nullptr;
}
// Update table on top of Lua stack - set entry with name 'idx' to value 'val'
// Return true if value has changed
bool LuaWidget::updateTable(const char* idx, int val)
{
bool update = false;
// Check existing value (or invalid value)
lua_getfield(lsWidgets, -1, idx);
if (lua_isnumber(lsWidgets, -1)) {
int v = lua_tointeger(lsWidgets, -1);
update = (v != val);
} else {
// Force table update
update = true;
}
lua_pop(lsWidgets, 1);
if (update) {
lua_pushinteger(lsWidgets, val);
lua_setfield(lsWidgets, -2, idx);
}
return update;
}
void LuaWidget::updateZoneRect(rect_t rect, bool updateUI)
{
if (lsWidgets) {
// Update widget zone with current size and position
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, zoneRectDataRef);
bool changed = false;
if (updateTable("w", rect.w)) changed = true;
if (updateTable("h", rect.h)) changed = true;
if (updateTable("xabs", rect.x)) changed = true;
if (updateTable("yabs", rect.y)) changed = true;
lua_pop(lsWidgets, 1);
if (changed && updateUI)
update();
}
}
void LuaWidget::onFullscreen(bool enable)
{
if (enable) {
setupHandler(this);
} else {
removeHandler(this);
luaEmptyEventBuffer();
}
}
void LuaWidget::setErrorMessage(const char* funcName)
{
const char* lua_err = lua_tostring(lsWidgets, -1);
TRACE("Error in widget %s %s function: %s", factory->getName(), funcName,
lua_err);
TRACE("Widget disabled");
size_t err_len = snprintf(NULL, 0, "ERROR in %s: %s", funcName, lua_err);
errorMessage = (char*)malloc(err_len + 1);
if (errorMessage) {
snprintf(errorMessage, err_len, "ERROR in %s: %s", funcName, lua_err);
errorMessage[err_len] = '\0';
}
}
const char* LuaWidget::getErrorMessage() const { return errorMessage; }
void LuaWidget::refresh(BitmapBuffer* dc)
{
if (lsWidgets == 0) return;
if (errorMessage) {
if (dc) {
dc->drawTextLines(0, 0, fullscreen ? LCD_W : rect.w,
fullscreen ? LCD_H : rect.h, errorMessage,
FONT(XS) | COLOR_THEME_WARNING);
} else {
if (errorLabel == nullptr) {
errorLabel = lv_label_create(lvobj);
lv_obj_set_pos(errorLabel, 0, 0);
lv_obj_set_size(errorLabel, width(), height());
lv_label_set_long_mode(errorLabel, LV_LABEL_LONG_WRAP);
etx_txt_color(errorLabel, COLOR_THEME_WARNING_INDEX);
etx_font(errorLabel, FONT_XS_INDEX);
etx_bg_color(errorLabel, COLOR_THEME_SECONDARY3_INDEX);
etx_obj_add_style(errorLabel, styles->bg_opacity_75, LV_PART_MAIN);
}
lv_label_set_text(errorLabel, errorMessage);
}
return;
}
luaSetInstructionsLimit(lsWidgets, MAX_INSTRUCTIONS);
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, luaFactory()->refreshFunction);
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, luaWidgetDataRef);
// Pass key event to fullscreen Lua widget
LuaEventData evt;
luaNextEvent(&evt);
if (fullscreen) {
lua_pushinteger(lsWidgets, evt.event);
} else
lua_pushnil(lsWidgets);
#if defined(HARDWARE_TOUCH)
if (fullscreen && IS_TOUCH_EVENT(evt.event)) {
luaPushTouchEventTable(lsWidgets, &evt);
} else
#endif
lua_pushnil(lsWidgets);
// Enable drawing into the current LCD buffer
luaLcdBuffer = dc;
// This little hack is needed to not interfere with the LCD usage of preempted
// scripts
bool lla = luaLcdAllowed;
luaLcdAllowed = true;
runningFS = this;
if (lua_pcall(lsWidgets, 3, 0, 0) != 0) {
setErrorMessage("refresh()");
}
runningFS = nullptr;
// Remove LCD
luaLcdAllowed = lla;
luaLcdBuffer = nullptr;
// mark as refreshed
refreshed = true;
}
void LuaWidget::background()
{
if (lsWidgets == 0 || errorMessage) return;
// TRACE("LuaWidget::background()");
luaSetInstructionsLimit(lsWidgets, MAX_INSTRUCTIONS);
if (luaFactory()->backgroundFunction) {
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, luaFactory()->backgroundFunction);
lua_rawgeti(lsWidgets, LUA_REGISTRYINDEX, luaWidgetDataRef);
runningFS = this;
if (lua_pcall(lsWidgets, 1, 0, 0) != 0) {
setErrorMessage("background()");
}
runningFS = nullptr;
}
}
void LuaWidget::onEvent(event_t event)
{
if (fullscreen) {
LuaEventHandler::onEvent(event);
}
Widget::onEvent(event);
}
void LuaWidget::clear()
{
clearRefs(lsWidgets);
Widget::clear();
}
bool LuaWidget::useLvglLayout() const { return luaFactory()->useLvglLayout(); }
void LuaLvglManager::clearRefs(lua_State *L)
{
for (size_t i = 0; i < lvglObjectRefs.size(); i += 1) {
lua_rawgeti(L, LUA_REGISTRYINDEX, lvglObjectRefs[i]);
auto lvobj = LvglWidgetObject::checkLvgl(L, -1);
lvobj->clearRefs(L);
luaL_unref(L, LUA_REGISTRYINDEX, lvglObjectRefs[i]);
}
lvglObjectRefs.clear();
}
bool LuaLvglManager::callRefs(lua_State *L)
{
bool rv = true;
for (size_t i = 0; i < lvglObjectRefs.size(); i += 1) {
lua_rawgeti(L, LUA_REGISTRYINDEX, lvglObjectRefs[i]);
auto lvobj = LvglWidgetObject::checkLvgl(L, -1);
lvobj->callRefs(L);
}
return rv;
}