mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-20 02:11:28 +10:00
Merge branch 'develop' into t-watch-ultra
This commit is contained in:
@@ -243,7 +243,7 @@ bool EInkDisplay::connect()
|
||||
adafruitDisplay->setRotation(1);
|
||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||
}
|
||||
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||
{
|
||||
spi1 = &SPI1;
|
||||
spi1->begin();
|
||||
|
||||
@@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay
|
||||
SPIClass *hspi = NULL;
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||
SPIClass *spi1 = NULL;
|
||||
#endif
|
||||
|
||||
|
||||
+103
-20
@@ -100,7 +100,7 @@ namespace graphics
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
// if defined a pixel will blink to show redraws
|
||||
// #define SHOW_REDRAWS
|
||||
|
||||
#define ASCII_BELL '\x07'
|
||||
// A text message frame + debug frame + all the node infos
|
||||
FrameCallback *normalFrames;
|
||||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||||
@@ -221,6 +221,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
ui->update();
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||
|
||||
if (NotificationRenderer::virtualKeyboard) {
|
||||
delete NotificationRenderer::virtualKeyboard;
|
||||
NotificationRenderer::virtualKeyboard = nullptr;
|
||||
}
|
||||
|
||||
NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
|
||||
if (header) {
|
||||
NotificationRenderer::virtualKeyboard->setHeader(header);
|
||||
}
|
||||
if (initialText) {
|
||||
NotificationRenderer::virtualKeyboard->setInputText(initialText);
|
||||
}
|
||||
|
||||
// Set up callback with safer cleanup mechanism
|
||||
NotificationRenderer::textInputCallback = textCallback;
|
||||
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
|
||||
|
||||
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
||||
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
uint8_t module_frame;
|
||||
@@ -743,13 +781,19 @@ int32_t Screen::runOnce()
|
||||
handleSetOn(false);
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
handleOnPress();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleOnPress();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
handleShowPrevFrame();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowPrevFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
handleShowNextFrame();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowNextFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::START_ALERT_FRAME: {
|
||||
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
||||
@@ -771,7 +815,9 @@ int32_t Screen::runOnce()
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
setFrames();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames();
|
||||
}
|
||||
break;
|
||||
case Cmd::NOOP:
|
||||
break;
|
||||
@@ -807,6 +853,7 @@ int32_t Screen::runOnce()
|
||||
if (showingNormalScreen) {
|
||||
// standard screen loop handling here
|
||||
if (config.display.auto_screen_carousel_secs > 0 &&
|
||||
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
|
||||
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
||||
|
||||
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
||||
@@ -897,6 +944,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
// Called when a frame should be added / removed, or custom frames should be cleared
|
||||
void Screen::setFrames(FrameFocus focus)
|
||||
{
|
||||
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
||||
uint8_t previousFrameCount = framesetInfo.frameCount;
|
||||
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
||||
@@ -1003,7 +1055,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
if (!hiddenFrames.chirpy) {
|
||||
fsi.positions.chirpy = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
|
||||
indicatorIcons.push_back(small_chirpy);
|
||||
indicatorIcons.push_back(chirpy_small);
|
||||
}
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
@@ -1265,7 +1317,8 @@ void Screen::blink()
|
||||
delay(50);
|
||||
count = count - 1;
|
||||
}
|
||||
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in OLEDDisplay.
|
||||
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in
|
||||
// OLEDDisplay.
|
||||
dispdev->setBrightness(brightness);
|
||||
}
|
||||
|
||||
@@ -1405,28 +1458,36 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
}
|
||||
// === Prepare banner content ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const meshtastic_Channel channel =
|
||||
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
|
||||
char banner[256];
|
||||
|
||||
// Check for bell character in message to determine alert type
|
||||
bool isAlert = false;
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
if (msgRaw[i] == '\x07') {
|
||||
isAlert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_bell_buzzer)
|
||||
// Check for bell character to determine if this message is an alert
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
if (msgRaw[i] == ASCII_BELL) {
|
||||
isAlert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
|
||||
// 'mute' preferences set to any specific node or channel.
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "Alert Received");
|
||||
}
|
||||
} else {
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else if (!channel.settings.mute) {
|
||||
if (longName && longName[0]) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
strcpy(banner, "New Message");
|
||||
@@ -1437,14 +1498,21 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
} else {
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
playLongBeep();
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(p))) {
|
||||
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
|
||||
// - packet contains an alert and alert bell buzzer is enabled
|
||||
// - packet is a non-broadcast that is addressed to this node
|
||||
playLongBeep();
|
||||
}
|
||||
#else
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1454,6 +1522,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Triggered by MeshModules
|
||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
{
|
||||
// Block UI frame events when virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (showingNormalScreen) {
|
||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||
@@ -1476,6 +1549,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (!screenOn)
|
||||
return 0;
|
||||
|
||||
// Handle text input notifications specially - pass input to virtual keyboard
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
||||
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||
namespace graphics
|
||||
{
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
@@ -307,6 +307,8 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
|
||||
{
|
||||
|
||||
@@ -284,7 +284,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
|
||||
if (isInverted) {
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(BLACK);
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
#include "VirtualKeyboard.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
|
||||
{
|
||||
initializeKeyboard();
|
||||
// Set cursor to H(2, 5)
|
||||
cursorRow = 2;
|
||||
cursorCol = 5;
|
||||
}
|
||||
|
||||
VirtualKeyboard::~VirtualKeyboard() {}
|
||||
|
||||
void VirtualKeyboard::initializeKeyboard()
|
||||
{
|
||||
// New 4 row, 11 column keyboard layout:
|
||||
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
|
||||
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
|
||||
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
|
||||
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
|
||||
|
||||
// Derive layout dimensions and assert they match the configured keyboard grid
|
||||
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
|
||||
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
|
||||
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
|
||||
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
|
||||
|
||||
// Initialize all keys to empty first
|
||||
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
// Fill keyboard from the 2D layout
|
||||
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||
char ch = LAYOUT[row][col];
|
||||
// No empty slots in the simplified layout
|
||||
|
||||
VirtualKeyType type = VK_CHAR;
|
||||
if (ch == '\b') {
|
||||
type = VK_BACKSPACE;
|
||||
} else if (ch == '\n') {
|
||||
type = VK_ENTER;
|
||||
} else if (ch == '\x1b') { // ESC
|
||||
type = VK_ESC;
|
||||
} else if (ch == ' ') {
|
||||
type = VK_SPACE;
|
||||
}
|
||||
|
||||
// Make action keys wider to fit text while keeping the last column aligned
|
||||
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
|
||||
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
// Repeat ticking is driven by NotificationRenderer once per frame
|
||||
// Base styles
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Screen geometry
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
|
||||
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
|
||||
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
|
||||
const bool isWide = screenW >= 200;
|
||||
|
||||
// Determine last-column label max width
|
||||
display->setFont(FONT_SMALL);
|
||||
const int wENTER = display->getStringWidth("ENTER");
|
||||
int lastColLabelW = wENTER; // ENTER is usually the widest
|
||||
// Smaller padding on very small screens to avoid excessive whitespace
|
||||
const int lastColPad = (screenW <= 128 ? 2 : 6);
|
||||
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
|
||||
|
||||
// Always reserve width for the rightmost text column to avoid overlap on small screens
|
||||
int cellW = 0;
|
||||
int leftoverW = 0;
|
||||
{
|
||||
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
|
||||
int usableW = screenW - reservedLastColW;
|
||||
if (usableW < leftCols) {
|
||||
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
|
||||
usableW = leftCols;
|
||||
}
|
||||
cellW = usableW / leftCols;
|
||||
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
|
||||
}
|
||||
|
||||
// Dynamic key geometry
|
||||
int cellH = KEY_HEIGHT;
|
||||
int keyboardStartY = 0;
|
||||
if (screenH <= 64) {
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
|
||||
const int gapBelowHeader = 0;
|
||||
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
|
||||
const int gapAboveKeyboard = 0;
|
||||
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
if (keyboardStartY > screenH)
|
||||
keyboardStartY = screenH;
|
||||
int keyboardHeight = screenH - keyboardStartY;
|
||||
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
|
||||
} else if (isWide) {
|
||||
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
|
||||
cellH = std::max((int)KEY_HEIGHT, cellW);
|
||||
|
||||
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
|
||||
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
|
||||
display->setFont(FONT_SMALL);
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
|
||||
const int headerToBoxGap = 1;
|
||||
const int gapAboveKb = 1;
|
||||
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
|
||||
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
|
||||
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
|
||||
if (maxCellHAllowed < (int)KEY_HEIGHT)
|
||||
maxCellHAllowed = KEY_HEIGHT;
|
||||
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
|
||||
cellH = maxCellHAllowed;
|
||||
}
|
||||
// Keyboard placement from bottom for wide screens
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
} else {
|
||||
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
|
||||
cellH = KEY_HEIGHT;
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
}
|
||||
|
||||
// Draw input area above keyboard
|
||||
drawInputArea(display, offsetX, offsetY, keyboardStartY);
|
||||
|
||||
// Precompute per-column x and width with leftover distributed over left columns for even spacing
|
||||
int colX[KEYBOARD_COLS];
|
||||
int colW[KEYBOARD_COLS];
|
||||
int runningX = offsetX;
|
||||
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
|
||||
int wcol = cellW + (col < leftoverW ? 1 : 0);
|
||||
colX[col] = runningX;
|
||||
colW[col] = wcol;
|
||||
runningX += wcol;
|
||||
}
|
||||
// Last column
|
||||
colX[KEYBOARD_COLS - 1] = runningX;
|
||||
colW[KEYBOARD_COLS - 1] = reservedLastColW;
|
||||
|
||||
// Draw keyboard grid
|
||||
for (int row = 0; row < KEYBOARD_ROWS; row++) {
|
||||
for (int col = 0; col < KEYBOARD_COLS; col++) {
|
||||
const VirtualKey &k = keyboard[row][col];
|
||||
if (k.character != 0 || k.type != VK_CHAR) {
|
||||
const bool isLastCol = (col == KEYBOARD_COLS - 1);
|
||||
int x = colX[col];
|
||||
int w = colW[col];
|
||||
int y = offsetY + keyboardStartY + row * cellH;
|
||||
int h = cellH;
|
||||
bool selected = (row == cursorRow && col == cursorCol);
|
||||
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
|
||||
{
|
||||
display->setColor(WHITE);
|
||||
|
||||
const int screenWidth = display->getWidth();
|
||||
const int screenHeight = display->getHeight();
|
||||
// Use the standard small font metrics for input box sizing (restore original size)
|
||||
const int inputLineH = FONT_HEIGHT_SMALL;
|
||||
|
||||
// Header uses the standard small (which may be larger on big screens)
|
||||
display->setFont(FONT_SMALL);
|
||||
int headerHeight = 0;
|
||||
if (!headerText.empty()) {
|
||||
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
|
||||
display->drawString(offsetX + 2, offsetY, headerText.c_str());
|
||||
if (screenHeight <= 64) {
|
||||
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
|
||||
} else {
|
||||
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
|
||||
}
|
||||
}
|
||||
|
||||
const int boxX = offsetX;
|
||||
const int boxWidth = screenWidth;
|
||||
int boxY;
|
||||
int boxHeight;
|
||||
if (screenHeight <= 64) {
|
||||
const int gapBelowHeader = 0;
|
||||
const int fixedBoxHeight = inputLineH;
|
||||
const int gapAboveKeyboard = 0;
|
||||
boxY = offsetY + headerHeight + gapBelowHeader;
|
||||
boxHeight = fixedBoxHeight;
|
||||
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
|
||||
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
|
||||
boxHeight = std::max(1, fixedBoxHeight - over);
|
||||
}
|
||||
} else {
|
||||
const int gapBelowHeader = 1;
|
||||
int gapAboveKeyboard = 1;
|
||||
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
|
||||
const int minBoxHeight = inputLineH + 2;
|
||||
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
|
||||
if (availableH < minBoxHeight)
|
||||
availableH = minBoxHeight;
|
||||
boxY = tmpBoxY;
|
||||
boxHeight = availableH;
|
||||
}
|
||||
|
||||
// Draw box border
|
||||
display->drawRect(boxX, boxY, boxWidth, boxHeight);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
|
||||
const int textX = boxX + 2;
|
||||
const int maxTextWidth = boxWidth - 4;
|
||||
const int maxLines = (boxHeight - 2) / inputLineH;
|
||||
if (maxLines >= 2) {
|
||||
// Inner bounds for caret clamping
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Wrap text greedily into lines that fit maxTextWidth
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::string remaining = inputText;
|
||||
while (!remaining.empty()) {
|
||||
int bestLen = 0;
|
||||
for (int len = 1; len <= (int)remaining.size(); ++len) {
|
||||
int w = display->getStringWidth(remaining.substr(0, len).c_str());
|
||||
if (w <= maxTextWidth)
|
||||
bestLen = len;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (bestLen == 0) {
|
||||
// At least show one character to make progress
|
||||
bestLen = 1;
|
||||
}
|
||||
lines.emplace_back(remaining.substr(0, bestLen));
|
||||
remaining.erase(0, bestLen);
|
||||
}
|
||||
}
|
||||
|
||||
const bool scrolledUp = ((int)lines.size() > maxLines);
|
||||
int caretX = textX;
|
||||
int caretY = innerTop;
|
||||
|
||||
// Leave a small top gap to render '...' without replacing the first line
|
||||
const int topInset = 2;
|
||||
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
|
||||
int lineY = innerTop + topInset;
|
||||
|
||||
if (scrolledUp) {
|
||||
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
|
||||
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
|
||||
const int firstLineTop = lineY; // baseline top for the first visible line
|
||||
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
|
||||
const int centerX = boxX + boxWidth / 2;
|
||||
const int dotSpacing = 3; // px between dots
|
||||
const int dotSize = 1; // small square dot
|
||||
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
|
||||
}
|
||||
|
||||
// How many lines fit with our top inset and tighter step
|
||||
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
|
||||
const int linesToShow = std::min((int)lines.size(), linesCapacity);
|
||||
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
|
||||
|
||||
for (int i = 0; i < linesToShow; ++i) {
|
||||
const std::string &chunk = lines[startIndex + i];
|
||||
display->drawString(textX, lineY, chunk.c_str());
|
||||
caretX = textX + display->getStringWidth(chunk.c_str());
|
||||
caretY = lineY;
|
||||
lineY += lineStep;
|
||||
}
|
||||
|
||||
// Draw caret at end of the last visible line
|
||||
int caretPadY = 2;
|
||||
if (boxHeight >= inputLineH + 4)
|
||||
caretPadY = 3;
|
||||
int cursorTop = caretY + caretPadY;
|
||||
// Use lineStep so caret height matches the row spacing
|
||||
int cursorH = lineStep - caretPadY * 2;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Clamp vertical bounds to stay inside the inner rect
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Only draw if cursor is inside inner bounds
|
||||
if (caretX >= innerLeft && caretX <= innerRight) {
|
||||
display->drawVerticalLine(caretX, cursorTop, cursorH);
|
||||
}
|
||||
} else {
|
||||
std::string displayText = inputText;
|
||||
int textW = display->getStringWidth(displayText.c_str());
|
||||
std::string scrolled = displayText;
|
||||
if (textW > maxTextWidth) {
|
||||
// Trim from the left until it fits
|
||||
while (textW > maxTextWidth && !scrolled.empty()) {
|
||||
scrolled.erase(0, 1);
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
// Add leading ellipsis and ensure it still fits
|
||||
if (scrolled != displayText) {
|
||||
scrolled = "..." + scrolled;
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
// If adding ellipsis causes overflow, trim more after the ellipsis
|
||||
while (textW > maxTextWidth && scrolled.size() > 3) {
|
||||
scrolled.erase(3, 1); // remove chars after the ellipsis
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Keep textW in sync with what we draw
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
|
||||
int textY;
|
||||
if (screenHeight <= 64) {
|
||||
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
|
||||
int innerH = innerBottom - innerTop + 1;
|
||||
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
|
||||
// Clamp fully inside the inner rect
|
||||
if (textY < innerTop)
|
||||
textY = innerTop;
|
||||
int maxTop = innerBottom - inputLineH + 1;
|
||||
if (textY > maxTop)
|
||||
textY = maxTop;
|
||||
}
|
||||
|
||||
if (!scrolled.empty()) {
|
||||
display->drawString(textX, textY, scrolled.c_str());
|
||||
}
|
||||
|
||||
int cursorX = textX + textW;
|
||||
if (screenHeight > 64) {
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
if (cursorX > innerRight)
|
||||
cursorX = innerRight;
|
||||
}
|
||||
|
||||
int cursorTop, cursorH;
|
||||
if (screenHeight <= 64) {
|
||||
cursorH = 10;
|
||||
cursorTop = boxY + (boxHeight - cursorH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
cursorTop = boxY + 2;
|
||||
cursorH = boxHeight - 4;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
|
||||
if (cursorX < innerLeft || cursorX > innerRight)
|
||||
return;
|
||||
}
|
||||
|
||||
display->drawVerticalLine(cursorX, cursorTop, cursorH);
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
|
||||
uint8_t height, bool isLastCol)
|
||||
{
|
||||
// Draw key content
|
||||
display->setFont(FONT_SMALL);
|
||||
const int fontH = FONT_HEIGHT_SMALL;
|
||||
// Build label and metrics first
|
||||
std::string keyText;
|
||||
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
|
||||
// Keep literal text labels for the action keys on the rightmost column
|
||||
keyText = (key.type == VK_BACKSPACE) ? "BACK"
|
||||
: (key.type == VK_ENTER) ? "ENTER"
|
||||
: (key.type == VK_SPACE) ? "SPACE"
|
||||
: (key.type == VK_ESC) ? "ESC"
|
||||
: "";
|
||||
} else {
|
||||
char c = getCharForKey(key, false);
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||
}
|
||||
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
// Label alignment
|
||||
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
|
||||
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
|
||||
int textX;
|
||||
if (isLastCol) {
|
||||
const int rightPad = 1;
|
||||
textX = x + width - textWidth - rightPad;
|
||||
if (textX < x)
|
||||
textX = x; // guard
|
||||
} else {
|
||||
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
|
||||
textX = x + (width - textWidth + 1) / 2;
|
||||
} else {
|
||||
textX = x + (width - textWidth) / 2;
|
||||
}
|
||||
}
|
||||
int contentTop = y;
|
||||
int contentH = height;
|
||||
if (selected) {
|
||||
display->setColor(WHITE);
|
||||
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
|
||||
|
||||
if (display->getHeight() <= 64 && !isAction) {
|
||||
display->fillRect(x, y, width, height);
|
||||
} else if (isAction) {
|
||||
const int padX = 1;
|
||||
const int padY = 2;
|
||||
int hlW = textWidth + padX * 2;
|
||||
int hlX = textX - padX;
|
||||
|
||||
if (hlX < x) {
|
||||
hlW -= (x - hlX);
|
||||
hlX = x;
|
||||
}
|
||||
int maxW = (x + width) - hlX;
|
||||
if (hlW > maxW)
|
||||
hlW = maxW;
|
||||
if (hlW < 1)
|
||||
hlW = 1;
|
||||
|
||||
int hlH = std::min(fontH + padY * 2, (int)height);
|
||||
int hlY = y + (height - hlH) / 2;
|
||||
display->fillRect(hlX, hlY, hlW, hlH);
|
||||
contentTop = hlY;
|
||||
contentH = hlH;
|
||||
} else {
|
||||
display->fillRect(x, y, width, height);
|
||||
}
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
int centeredTextY;
|
||||
if (display->getHeight() <= 64) {
|
||||
centeredTextY = y + (height - fontH) / 2;
|
||||
} else {
|
||||
centeredTextY = contentTop + (contentH - fontH) / 2;
|
||||
}
|
||||
if (display->getHeight() > 64) {
|
||||
if (centeredTextY < contentTop)
|
||||
centeredTextY = contentTop;
|
||||
if (centeredTextY + fontH > contentTop + contentH)
|
||||
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
|
||||
}
|
||||
|
||||
if (display->getHeight() <= 64 && keyText.size() == 1) {
|
||||
char ch = keyText[0];
|
||||
if (ch == '.' || ch == ',' || ch == ';') {
|
||||
centeredTextY -= 1;
|
||||
}
|
||||
}
|
||||
display->drawString(textX, centeredTextY, keyText.c_str());
|
||||
}
|
||||
|
||||
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
{
|
||||
if (key.type != VK_CHAR) {
|
||||
return key.character;
|
||||
}
|
||||
|
||||
char c = key.character;
|
||||
|
||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
|
||||
{
|
||||
resetTimeout();
|
||||
// wrap around rows and cols in the 4x11 grid
|
||||
int r = (int)cursorRow + dRow;
|
||||
int c = (int)cursorCol + dCol;
|
||||
if (r < 0)
|
||||
r = KEYBOARD_ROWS - 1;
|
||||
else if (r >= KEYBOARD_ROWS)
|
||||
r = 0;
|
||||
if (c < 0)
|
||||
c = KEYBOARD_COLS - 1;
|
||||
else if (c >= KEYBOARD_COLS)
|
||||
c = 0;
|
||||
cursorRow = (uint8_t)r;
|
||||
cursorCol = (uint8_t)c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorUp()
|
||||
{
|
||||
moveCursorDelta(-1, 0);
|
||||
}
|
||||
void VirtualKeyboard::moveCursorDown()
|
||||
{
|
||||
moveCursorDelta(1, 0);
|
||||
}
|
||||
void VirtualKeyboard::moveCursorLeft()
|
||||
{
|
||||
resetTimeout();
|
||||
|
||||
if (cursorCol > 0) {
|
||||
cursorCol--;
|
||||
} else {
|
||||
if (cursorRow > 0) {
|
||||
cursorRow--;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
} else {
|
||||
cursorRow = KEYBOARD_ROWS - 1;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
void VirtualKeyboard::moveCursorRight()
|
||||
{
|
||||
resetTimeout();
|
||||
|
||||
if (cursorCol < KEYBOARD_COLS - 1) {
|
||||
cursorCol++;
|
||||
} else {
|
||||
if (cursorRow < KEYBOARD_ROWS - 1) {
|
||||
cursorRow++;
|
||||
cursorCol = 0;
|
||||
} else {
|
||||
cursorRow = 0;
|
||||
cursorCol = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handlePress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert lowercase character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle non-character keys immediately
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
deleteCharacter();
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
case VK_SPACE:
|
||||
insertCharacter(' ');
|
||||
break;
|
||||
case VK_ESC:
|
||||
if (onTextEntered) {
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
onTextEntered = nullptr;
|
||||
inputText = "";
|
||||
callback("");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handleLongPress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert uppercase/alternate character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
// One-shot: delete up to 5 characters on long press
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (inputText.empty())
|
||||
break;
|
||||
deleteCharacter();
|
||||
}
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
case VK_SPACE:
|
||||
insertCharacter(' ');
|
||||
break;
|
||||
case VK_ESC:
|
||||
if (onTextEntered) {
|
||||
onTextEntered("");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::insertCharacter(char c)
|
||||
{
|
||||
if (inputText.length() < 160) { // Reasonable text length limit
|
||||
inputText += c;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::deleteCharacter()
|
||||
{
|
||||
if (!inputText.empty()) {
|
||||
inputText.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::submitText()
|
||||
{
|
||||
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
|
||||
|
||||
// Only submit if text is not empty
|
||||
if (!inputText.empty() && onTextEntered) {
|
||||
// Store callback and text to submit before clearing callback
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
std::string textToSubmit = inputText;
|
||||
onTextEntered = nullptr;
|
||||
// Don't clear inputText here - let the calling module handle cleanup
|
||||
// inputText = ""; // Removed: keep text visible until module cleans up
|
||||
callback(textToSubmit);
|
||||
} else if (inputText.empty()) {
|
||||
// For empty text, just ignore the submission - don't clear callback
|
||||
// This keeps the virtual keyboard responsive for further input
|
||||
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
|
||||
} else {
|
||||
// No callback available
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setInputText(const std::string &text)
|
||||
{
|
||||
inputText = text;
|
||||
}
|
||||
|
||||
std::string VirtualKeyboard::getInputText() const
|
||||
{
|
||||
return inputText;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setHeader(const std::string &header)
|
||||
{
|
||||
headerText = header;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
|
||||
{
|
||||
onTextEntered = callback;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::resetTimeout()
|
||||
{
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
|
||||
bool VirtualKeyboard::isTimedOut() const
|
||||
{
|
||||
return (millis() - lastActivityTime) > TIMEOUT_MS;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
|
||||
|
||||
struct VirtualKey {
|
||||
char character;
|
||||
VirtualKeyType type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
};
|
||||
|
||||
class VirtualKeyboard
|
||||
{
|
||||
public:
|
||||
VirtualKeyboard();
|
||||
~VirtualKeyboard();
|
||||
|
||||
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void setInputText(const std::string &text);
|
||||
std::string getInputText() const;
|
||||
void setHeader(const std::string &header);
|
||||
void setCallback(std::function<void(const std::string &)> callback);
|
||||
|
||||
// Navigation methods for encoder input
|
||||
void moveCursorUp();
|
||||
void moveCursorDown();
|
||||
void moveCursorLeft();
|
||||
void moveCursorRight();
|
||||
void handlePress();
|
||||
void handleLongPress();
|
||||
|
||||
// Timeout management
|
||||
void resetTimeout();
|
||||
bool isTimedOut() const;
|
||||
|
||||
private:
|
||||
static const uint8_t KEYBOARD_ROWS = 4;
|
||||
static const uint8_t KEYBOARD_COLS = 11;
|
||||
static const uint8_t KEY_WIDTH = 9;
|
||||
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
|
||||
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
|
||||
|
||||
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||
|
||||
std::string inputText;
|
||||
std::string headerText;
|
||||
std::function<void(const std::string &)> onTextEntered;
|
||||
|
||||
uint8_t cursorRow;
|
||||
uint8_t cursorCol;
|
||||
|
||||
// Timeout management for auto-exit
|
||||
uint32_t lastActivityTime;
|
||||
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||
|
||||
void initializeKeyboard();
|
||||
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
|
||||
bool isLastCol);
|
||||
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
|
||||
|
||||
// Unified cursor movement helper
|
||||
void moveCursorDelta(int dRow, int dCol);
|
||||
|
||||
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||
void insertCharacter(char c);
|
||||
void deleteCharacter();
|
||||
void submitText();
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
|
||||
@@ -692,6 +692,7 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "World!");
|
||||
}
|
||||
|
||||
} // namespace DebugRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -33,7 +33,6 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
|
||||
// System screen display
|
||||
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Chirpy screen display
|
||||
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
} // namespace DebugRenderer
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/KeyVerificationModule.h"
|
||||
@@ -28,19 +31,21 @@ uint8_t test_count = 0;
|
||||
|
||||
void menuHandler::loraMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Region Picker", "Device Role"};
|
||||
enum optionsNumbers { Back = 0, lora_picker = 1, device_role_picker = 2 };
|
||||
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
|
||||
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
// No action
|
||||
} else if (selected == lora_picker) {
|
||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||
} else if (selected == device_role_picker) {
|
||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||
} else if (selected == radio_preset_picker) {
|
||||
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
||||
} else if (selected == lora_picker) {
|
||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -177,6 +182,53 @@ void menuHandler::DeviceRolePicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::RadioPresetPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
|
||||
"MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
|
||||
enum optionsNumbers {
|
||||
Back = 0,
|
||||
radiopreset_LongSlow = 1,
|
||||
radiopreset_LongModerate = 2,
|
||||
radiopreset_LongFast = 3,
|
||||
radiopreset_MediumSlow = 4,
|
||||
radiopreset_MediumFast = 5,
|
||||
radiopreset_ShortSlow = 6,
|
||||
radiopreset_ShortFast = 7,
|
||||
radiopreset_ShortTurbo = 8
|
||||
};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Radio Preset";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 9;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
screen->runNow();
|
||||
return;
|
||||
} else if (selected == radiopreset_LongSlow) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
|
||||
} else if (selected == radiopreset_LongModerate) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
|
||||
} else if (selected == radiopreset_LongFast) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
} else if (selected == radiopreset_MediumSlow) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
|
||||
} else if (selected == radiopreset_MediumFast) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||
} else if (selected == radiopreset_ShortSlow) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
|
||||
} else if (selected == radiopreset_ShortFast) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
|
||||
} else if (selected == radiopreset_ShortTurbo) {
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||
}
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::TwelveHourPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||
@@ -800,24 +852,31 @@ void menuHandler::GPSFormatMenu()
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 1) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else if (selected == 2) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else if (selected == 3) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else if (selected == 4) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else if (selected == 5) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else if (selected == 6) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else if (selected == 7) {
|
||||
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else {
|
||||
menuQueue = position_base_menu;
|
||||
@@ -852,11 +911,11 @@ void menuHandler::BluetoothToggleMenu()
|
||||
|
||||
void menuHandler::BuzzerModeMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
|
||||
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Buzzer Mode";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
@@ -1475,6 +1534,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case device_role_picker:
|
||||
DeviceRolePicker();
|
||||
break;
|
||||
case radio_preset_picker:
|
||||
RadioPresetPicker();
|
||||
break;
|
||||
case no_timeout_lora_picker:
|
||||
LoraRegionPicker(0);
|
||||
break;
|
||||
|
||||
@@ -12,6 +12,7 @@ class menuHandler
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
radio_preset_picker,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
@@ -50,6 +51,7 @@ class menuHandler
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void DeviceRolePicker();
|
||||
static void RadioPresetPicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
@@ -83,8 +85,8 @@ class menuHandler
|
||||
static void notificationsMenu();
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void textMessageMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#if HAS_BUTTON
|
||||
#include "input/ButtonThread.h"
|
||||
#endif
|
||||
#include "main.h"
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "esp_task_wdt.h"
|
||||
@@ -18,6 +26,11 @@
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
#if HAS_BUTTON
|
||||
// Global button thread pointer defined in main.cpp
|
||||
extern ::ButtonThread *UserButtonThread;
|
||||
#endif
|
||||
|
||||
// External references to global variables from Screen.cpp
|
||||
extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
@@ -38,6 +51,8 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
uint32_t pow_of_10(uint32_t n)
|
||||
{
|
||||
@@ -89,14 +104,33 @@ void NotificationRenderer::resetBanner()
|
||||
|
||||
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
|
||||
resetBanner();
|
||||
if (!isOverlayBannerShowing() || pauseBanner)
|
||||
// Handle text_input notifications first - they have their own timeout/banner logic
|
||||
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Check for timeout and reset if needed for text input
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
drawTextInput(display, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
}
|
||||
|
||||
// Exit if no banner is showing or banner is paused
|
||||
if (!isOverlayBannerShowing() || pauseBanner) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (current_notification_type) {
|
||||
case notificationTypeEnum::none:
|
||||
// Do nothing - no notification to display
|
||||
break;
|
||||
case notificationTypeEnum::text_input:
|
||||
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
|
||||
break;
|
||||
case notificationTypeEnum::text_banner:
|
||||
case notificationTypeEnum::selection_picker:
|
||||
drawAlertBannerOverlay(display, state);
|
||||
@@ -269,12 +303,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
|
||||
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
|
||||
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
|
||||
|
||||
} else {
|
||||
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
|
||||
}
|
||||
// make temp buffer for name
|
||||
// fi
|
||||
if (i == curSelected) {
|
||||
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
|
||||
if (isHighResolution) {
|
||||
@@ -288,7 +319,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
}
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
} else {
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
}
|
||||
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
|
||||
}
|
||||
@@ -709,6 +741,99 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
||||
"Please be patient and do not power off.");
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (virtualKeyboard) {
|
||||
// Check for timeout and auto-exit if needed
|
||||
if (virtualKeyboard->isTimedOut()) {
|
||||
LOG_INFO("Virtual keyboard timeout - auto-exiting");
|
||||
// Cancel virtual keyboard - call callback with empty string to indicate timeout
|
||||
auto callback = textInputCallback; // Store callback before clearing
|
||||
|
||||
// Clean up first to prevent re-entry
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
|
||||
// Call callback after cleanup
|
||||
if (callback) {
|
||||
callback("");
|
||||
}
|
||||
|
||||
// Restore normal overlays
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP) {
|
||||
// high frequency for move cursor left/right than up/down with encoders
|
||||
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else {
|
||||
virtualKeyboard->moveCursorUp();
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
|
||||
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else {
|
||||
virtualKeyboard->moveCursorDown();
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
virtualKeyboard->moveCursorUp();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
virtualKeyboard->moveCursorDown();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||
virtualKeyboard->handlePress();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
|
||||
virtualKeyboard->handleLongPress();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
|
||||
auto callback = textInputCallback;
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
if (callback) {
|
||||
callback("");
|
||||
}
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume the event after processing for virtual keyboard
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
}
|
||||
|
||||
// Clear the screen to avoid overlapping with underlying frames or overlays
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||
display->setColor(WHITE);
|
||||
// Draw the virtual keyboard
|
||||
virtualKeyboard->draw(display, 0, 0);
|
||||
} else {
|
||||
// If virtualKeyboard is null, reset the banner to avoid getting stuck
|
||||
LOG_INFO("Virtual keyboard is null - resetting banner");
|
||||
resetBanner();
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
|
||||
namespace graphics
|
||||
@@ -22,6 +25,8 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
@@ -30,6 +35,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
|
||||
// External variables
|
||||
extern graphics::Screen *screen;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static uint32_t lastSwitchTime = 0;
|
||||
#endif
|
||||
namespace graphics
|
||||
{
|
||||
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
||||
@@ -123,11 +125,15 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
|
||||
char displayLine[32];
|
||||
|
||||
if (!gps->getIsConnected() && !config.position.fixed_position) {
|
||||
strcpy(displayLine, "No GPS present");
|
||||
display->drawString(x, y, displayLine);
|
||||
if (strcmp(mode, "line1") == 0) {
|
||||
strcpy(displayLine, "No GPS present");
|
||||
display->drawString(x, y, displayLine);
|
||||
}
|
||||
} else if (!gps->getHasLock() && !config.position.fixed_position) {
|
||||
strcpy(displayLine, "No GPS Lock");
|
||||
display->drawString(x, y, displayLine);
|
||||
if (strcmp(mode, "line1") == 0) {
|
||||
strcpy(displayLine, "No GPS Lock");
|
||||
display->drawString(x, y, displayLine);
|
||||
}
|
||||
} else {
|
||||
|
||||
geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude()));
|
||||
@@ -286,9 +292,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
||||
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
|
||||
return;
|
||||
uint32_t now = millis();
|
||||
display->clear();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
uint32_t now = millis();
|
||||
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
|
||||
{
|
||||
display->display();
|
||||
@@ -733,7 +739,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
int textWidth = 0;
|
||||
int nameX = 0;
|
||||
int yOffset = (isHighResolution) ? 0 : 5;
|
||||
const char *longName = nullptr;
|
||||
std::string longNameStr;
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
@@ -1101,6 +1106,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
// === Fourth Row: Line 2 GPS Info ===
|
||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
|
||||
}
|
||||
|
||||
// === Final Row: Altitude ===
|
||||
char altitudeLine[32] = {0};
|
||||
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
? ourNode->position.altitude
|
||||
: geoCoord.getAltitude();
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
|
||||
} else {
|
||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
|
||||
}
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === Draw Compass if heading is valid ===
|
||||
@@ -1185,7 +1202,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif // HAS_GPS
|
||||
}
|
||||
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
@@ -1278,14 +1295,13 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
|
||||
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
|
||||
|
||||
// Only show bar briefly after switching frames
|
||||
static uint32_t navBarLastShown = 0;
|
||||
static bool cosmeticRefreshDone = false;
|
||||
|
||||
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
|
||||
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
|
||||
|
||||
#if defined(USE_EINK)
|
||||
// Only show bar briefly after switching frames
|
||||
static uint32_t navBarLastShown = 0;
|
||||
static bool cosmeticRefreshDone = false;
|
||||
static bool navBarPrevVisible = false;
|
||||
|
||||
if (navBarVisible && !navBarPrevVisible) {
|
||||
|
||||
@@ -287,12 +287,10 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101
|
||||
#define analog_icon_clock_height 8
|
||||
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
|
||||
0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
||||
#ifdef M5STACK_UNITC6L
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
|
||||
#define chirpy_width 38
|
||||
#define chirpy_height 50
|
||||
static unsigned char chirpy[] = {
|
||||
const uint8_t chirpy[] = {
|
||||
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01,
|
||||
0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00,
|
||||
0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f,
|
||||
@@ -308,7 +306,7 @@ static unsigned char chirpy[] = {
|
||||
|
||||
#define chirpy_width_hirez 76
|
||||
#define chirpy_height_hirez 100
|
||||
static unsigned char chirpy_hirez[] = {
|
||||
const uint8_t chirpy_hirez[] = {
|
||||
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
@@ -360,8 +358,11 @@ static unsigned char chirpy_hirez[] = {
|
||||
|
||||
#define chirpy_small_image_width 8
|
||||
#define chirpy_small_image_height 8
|
||||
static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
|
||||
#ifdef M5STACK_UNITC6L
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
#include "img/icon.xbm"
|
||||
#endif
|
||||
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
|
||||
@@ -27,90 +27,4 @@ static uint8_t icon_bits[] = {
|
||||
0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00 };
|
||||
#endif
|
||||
|
||||
// Chirpy image definitions for M5STACK_UNITC6L compatibility
|
||||
#define chirpy_width 38
|
||||
#define chirpy_height 50
|
||||
static unsigned char chirpy[] = {
|
||||
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01,
|
||||
0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00,
|
||||
0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f,
|
||||
0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc,
|
||||
0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0,
|
||||
0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1,
|
||||
0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff,
|
||||
0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3,
|
||||
0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03,
|
||||
0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0,
|
||||
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
|
||||
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
|
||||
|
||||
#define chirpy_width_hirez 76
|
||||
#define chirpy_height_hirez 100
|
||||
static unsigned char chirpy_hirez[] = {
|
||||
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
|
||||
0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc,
|
||||
0x03, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03,
|
||||
0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0,
|
||||
0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f,
|
||||
0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe,
|
||||
0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff,
|
||||
0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f,
|
||||
0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0,
|
||||
0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f,
|
||||
0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00,
|
||||
0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc,
|
||||
0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03,
|
||||
0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0,
|
||||
0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f,
|
||||
0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe,
|
||||
0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff,
|
||||
0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f,
|
||||
0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0,
|
||||
0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f,
|
||||
0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00,
|
||||
0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc,
|
||||
0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03,
|
||||
0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0,
|
||||
0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x1f,
|
||||
0xff, 0xff, 0xff, 0xf8, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00,
|
||||
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x83, 0xc1, 0x03, 0x00, 0x00,
|
||||
0xfc, 0x03, 0x00, 0x00, 0xc0, 0x83, 0xc1, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0xfc,
|
||||
0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xfc, 0x03,
|
||||
0x00, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x78, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
0x3c, 0x00, 0x00, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x78, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x1e,
|
||||
0x00, 0x00, 0x78, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0x00,
|
||||
0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xfc, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00,
|
||||
0xe0, 0x01, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0,
|
||||
0x03, 0x00, 0xfc, 0x03, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0xfc, 0x03, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0x07,
|
||||
0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00,
|
||||
0xfc, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0xfc, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0xfc,
|
||||
0x03, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0xfc, 0x03, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0xfc, 0x03,
|
||||
0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xfc, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xfc, 0x03, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xfc, 0x03, 0x80, 0x07,
|
||||
0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0xfc, 0x03, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0xfc, 0x03, 0xc0, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0xe0, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x80, 0x07, 0xfc, 0x03, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0xfc, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x0f, 0xfc, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3
|
||||
};
|
||||
|
||||
#define chirpy_small_image_width 8
|
||||
#define chirpy_small_image_height 8
|
||||
static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
#endif
|
||||
Reference in New Issue
Block a user