You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
17 KiB
543 lines
17 KiB
/* |
|
* EEZ Modular Firmware |
|
* Copyright (C) 2015-present, Envox d.o.o. |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation, either version 3 of the License, or |
|
* (at your option) any later version. |
|
|
|
* 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. |
|
|
|
* You should have received a copy of the GNU General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
#include <math.h> |
|
#include <assert.h> |
|
#include <memory.h> |
|
|
|
#include <eez/conf.h> |
|
#include <eez/core/sound.h> |
|
#include <eez/core/os.h> |
|
#include <eez/core/util.h> |
|
|
|
#if OPTION_KEYBOARD |
|
#include <eez/core/keyboard.h> |
|
#endif |
|
|
|
#if OPTION_MOUSE |
|
#include <eez/core/mouse.h> |
|
#endif |
|
|
|
#include <eez/gui/gui.h> |
|
#include <eez/gui/thread.h> |
|
#include <eez/gui/touch_calibration.h> |
|
#include <eez/gui/widgets/button.h> |
|
|
|
#include <eez/flow/debugger.h> |
|
#include <eez/flow/private.h> |
|
|
|
#include <eez/core/hmi.h> |
|
|
|
#define CONF_GUI_TOAST_DURATION_MS 1000L |
|
|
|
namespace eez { |
|
namespace gui { |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
AppContext::AppContext() { |
|
m_updatePageIndex = -1; |
|
} |
|
|
|
void AppContext::stateManagment() { |
|
// remove alert message after period of time |
|
if (getActivePageId() == INTERNAL_PAGE_ID_TOAST_MESSAGE) { |
|
ToastMessagePage *page = (ToastMessagePage *)getActivePage(); |
|
if (!page->hasAction() && eez::hmi::getInactivityPeriodMs() >= CONF_GUI_TOAST_DURATION_MS) { |
|
popPage(); |
|
} |
|
} |
|
} |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
bool AppContext::isActivePageInternal() { |
|
return isPageInternal(getActivePageId()); |
|
} |
|
|
|
bool AppContext::isWidgetActionEnabled(const WidgetCursor &widgetCursor) { |
|
const Widget *widget = widgetCursor.widget; |
|
auto action = getWidgetAction(widgetCursor); |
|
if (action) { |
|
if (widget->type == WIDGET_TYPE_BUTTON) { |
|
auto buttonWidget = (const ButtonWidget *)widget; |
|
auto enabled = get(widgetCursor, buttonWidget->enabled); |
|
if (!(enabled.getType() == VALUE_TYPE_UNDEFINED || enabled.getInt() ? 1 : 0)) { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
bool AppContext::isAutoRepeatAction(int action) { |
|
return false; |
|
} |
|
|
|
bool AppContext::isFocusWidget(const WidgetCursor &widgetCursor) { |
|
return false; |
|
} |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
void AppContext::onPageChanged(int previousPageId, int activePageId) { |
|
display::turnOn(); |
|
hmi::noteActivity(); |
|
|
|
#if OPTION_MOUSE |
|
mouse::onPageChanged(); |
|
#endif |
|
|
|
#if OPTION_KEYBOARD |
|
keyboard::onPageChanged(); |
|
#endif |
|
|
|
flow::onPageChanged(previousPageId, activePageId); |
|
} |
|
|
|
void AppContext::doShowPage(int pageId, Page *page, int previousPageId) { |
|
#if CONF_OPTION_FPGA |
|
pageId = PAGE_ID_WELCOME_800X480; |
|
page = nullptr; |
|
#endif |
|
|
|
page = page ? page : g_hooks.getPageFromId(pageId); |
|
|
|
m_pageNavigationStack[m_pageNavigationStackPointer].page = page; |
|
m_pageNavigationStack[m_pageNavigationStackPointer].pageId = pageId; |
|
m_pageNavigationStack[m_pageNavigationStackPointer].displayBufferIndex = -1; |
|
m_pageNavigationStack[m_pageNavigationStackPointer].timelinePosition = 0; |
|
|
|
if (page) { |
|
page->pageWillAppear(); |
|
} |
|
|
|
m_showPageTime = millis(); |
|
|
|
onPageChanged(previousPageId, pageId); |
|
|
|
refreshScreen(); |
|
} |
|
|
|
void AppContext::setPage(int pageId) { |
|
int previousPageId = getActivePageId(); |
|
|
|
// delete stack |
|
for (int i = 0; i <= m_pageNavigationStackPointer; ++i) { |
|
if (m_pageNavigationStack[i].page) { |
|
m_pageNavigationStack[i].page->pageFree(); |
|
} |
|
} |
|
m_pageNavigationStackPointer = 0; |
|
|
|
// |
|
doShowPage(pageId, nullptr, previousPageId); |
|
} |
|
|
|
void AppContext::replacePage(int pageId, Page *page) { |
|
int previousPageId = getActivePageId(); |
|
|
|
Page *activePage = getActivePage(); |
|
if (activePage) { |
|
activePage->pageFree(); |
|
} |
|
|
|
doShowPage(pageId, page, previousPageId); |
|
} |
|
|
|
void AppContext::pushPage(int pageId, Page *page) { |
|
if (pushPageInGuiThread(this, pageId, page)) { |
|
return; |
|
} |
|
|
|
int previousPageId = getActivePageId(); |
|
|
|
// advance stack pointer |
|
if (getActivePageId() != PAGE_ID_NONE && getActivePageId() != PAGE_ID_ASYNC_OPERATION_IN_PROGRESS && getActivePageId() != INTERNAL_PAGE_ID_TOAST_MESSAGE) { |
|
m_pageNavigationStackPointer++; |
|
assert (m_pageNavigationStackPointer < CONF_GUI_PAGE_NAVIGATION_STACK_SIZE); |
|
} |
|
|
|
doShowPage(pageId, page, previousPageId); |
|
} |
|
|
|
void AppContext::popPage() { |
|
if (m_pageNavigationStackPointer > 0) { |
|
int previousPageId = getActivePageId(); |
|
|
|
if (m_pageNavigationStack[m_pageNavigationStackPointer].page) { |
|
m_pageNavigationStack[m_pageNavigationStackPointer].page->pageFree(); |
|
m_pageNavigationStack[m_pageNavigationStackPointer].page = nullptr; |
|
} |
|
--m_pageNavigationStackPointer; |
|
|
|
doShowPage(m_pageNavigationStack[m_pageNavigationStackPointer].pageId, m_pageNavigationStack[m_pageNavigationStackPointer].page, previousPageId); |
|
} |
|
} |
|
|
|
void AppContext::removePageFromStack(int pageId) { |
|
for (int i = m_pageNavigationStackPointer; i > 0; i--) { |
|
if (m_pageNavigationStack[i].pageId == pageId) { |
|
if (i == m_pageNavigationStackPointer) { |
|
popPage(); |
|
} else { |
|
if (m_pageNavigationStack[i].page) { |
|
m_pageNavigationStack[i].page->pageFree(); |
|
} |
|
|
|
for (int j = i + 1; j <= m_pageNavigationStackPointer; j++) { |
|
memcpy(m_pageNavigationStack + j - 1, m_pageNavigationStack + j, sizeof(PageOnStack)); |
|
} |
|
|
|
m_pageNavigationStack[m_pageNavigationStackPointer].page = nullptr; |
|
|
|
--m_pageNavigationStackPointer; |
|
} |
|
refreshScreen(); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
Page *AppContext::getPage(int pageId) { |
|
for (int i = 0; i <= m_pageNavigationStackPointer; ++i) { |
|
if (m_pageNavigationStack[i].pageId == pageId) { |
|
return m_pageNavigationStack[i].page; |
|
} |
|
} |
|
return nullptr; |
|
} |
|
|
|
bool AppContext::isPageOnStack(int pageId) { |
|
for (int i = 0; i <= m_pageNavigationStackPointer; ++i) { |
|
if (m_pageNavigationStack[i].pageId == pageId) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
bool AppContext::isExternalPageOnStack() { |
|
for (int i = 0; i <= m_pageNavigationStackPointer; ++i) { |
|
if (m_pageNavigationStack[i].pageId < 0) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
void AppContext::removeExternalPagesFromTheStack() { |
|
for (int i = 0; i <= m_pageNavigationStackPointer; ++i) { |
|
if (m_pageNavigationStack[i].pageId < 0) { |
|
removePageFromStack(m_pageNavigationStack[i].pageId); |
|
i = 0; |
|
} |
|
} |
|
} |
|
|
|
void AppContext::showPage(int pageId) { |
|
if (showPageInGuiThread(this, pageId)) { |
|
return; |
|
} |
|
|
|
if (pageId != getActivePageId()) { |
|
setPage(pageId); |
|
} |
|
} |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
bool AppContext::testExecuteActionOnTouchDown(int action) { |
|
return false; |
|
} |
|
|
|
bool AppContext::isBlinking(const WidgetCursor &widgetCursor, int16_t id) { |
|
return false; |
|
} |
|
|
|
bool AppContext::canExecuteActionWhenTouchedOutsideOfActivePage(int pageId, int action) { |
|
return false; |
|
} |
|
|
|
void AppContext::onPageTouch(const WidgetCursor &foundWidget, Event &touchEvent) { |
|
int activePageId = getActivePageId(); |
|
#if OPTION_TOUCH_CALIBRATION |
|
if (activePageId == PAGE_ID_TOUCH_CALIBRATION) { |
|
onTouchCalibrationPageTouch(foundWidget, touchEvent); |
|
return; |
|
} |
|
#endif |
|
|
|
if (activePageId != PAGE_ID_NONE && !isPageInternal(activePageId)) { |
|
auto page = getPageAsset(activePageId); |
|
if ((page->flags & CLOSE_PAGE_IF_TOUCHED_OUTSIDE_FLAG) != 0) { |
|
int xPage; |
|
int yPage; |
|
int wPage; |
|
int hPage; |
|
getPageRect(activePageId, getActivePage(), xPage, yPage, wPage, hPage); |
|
|
|
if (!pointInsideRect(touchEvent.x, touchEvent.y, xPage, yPage, wPage, hPage)) { |
|
int activePageId = getActivePageId(); |
|
|
|
// clicked outside page, close page |
|
popPage(); |
|
|
|
auto widgetCursor = findWidget(touchEvent.x, touchEvent.y); |
|
|
|
if (widgetCursor.widget) { |
|
auto action = getWidgetAction(widgetCursor); |
|
if (action != ACTION_ID_NONE && canExecuteActionWhenTouchedOutsideOfActivePage(activePageId, action)) { |
|
processTouchEvent(touchEvent); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
|
|
void AppContext::updatePage(int i, WidgetCursor &widgetCursor) { |
|
if (g_findCallback == nullptr) { |
|
m_pageNavigationStack[i].displayBufferIndex = display::beginBufferRendering(); |
|
} |
|
|
|
m_updatePageIndex = i; |
|
|
|
int x; |
|
int y; |
|
int width; |
|
int height; |
|
bool withShadow; |
|
|
|
if (isPageInternal(m_pageNavigationStack[i].pageId)) { |
|
auto internalPage = ((InternalPage *)m_pageNavigationStack[i].page); |
|
|
|
x = internalPage->x; |
|
y = internalPage->y; |
|
width = internalPage->width; |
|
height = internalPage->height; |
|
withShadow = true; |
|
|
|
widgetCursor.w = width; |
|
widgetCursor.h = height; |
|
|
|
if (g_findCallback == nullptr) { |
|
internalPage->updateInternalPage(); |
|
} |
|
|
|
enumNoneWidget(); |
|
} else { |
|
auto page = getPageAsset(m_pageNavigationStack[i].pageId, widgetCursor); |
|
|
|
if (widgetCursor.flowState && widgetCursor.flowState->timelinePosition != m_pageNavigationStack[i].timelinePosition) { |
|
widgetCursor.hasPreviousState = false; |
|
m_pageNavigationStack[i].timelinePosition = widgetCursor.flowState->timelinePosition; |
|
} |
|
|
|
auto savedWidget = widgetCursor.widget; |
|
widgetCursor.widget = page; |
|
|
|
if ((page->flags & PAGE_SCALE_TO_FIT) && flow::g_debuggerMode == flow::DEBUGGER_MODE_RUN) { |
|
x = rect.x; |
|
y = rect.y; |
|
width = rect.w; |
|
height = rect.h; |
|
} else { |
|
x = widgetCursor.x + page->x; |
|
y = widgetCursor.y + page->y; |
|
width = page->width; |
|
height = page->height; |
|
} |
|
|
|
withShadow = page->x > 0; |
|
|
|
auto savedX = widgetCursor.x; |
|
auto savedY = widgetCursor.y; |
|
|
|
widgetCursor.x = x; |
|
widgetCursor.y = y; |
|
|
|
widgetCursor.w = width; |
|
widgetCursor.h = height; |
|
|
|
enumWidget(); |
|
|
|
widgetCursor.x = savedX; |
|
widgetCursor.y = savedY; |
|
|
|
widgetCursor.widget = savedWidget; |
|
} |
|
|
|
if (g_findCallback == nullptr) { |
|
pageRenderCustom(i, widgetCursor); |
|
display::endBufferRendering(m_pageNavigationStack[i].displayBufferIndex, x, y, width, height, withShadow, 255, 0, 0, withShadow && g_hooks.activePageHasBackdrop() ? &rect : nullptr); |
|
} |
|
|
|
m_updatePageIndex = -1; |
|
} |
|
|
|
void AppContext::pageRenderCustom(int i, WidgetCursor &widgetCursor) { |
|
} |
|
|
|
bool isRect1FullyCoveredByRect2(int xRect1, int yRect1, int wRect1, int hRect1, int xRect2, int yRect2, int wRect2, int hRect2) { |
|
return xRect2 <= xRect1 && yRect2 <= yRect1 && xRect2 + wRect2 >= xRect1 + wRect1 && yRect2 + hRect2 >= yRect1 + hRect1; |
|
} |
|
|
|
void AppContext::getPageRect(int pageId, const Page *page, int &x, int &y, int &w, int &h) { |
|
if (isPageInternal(pageId)) { |
|
x = rect.x + ((InternalPage *)page)->x; |
|
y = rect.y + ((InternalPage *)page)->y; |
|
w = ((InternalPage *)page)->width; |
|
h = ((InternalPage *)page)->height; |
|
} else { |
|
auto page = getPageAsset(pageId); |
|
if ((page->flags & PAGE_SCALE_TO_FIT) && flow::g_debuggerMode == flow::DEBUGGER_MODE_RUN) { |
|
x = rect.x; |
|
y = rect.y; |
|
w = rect.w; |
|
h = rect.h; |
|
} else { |
|
x = rect.x + page->x; |
|
y = rect.y + page->y; |
|
w = page->width; |
|
h = page->height; |
|
} |
|
} |
|
} |
|
|
|
bool AppContext::isPageFullyCovered(int pageNavigationStackIndex) { |
|
int xPage, yPage, wPage, hPage; |
|
getPageRect(m_pageNavigationStack[pageNavigationStackIndex].pageId, m_pageNavigationStack[pageNavigationStackIndex].page, xPage, yPage, wPage, hPage); |
|
|
|
for (int i = pageNavigationStackIndex + 1; i <= m_pageNavigationStackPointer; i++) { |
|
int xPageAbove, yPageAbove, wPageAbove, hPageAbove; |
|
getPageRect(m_pageNavigationStack[i].pageId, m_pageNavigationStack[i].page, xPageAbove, yPageAbove, wPageAbove, hPageAbove); |
|
|
|
if (isRect1FullyCoveredByRect2(xPage, yPage, wPage, hPage, xPageAbove, yPageAbove, wPageAbove, hPageAbove)) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
int AppContext::getLongTouchActionHook(const WidgetCursor &widgetCursor) { |
|
return ACTION_ID_NONE; |
|
} |
|
|
|
void AppContext::yesNoDialog(int yesNoPageId, const char *message, void (*yes_callback)(), void (*no_callback)(), void (*cancel_callback)()) { |
|
set(WidgetCursor(), DATA_ID_ALERT_MESSAGE, Value(message)); |
|
|
|
m_dialogYesCallback = yes_callback; |
|
m_dialogNoCallback = no_callback; |
|
m_dialogCancelCallback = cancel_callback; |
|
|
|
pushPage(yesNoPageId); |
|
} |
|
|
|
void AppContext::yesNoDialog(int yesNoPageId, Value value, void(*yes_callback)(), void(*no_callback)(), void(*cancel_callback)()) { |
|
set(WidgetCursor(), DATA_ID_ALERT_MESSAGE, value); |
|
|
|
m_dialogYesCallback = yes_callback; |
|
m_dialogNoCallback = no_callback; |
|
m_dialogCancelCallback = cancel_callback; |
|
|
|
pushPage(yesNoPageId); |
|
} |
|
|
|
void AppContext::infoMessage(const char *message) { |
|
pushToastMessage(ToastMessagePage::create(this, INFO_TOAST, message)); |
|
} |
|
|
|
void AppContext::infoMessage(Value value) { |
|
pushToastMessage(ToastMessagePage::create(this, INFO_TOAST, value)); |
|
} |
|
|
|
void AppContext::infoMessage(const char *message, void (*action)(), const char *actionLabel) { |
|
pushToastMessage(ToastMessagePage::create(this, INFO_TOAST, message, action, actionLabel)); |
|
} |
|
|
|
void AppContext::errorMessage(const char *message, bool autoDismiss) { |
|
AppContext::pushToastMessage(ToastMessagePage::create(this, ERROR_TOAST, message, autoDismiss)); |
|
sound::playBeep(); |
|
} |
|
|
|
void AppContext::errorMessage(Value value) { |
|
AppContext::pushToastMessage(ToastMessagePage::create(this, ERROR_TOAST, value)); |
|
sound::playBeep(); |
|
} |
|
|
|
void AppContext::errorMessageWithAction(Value value, void (*action)(int param), const char *actionLabel, int actionParam) { |
|
AppContext::pushToastMessage(ToastMessagePage::create(this, ERROR_TOAST, value, action, actionLabel, actionParam)); |
|
sound::playBeep(); |
|
} |
|
|
|
void AppContext::errorMessageWithAction(const char *message, void (*action)(), const char *actionLabel) { |
|
AppContext::pushToastMessage(ToastMessagePage::create(this, ERROR_TOAST, message, action, actionLabel)); |
|
sound::playBeep(); |
|
} |
|
|
|
void AppContext::pushToastMessage(ToastMessagePage *toastMessage) { |
|
pushPage(INTERNAL_PAGE_ID_TOAST_MESSAGE, toastMessage); |
|
} |
|
|
|
void AppContext::getBoundingRect(Rect &rectBounding) { |
|
if (m_pageNavigationStackPointer >= 0) { |
|
int x1 = rect.x + rect.w; |
|
int y1 = rect.y + rect.h; |
|
int x2 = rect.x; |
|
int y2 = rect.y; |
|
|
|
for (int i = 0; i <= m_pageNavigationStackPointer; ++i) { |
|
if (!isPageInternal(m_pageNavigationStack[i].pageId)) { |
|
int xPage, yPage, wPage, hPage; |
|
getPageRect(m_pageNavigationStack[i].pageId, m_pageNavigationStack[i].page, xPage, yPage, wPage, hPage); |
|
if (xPage < x1) x1 = xPage; |
|
if (xPage + wPage > x2) x2 = xPage + wPage; |
|
if (yPage < y1) y1 = yPage; |
|
if (yPage + hPage > y2) y2 = yPage + hPage; |
|
} |
|
} |
|
|
|
rectBounding.x = x1; |
|
rectBounding.y = y1; |
|
rectBounding.w = x2 - x1; |
|
rectBounding.h = y2 - y1; |
|
} else { |
|
rectBounding.x = rect.x; |
|
rectBounding.y = rect.y; |
|
rectBounding.w = rect.w; |
|
rectBounding.h = rect.h; |
|
} |
|
} |
|
|
|
AppContext *getRootAppContext() { |
|
#ifdef EEZ_PLATFORM_SIMULATOR |
|
return getAppContextFromId(APP_CONTEXT_ID_SIMULATOR_FRONT_PANEL); |
|
#else |
|
return getAppContextFromId(APP_CONTEXT_ID_DEVICE); |
|
#endif |
|
} |
|
|
|
} // namespace gui |
|
} // namespace eez
|
|
|