mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-16 00:21:35 +10:00
Text Prediction
This commit is contained in:
@@ -866,6 +866,17 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
|
||||
// Confirm select (Enter)
|
||||
bool isSelect = isSelectEvent(event);
|
||||
if (isSelect) {
|
||||
// When multiple predictions exist, SELECT accepts the currently highlighted
|
||||
// suggestion first. Press SELECT again to send.
|
||||
if (this->cursor == this->freetext.length() && this->freeTextPredictionCount > 0 &&
|
||||
this->acceptFreeTextPrediction(true)) {
|
||||
this->payload = 0;
|
||||
this->lastTouchMillis = millis();
|
||||
requestFocus();
|
||||
runOnce();
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel,
|
||||
freetext.c_str());
|
||||
if (dest == 0)
|
||||
@@ -1291,13 +1302,25 @@ int32_t CannedMessageModule::runOnce()
|
||||
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
|
||||
switch (this->payload) {
|
||||
case INPUT_BROKER_LEFT:
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) {
|
||||
this->cursor--;
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->cursor == this->freetext.length() && this->freeTextPredictionCount > 1) {
|
||||
this->cycleFreeTextPrediction(-1);
|
||||
} else if (this->cursor > 0) {
|
||||
this->cursor--;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case INPUT_BROKER_RIGHT:
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) {
|
||||
this->cursor++;
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->cursor < this->freetext.length()) {
|
||||
this->cursor++;
|
||||
} else if (this->freeTextPredictionCount > 1) {
|
||||
this->cycleFreeTextPrediction(1);
|
||||
} else if (this->freeTextPredictionCount > 0) {
|
||||
this->acceptFreeTextPrediction(true);
|
||||
} else {
|
||||
// Keep right-at-end as navigation only.
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -1307,6 +1330,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
switch (this->payload) {
|
||||
case 0x08: // backspace
|
||||
this->freeTextPredictionSuppressed = false;
|
||||
if (this->freetext.length() > 0) {
|
||||
if (this->cursor > 0) {
|
||||
if (this->cursor == this->freetext.length()) {
|
||||
@@ -1328,14 +1352,16 @@ int32_t CannedMessageModule::runOnce()
|
||||
default:
|
||||
// Only insert ASCII printable characters (32–126)
|
||||
if (this->payload >= 32 && this->payload <= 126) {
|
||||
this->freeTextPredictionSuppressed = false;
|
||||
requestFocus();
|
||||
if (this->cursor == this->freetext.length()) {
|
||||
this->freetext += (char)this->payload;
|
||||
this->freetext += static_cast<char>(this->payload);
|
||||
this->cursor++;
|
||||
} else {
|
||||
this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload +
|
||||
this->freetext = this->freetext.substring(0, this->cursor) + static_cast<char>(this->payload) +
|
||||
this->freetext.substring(this->cursor);
|
||||
this->cursor++;
|
||||
}
|
||||
this->cursor++;
|
||||
const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0);
|
||||
if (this->freetext.length() > maxChars) {
|
||||
this->cursor = maxChars;
|
||||
@@ -1344,6 +1370,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->updateFreeTextPrediction();
|
||||
}
|
||||
this->lastTouchMillis = millis();
|
||||
this->notifyObservers(&e);
|
||||
@@ -1993,7 +2020,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
// Draw Free Text input with multi-emote support and proper line wrapping
|
||||
display->setColor(WHITE);
|
||||
{
|
||||
int inputY = 0 + y + FONT_HEIGHT_SMALL;
|
||||
const int inputTopOffset = -3;
|
||||
int inputY = y + FONT_HEIGHT_SMALL + inputTopOffset;
|
||||
String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor);
|
||||
|
||||
// Tokenize input into (isEmote, token) pairs
|
||||
@@ -2064,21 +2092,69 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
if (!currentLine.empty())
|
||||
lines.push_back(currentLine);
|
||||
|
||||
// Draw lines with emotes
|
||||
int rowHeight = FONT_HEIGHT_SMALL;
|
||||
int yLine = inputY;
|
||||
for (const auto &line : lines) {
|
||||
const int rowHeight = std::max(8, FONT_HEIGHT_SMALL - 3);
|
||||
const int viewportTop = inputY;
|
||||
const int viewportBottom = y + display->getHeight();
|
||||
const int viewportHeight = std::max(1, viewportBottom - viewportTop);
|
||||
const int viewportRows = std::max(1, viewportHeight / rowHeight);
|
||||
|
||||
int cursorRow = static_cast<int>(lines.size()) - 1;
|
||||
if (cursorRow < 0) {
|
||||
cursorRow = 0;
|
||||
}
|
||||
if (this->cursor < this->freetext.length()) {
|
||||
for (int lineIdx = 0; lineIdx < static_cast<int>(lines.size()); ++lineIdx) {
|
||||
bool hasCursorMarker = false;
|
||||
for (const auto &token : lines[lineIdx]) {
|
||||
if (!token.first && token.second.indexOf('_') >= 0) {
|
||||
hasCursorMarker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasCursorMarker) {
|
||||
cursorRow = lineIdx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String predictionPrefix = this->getFreeTextPrefix();
|
||||
const bool showPredictionRow =
|
||||
(this->cursor == this->freetext.length() && this->freeTextPredictionCount > 1 && predictionPrefix.length() >= 2);
|
||||
const int totalRows = static_cast<int>(lines.size()) + (showPredictionRow ? 1 : 0);
|
||||
int scrollRows = std::max(0, totalRows - viewportRows);
|
||||
int targetRow = cursorRow + (showPredictionRow ? 1 : 0);
|
||||
if (targetRow < scrollRows) {
|
||||
scrollRows = targetRow;
|
||||
}
|
||||
if (targetRow >= (scrollRows + viewportRows)) {
|
||||
scrollRows = targetRow - viewportRows + 1;
|
||||
}
|
||||
if (scrollRows < 0) {
|
||||
scrollRows = 0;
|
||||
}
|
||||
|
||||
// Draw wrapped text rows with vertical scrolling.
|
||||
for (int rowIdx = 0; rowIdx < static_cast<int>(lines.size()); ++rowIdx) {
|
||||
int yLine = viewportTop + ((rowIdx - scrollRows) * rowHeight);
|
||||
if (yLine < viewportTop || (yLine + rowHeight) > (viewportBottom + rowHeight)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int nextX = x;
|
||||
for (const auto &token : line) {
|
||||
for (const auto &token : lines[rowIdx]) {
|
||||
if (token.first) {
|
||||
// Emote rendering centralized in helper
|
||||
renderEmote(display, nextX, yLine, rowHeight, token.second);
|
||||
} else {
|
||||
display->drawString(nextX, yLine, token.second);
|
||||
nextX += display->getStringWidth(token.second);
|
||||
}
|
||||
}
|
||||
yLine += rowHeight;
|
||||
}
|
||||
|
||||
if (showPredictionRow) {
|
||||
this->drawFreeTextPredictionRow(display, x, viewportTop, viewportBottom, rowHeight,
|
||||
static_cast<int>(lines.size()), scrollRows, predictionPrefix);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2455,10 +2531,4 @@ void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_
|
||||
}
|
||||
}
|
||||
|
||||
String CannedMessageModule::drawWithCursor(String text, int cursor)
|
||||
{
|
||||
String result = text.substring(0, cursor) + "_" + text.substring(cursor);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -148,6 +148,12 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
unsigned long lastUpdateMillis = 0;
|
||||
String searchQuery;
|
||||
String freetext;
|
||||
static constexpr uint8_t maxFreeTextPredictions = 4;
|
||||
String freeTextPrediction;
|
||||
String freeTextPredictions[maxFreeTextPredictions];
|
||||
uint8_t freeTextPredictionCount = 0;
|
||||
uint8_t freeTextPredictionIndex = 0;
|
||||
bool freeTextPredictionSuppressed = false;
|
||||
|
||||
// === Message Storage ===
|
||||
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
|
||||
@@ -190,6 +196,12 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
|
||||
bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
|
||||
bool handleFreeTextInput(const InputEvent *event);
|
||||
String getFreeTextPrefix() const;
|
||||
void updateFreeTextPrediction();
|
||||
bool cycleFreeTextPrediction(int8_t step);
|
||||
bool acceptFreeTextPrediction(bool appendSpace);
|
||||
void drawFreeTextPredictionRow(OLEDDisplay *display, int16_t x, int16_t viewportTop, int16_t viewportBottom, int rowHeight,
|
||||
int linesCount, int scrollRows, const String &predictionPrefix);
|
||||
|
||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||
Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0},
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
char toLowerAscii(char c)
|
||||
{
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
return static_cast<char>(c + ('a' - 'A'));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// Small built-in dictionary for lightweight freetext word prediction.
|
||||
// Keep this list compact to limit flash/RAM usage on constrained targets.
|
||||
static const char *const kFreeTextPredictWords[] = {
|
||||
"about", "above", "across", "ack", "after", "again", "ahead", "all", "alive",
|
||||
"already", "answer", "anyone", "arrived", "asap", "at", "available", "back", "base",
|
||||
"battery", "because", "before", "behind", "below", "between", "busy", "bye", "call",
|
||||
"can", "cancel", "cannot", "careful", "channel", "check", "checkin", "clear", "close",
|
||||
"come", "coming", "confirmed", "contact", "continue", "copy", "current", "danger", "data",
|
||||
"delayed", "destination", "direct", "done", "down", "east", "emergency", "enroute", "ETA",
|
||||
"evening", "everyone", "failed", "feedback", "fine", "for", "from", "friendly", "going",
|
||||
"good", "got", "GPS", "grid", "group", "have", "he", "hear", "hello",
|
||||
"help", "here", "hey", "high", "hold", "home", "how", "inside", "later",
|
||||
"left", "listen", "location", "lost", "low", "maintain", "meet", "mesh", "Meshtastic",
|
||||
"message", "morning", "moving", "near", "nearby", "need", "negative", "net", "network",
|
||||
"night", "node", "none", "north", "nothing", "now", "offgrid", "offline", "okay",
|
||||
"online", "open", "out", "outside", "over", "perfect", "ping", "pickup", "please",
|
||||
"point", "positive", "position", "power", "priority", "proceed", "quick", "quiet", "radio",
|
||||
"ready", "reading", "receive", "received", "repeat", "reply", "request", "resend", "respond",
|
||||
"return", "returning", "right", "roger", "route", "running", "safe", "safety", "search",
|
||||
"secure", "see", "seen", "send", "signal", "soon", "south", "standby", "station",
|
||||
"status", "still", "stop", "success", "support", "target", "team", "temperature", "test",
|
||||
"thank", "thanks", "that", "the", "there", "these", "this", "towards", "track",
|
||||
"traffic", "unable", "understood", "update", "urgent", "vehicle", "visual", "wait", "warning",
|
||||
"watch", "weather", "welcome", "west", "when", "where", "who", "why", "will",
|
||||
"with", "work", "yes", "you", "your"};
|
||||
|
||||
bool startsWithAscii(const char *word, const String &prefixLower)
|
||||
{
|
||||
size_t prefixLen = prefixLower.length();
|
||||
if (prefixLen == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < prefixLen; ++i) {
|
||||
char wc = word[i];
|
||||
if (wc == '\0') {
|
||||
return false;
|
||||
}
|
||||
if (toLowerAscii(wc) != prefixLower[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int compareCaseInsensitiveAscii(const char *lhs, const char *rhs)
|
||||
{
|
||||
size_t i = 0;
|
||||
while (lhs[i] != '\0' && rhs[i] != '\0') {
|
||||
char l = toLowerAscii(lhs[i]);
|
||||
char r = toLowerAscii(rhs[i]);
|
||||
if (l < r) {
|
||||
return -1;
|
||||
}
|
||||
if (l > r) {
|
||||
return 1;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
if (lhs[i] == '\0' && rhs[i] == '\0') {
|
||||
return strcmp(lhs, rhs);
|
||||
}
|
||||
return (lhs[i] == '\0') ? -1 : 1;
|
||||
}
|
||||
|
||||
bool startsWithLowercasePrefix(const String &word, const String &prefixLower)
|
||||
{
|
||||
if (prefixLower.length() == 0 || word.length() < prefixLower.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < prefixLower.length(); ++i) {
|
||||
if (toLowerAscii(word[i]) != prefixLower[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String CannedMessageModule::getFreeTextPrefix() const
|
||||
{
|
||||
if (this->cursor == 0 || this->cursor > this->freetext.length() || this->cursor != this->freetext.length()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
unsigned int start = this->cursor;
|
||||
while (start > 0) {
|
||||
char c = this->freetext[start - 1];
|
||||
if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z')) {
|
||||
break;
|
||||
}
|
||||
--start;
|
||||
}
|
||||
|
||||
if (start == this->cursor) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String prefix = this->freetext.substring(start, this->cursor);
|
||||
prefix.toLowerCase();
|
||||
return prefix;
|
||||
}
|
||||
|
||||
void CannedMessageModule::updateFreeTextPrediction()
|
||||
{
|
||||
String previousSelection = this->freeTextPrediction;
|
||||
this->freeTextPrediction = "";
|
||||
this->freeTextPredictionCount = 0;
|
||||
this->freeTextPredictionIndex = 0;
|
||||
if (this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
return;
|
||||
}
|
||||
if (this->freeTextPredictionSuppressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
String prefixLower = getFreeTextPrefix();
|
||||
if (prefixLower.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < (sizeof(kFreeTextPredictWords) / sizeof(kFreeTextPredictWords[0])); ++i) {
|
||||
const char *candidate = kFreeTextPredictWords[i];
|
||||
if (!startsWithAscii(candidate, prefixLower)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t candidateLen = strlen(candidate);
|
||||
if (candidateLen <= static_cast<size_t>(prefixLower.length())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t insertAt = this->freeTextPredictionCount;
|
||||
while (insertAt > 0) {
|
||||
const String &existing = this->freeTextPredictions[insertAt - 1];
|
||||
const size_t existingLen = existing.length();
|
||||
const int lexicalCompare = compareCaseInsensitiveAscii(candidate, existing.c_str());
|
||||
if (candidateLen > existingLen || (candidateLen == existingLen && lexicalCompare >= 0)) {
|
||||
break;
|
||||
}
|
||||
--insertAt;
|
||||
}
|
||||
|
||||
if (insertAt >= maxFreeTextPredictions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->freeTextPredictionCount < maxFreeTextPredictions) {
|
||||
this->freeTextPredictionCount++;
|
||||
}
|
||||
for (int j = static_cast<int>(this->freeTextPredictionCount) - 1; j > static_cast<int>(insertAt); --j) {
|
||||
this->freeTextPredictions[j] = this->freeTextPredictions[j - 1];
|
||||
}
|
||||
this->freeTextPredictions[insertAt] = candidate;
|
||||
}
|
||||
|
||||
if (this->freeTextPredictionCount > 0) {
|
||||
uint8_t selectedIndex = 0;
|
||||
for (uint8_t i = 0; i < this->freeTextPredictionCount; ++i) {
|
||||
if (this->freeTextPredictions[i] == previousSelection) {
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->freeTextPredictionIndex = selectedIndex;
|
||||
this->freeTextPrediction = this->freeTextPredictions[this->freeTextPredictionIndex];
|
||||
}
|
||||
}
|
||||
|
||||
bool CannedMessageModule::cycleFreeTextPrediction(int8_t step)
|
||||
{
|
||||
if (this->freeTextPredictionCount < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int next = static_cast<int>(this->freeTextPredictionIndex) + static_cast<int>(step);
|
||||
while (next < 0) {
|
||||
next += this->freeTextPredictionCount;
|
||||
}
|
||||
while (next >= this->freeTextPredictionCount) {
|
||||
next -= this->freeTextPredictionCount;
|
||||
}
|
||||
this->freeTextPredictionIndex = static_cast<uint8_t>(next);
|
||||
this->freeTextPrediction = this->freeTextPredictions[this->freeTextPredictionIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CannedMessageModule::acceptFreeTextPrediction(bool appendSpace)
|
||||
{
|
||||
if (this->freeTextPrediction.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String prefixLower = getFreeTextPrefix();
|
||||
if (prefixLower.length() < 2 || this->freeTextPrediction.length() <= prefixLower.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned int start = this->cursor - prefixLower.length();
|
||||
String acceptedWord = this->freeTextPrediction;
|
||||
if (start < this->cursor && acceptedWord.length() > 0) {
|
||||
char typedFirstChar = this->freetext[start];
|
||||
if (typedFirstChar >= 'A' && typedFirstChar <= 'Z') {
|
||||
char acceptedFirstChar = acceptedWord[0];
|
||||
if (acceptedFirstChar >= 'a' && acceptedFirstChar <= 'z') {
|
||||
acceptedWord[0] = static_cast<char>(acceptedFirstChar - ('a' - 'A'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->freetext = this->freetext.substring(0, start) + acceptedWord + this->freetext.substring(this->cursor);
|
||||
this->cursor = start + acceptedWord.length();
|
||||
|
||||
const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0);
|
||||
if (appendSpace && this->cursor < maxChars) {
|
||||
this->freetext = this->freetext.substring(0, this->cursor) + " " + this->freetext.substring(this->cursor);
|
||||
this->cursor++;
|
||||
}
|
||||
if (this->freetext.length() > maxChars) {
|
||||
this->freetext = this->freetext.substring(0, maxChars);
|
||||
this->cursor = maxChars;
|
||||
}
|
||||
|
||||
// Treat a just-selected prediction as final until the user edits again.
|
||||
this->freeTextPredictionSuppressed = true;
|
||||
this->freeTextPrediction = "";
|
||||
this->freeTextPredictionCount = 0;
|
||||
this->freeTextPredictionIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CannedMessageModule::drawFreeTextPredictionRow(OLEDDisplay *display, int16_t x, int16_t viewportTop, int16_t viewportBottom,
|
||||
int rowHeight, int linesCount, int scrollRows, const String &predictionPrefix)
|
||||
{
|
||||
const int predictionRowY = viewportTop + ((linesCount - scrollRows) * rowHeight);
|
||||
if (predictionRowY < (viewportTop - rowHeight) || predictionRowY > viewportBottom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int spaceWidth = display->getStringWidth(" ");
|
||||
const int separatorWidth = spaceWidth * 2;
|
||||
const int viewportWidth = display->getWidth();
|
||||
|
||||
struct ChoiceLayout {
|
||||
uint8_t idx;
|
||||
String word;
|
||||
int startX;
|
||||
int width;
|
||||
};
|
||||
|
||||
std::vector<ChoiceLayout> choices;
|
||||
int runningX = 0;
|
||||
bool hasChoices = false;
|
||||
int selectedStart = 0;
|
||||
int selectedEnd = 0;
|
||||
|
||||
for (uint8_t i = 0; i < this->freeTextPredictionCount; ++i) {
|
||||
const String &candidate = this->freeTextPredictions[i];
|
||||
if (!startsWithLowercasePrefix(candidate, predictionPrefix) || candidate.length() <= predictionPrefix.length()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasChoices) {
|
||||
runningX += separatorWidth;
|
||||
}
|
||||
|
||||
int tokenWidth = display->getStringWidth(candidate);
|
||||
choices.push_back({i, candidate, runningX, tokenWidth});
|
||||
if (i == this->freeTextPredictionIndex) {
|
||||
selectedStart = runningX;
|
||||
selectedEnd = runningX + tokenWidth;
|
||||
}
|
||||
|
||||
runningX += tokenWidth;
|
||||
hasChoices = true;
|
||||
}
|
||||
|
||||
int choiceScrollX = 0;
|
||||
if (runningX > viewportWidth) {
|
||||
choiceScrollX = std::max(0, selectedEnd - viewportWidth);
|
||||
if (selectedStart < choiceScrollX) {
|
||||
choiceScrollX = selectedStart;
|
||||
}
|
||||
const int maxScrollX = std::max(0, runningX - viewportWidth);
|
||||
if (choiceScrollX > maxScrollX) {
|
||||
choiceScrollX = maxScrollX;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &choice : choices) {
|
||||
int drawX = x + (choice.startX - choiceScrollX);
|
||||
if ((drawX + choice.width) < x || drawX > (x + viewportWidth)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (choice.idx == this->freeTextPredictionIndex) {
|
||||
#ifdef USE_EINK
|
||||
display->drawRect(drawX - 1, predictionRowY, choice.width + 2, FONT_HEIGHT_SMALL - 1);
|
||||
display->drawString(drawX, predictionRowY, choice.word);
|
||||
#else
|
||||
display->fillRect(drawX - 1, predictionRowY, choice.width + 2, FONT_HEIGHT_SMALL);
|
||||
display->setColor(BLACK);
|
||||
display->drawString(drawX, predictionRowY, choice.word);
|
||||
display->setColor(WHITE);
|
||||
#endif
|
||||
} else {
|
||||
display->drawString(drawX, predictionRowY, choice.word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String CannedMessageModule::drawWithCursor(String text, int cursor)
|
||||
{
|
||||
if (cursor < 0) {
|
||||
cursor = 0;
|
||||
}
|
||||
if (cursor > static_cast<int>(text.length())) {
|
||||
cursor = text.length();
|
||||
}
|
||||
|
||||
String completionSuffix = "";
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor == this->freetext.length() &&
|
||||
this->freeTextPredictionCount > 0 && this->freeTextPredictionIndex < this->freeTextPredictionCount) {
|
||||
String prefixLower = this->getFreeTextPrefix();
|
||||
const String &candidate = this->freeTextPredictions[this->freeTextPredictionIndex];
|
||||
if (prefixLower.length() >= 2 && startsWithAscii(candidate.c_str(), prefixLower) &&
|
||||
candidate.length() > prefixLower.length()) {
|
||||
completionSuffix = candidate.substring(prefixLower.length());
|
||||
}
|
||||
}
|
||||
|
||||
return text.substring(0, cursor) + "|" + completionSuffix + text.substring(cursor);
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user