mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-08 08:37:38 +03:00
Compare commits
No commits in common. "bf031fd999c1fc3bd62c3761d27f8ea750dabce4" and "838246d1479f5aaed50f3eb4d43fd692bd77660f" have entirely different histories.
bf031fd999
...
838246d147
@ -106,18 +106,6 @@ std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int
|
|||||||
ans[i] = j; // j is the index of the last word in this optimal line
|
ans[i] = j; // j is the index of the last word in this optimal line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle oversized word: if no valid configuration found, force single-word line
|
|
||||||
// This prevents cascade failure where one oversized word breaks all preceding words
|
|
||||||
if (dp[i] == MAX_COST) {
|
|
||||||
ans[i] = i; // Just this word on its own line
|
|
||||||
// Inherit cost from next word to allow subsequent words to find valid configurations
|
|
||||||
if (i + 1 < static_cast<int>(totalWordCount)) {
|
|
||||||
dp[i] = dp[i + 1];
|
|
||||||
} else {
|
|
||||||
dp[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stores the index of the word that starts the next line (last_word_index + 1)
|
// Stores the index of the word that starts the next line (last_word_index + 1)
|
||||||
|
|||||||
@ -115,56 +115,26 @@ bool Section::clearCache() const {
|
|||||||
|
|
||||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const int marginRight, const int marginBottom, const int marginLeft,
|
||||||
const bool extraParagraphSpacing, const std::function<void()>& progressSetupFn,
|
const bool extraParagraphSpacing) {
|
||||||
const std::function<void(int)>& progressFn) {
|
|
||||||
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
|
||||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
|
|
||||||
// Retry logic for SD card timing issues
|
|
||||||
bool success = false;
|
|
||||||
size_t fileSize = 0;
|
|
||||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
|
||||||
if (attempt > 0) {
|
|
||||||
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
|
||||||
delay(50); // Brief delay before retry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any incomplete file from previous attempt before retrying
|
|
||||||
if (SD.exists(tmpHtmlPath.c_str())) {
|
|
||||||
SD.remove(tmpHtmlPath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
File tmpHtml;
|
File tmpHtml;
|
||||||
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
continue;
|
return false;
|
||||||
}
|
}
|
||||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
bool success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||||
fileSize = tmpHtml.size();
|
|
||||||
tmpHtml.close();
|
tmpHtml.close();
|
||||||
|
|
||||||
// If streaming failed, remove the incomplete file immediately
|
|
||||||
if (!success && SD.exists(tmpHtmlPath.c_str())) {
|
|
||||||
SD.remove(tmpHtmlPath.c_str());
|
|
||||||
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str());
|
||||||
|
|
||||||
// Only show progress bar for larger chapters where rendering overhead is worth it
|
ChapterHtmlSlimParser visitor(tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom,
|
||||||
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
marginLeft, extraParagraphSpacing,
|
||||||
progressSetupFn();
|
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
||||||
}
|
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
|
||||||
tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
|
||||||
extraParagraphSpacing, [this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); }, progressFn);
|
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
SD.remove(tmpHtmlPath.c_str());
|
SD.remove(tmpHtmlPath.c_str());
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
@ -32,8 +31,6 @@ class Section {
|
|||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||||
int marginLeft, bool extraParagraphSpacing,
|
int marginLeft, bool extraParagraphSpacing);
|
||||||
const std::function<void()>& progressSetupFn = nullptr,
|
|
||||||
const std::function<void(int)>& progressFn = nullptr);
|
|
||||||
std::unique_ptr<Page> loadPageFromSD() const;
|
std::unique_ptr<Page> loadPageFromSD() const;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,18 +4,11 @@
|
|||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
// Validate iterator bounds before rendering
|
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
|
||||||
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
|
||||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto wordIt = words.begin();
|
auto wordIt = words.begin();
|
||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
auto wordXposIt = wordXpos.begin();
|
auto wordXposIt = wordXpos.begin();
|
||||||
|
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
for (int i = 0; i < words.size(); i++) {
|
||||||
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
||||||
|
|
||||||
std::advance(wordIt, 1);
|
std::advance(wordIt, 1);
|
||||||
@ -53,13 +46,6 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
|
|||||||
|
|
||||||
// words
|
// words
|
||||||
serialization::readPod(file, wc);
|
serialization::readPod(file, wc);
|
||||||
|
|
||||||
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
|
||||||
if (wc > 10000) {
|
|
||||||
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
words.resize(wc);
|
words.resize(wc);
|
||||||
for (auto& w : words) serialization::readString(file, w);
|
for (auto& w : words) serialization::readString(file, w);
|
||||||
|
|
||||||
@ -73,13 +59,6 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
|
|||||||
wordStyles.resize(sc);
|
wordStyles.resize(sc);
|
||||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||||
|
|
||||||
// Validate data consistency: all three lists must have the same size
|
|
||||||
if (wc != xc || wc != sc) {
|
|
||||||
Serial.printf("[%lu] [TXB] Deserialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), wc,
|
|
||||||
xc, sc);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// style
|
// style
|
||||||
serialization::readPod(file, style);
|
serialization::readPod(file, style);
|
||||||
|
|
||||||
|
|||||||
@ -11,9 +11,6 @@
|
|||||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||||
|
|
||||||
// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it
|
|
||||||
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
|
||||||
|
|
||||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||||
|
|
||||||
@ -224,11 +221,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file size for progress calculation
|
|
||||||
const size_t totalSize = file.size();
|
|
||||||
size_t bytesRead = 0;
|
|
||||||
int lastProgress = -1;
|
|
||||||
|
|
||||||
XML_SetUserData(parser, this);
|
XML_SetUserData(parser, this);
|
||||||
XML_SetElementHandler(parser, startElement, endElement);
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
XML_SetCharacterDataHandler(parser, characterData);
|
XML_SetCharacterDataHandler(parser, characterData);
|
||||||
@ -257,17 +249,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update progress (call every 10% change to avoid too frequent updates)
|
|
||||||
// Only show progress for larger chapters where rendering overhead is worth it
|
|
||||||
bytesRead += len;
|
|
||||||
if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) {
|
|
||||||
const int progress = static_cast<int>((bytesRead * 100) / totalSize);
|
|
||||||
if (lastProgress / 10 != progress / 10) {
|
|
||||||
lastProgress = progress;
|
|
||||||
progressFn(progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
done = file.available() == 0;
|
done = file.available() == 0;
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
|
|||||||
@ -18,7 +18,6 @@ class ChapterHtmlSlimParser {
|
|||||||
const std::string& filepath;
|
const std::string& filepath;
|
||||||
GfxRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||||
std::function<void(int)> progressFn; // Progress callback (0-100)
|
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
int skipUntilDepth = INT_MAX;
|
int skipUntilDepth = INT_MAX;
|
||||||
int boldUntilDepth = INT_MAX;
|
int boldUntilDepth = INT_MAX;
|
||||||
@ -49,8 +48,7 @@ class ChapterHtmlSlimParser {
|
|||||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
||||||
const float lineCompression, const int marginTop, const int marginRight,
|
const float lineCompression, const int marginTop, const int marginRight,
|
||||||
const int marginBottom, const int marginLeft, const bool extraParagraphSpacing,
|
const int marginBottom, const int marginLeft, const bool extraParagraphSpacing,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
|
||||||
const std::function<void(int)>& progressFn = nullptr)
|
|
||||||
: filepath(filepath),
|
: filepath(filepath),
|
||||||
renderer(renderer),
|
renderer(renderer),
|
||||||
fontId(fontId),
|
fontId(fontId),
|
||||||
@ -60,8 +58,7 @@ class ChapterHtmlSlimParser {
|
|||||||
marginBottom(marginBottom),
|
marginBottom(marginBottom),
|
||||||
marginLeft(marginLeft),
|
marginLeft(marginLeft),
|
||||||
extraParagraphSpacing(extraParagraphSpacing),
|
extraParagraphSpacing(extraParagraphSpacing),
|
||||||
completePageFn(completePageFn),
|
completePageFn(completePageFn) {}
|
||||||
progressFn(progressFn) {}
|
|
||||||
~ChapterHtmlSlimParser() = default;
|
~ChapterHtmlSlimParser() = default;
|
||||||
bool parseAndBuildPages();
|
bool parseAndBuildPages();
|
||||||
void addLineToPage(std::shared_ptr<TextBlock> line);
|
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||||
|
|||||||
@ -187,21 +187,11 @@ void WifiSelectionActivity::selectNetwork(const int index) {
|
|||||||
if (selectedRequiresPassword) {
|
if (selectedRequiresPassword) {
|
||||||
// Show password entry
|
// Show password entry
|
||||||
state = WifiSelectionState::PASSWORD_ENTRY;
|
state = WifiSelectionState::PASSWORD_ENTRY;
|
||||||
enterNewActivity(new KeyboardEntryActivity(
|
enterNewActivity(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password",
|
||||||
renderer, inputManager, "Enter WiFi Password",
|
|
||||||
"", // No initial text
|
"", // No initial text
|
||||||
50, // Y position
|
|
||||||
64, // Max password length
|
64, // Max password length
|
||||||
false, // Show password by default (hard keyboard to use)
|
false // Show password by default (hard keyboard to use)
|
||||||
[this](const std::string& text) {
|
));
|
||||||
enteredPassword = text;
|
|
||||||
exitActivity();
|
|
||||||
},
|
|
||||||
[this] {
|
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
|
||||||
updateRequired = true;
|
|
||||||
exitActivity();
|
|
||||||
}));
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
// Connect directly for open networks
|
// Connect directly for open networks
|
||||||
@ -218,6 +208,11 @@ void WifiSelectionActivity::attemptConnection() {
|
|||||||
|
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
// Get password from keyboard if we just entered it
|
||||||
|
if (subActivity && !usedSavedPassword) {
|
||||||
|
enteredPassword = static_cast<KeyboardEntryActivity*>(subActivity.get())->getText();
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedRequiresPassword && !enteredPassword.empty()) {
|
if (selectedRequiresPassword && !enteredPassword.empty()) {
|
||||||
WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str());
|
WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str());
|
||||||
} else {
|
} else {
|
||||||
@ -274,11 +269,6 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::loop() {
|
void WifiSelectionActivity::loop() {
|
||||||
if (subActivity) {
|
|
||||||
subActivity->loop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check scan progress
|
// Check scan progress
|
||||||
if (state == WifiSelectionState::SCANNING) {
|
if (state == WifiSelectionState::SCANNING) {
|
||||||
processWifiScanResults();
|
processWifiScanResults();
|
||||||
@ -291,12 +281,27 @@ void WifiSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == WifiSelectionState::PASSWORD_ENTRY) {
|
// Handle password entry state
|
||||||
// Reach here once password entry finished in subactivity
|
if (state == WifiSelectionState::PASSWORD_ENTRY && subActivity) {
|
||||||
|
const auto keyboard = static_cast<KeyboardEntryActivity*>(subActivity.get());
|
||||||
|
keyboard->handleInput();
|
||||||
|
|
||||||
|
if (keyboard->isComplete()) {
|
||||||
attemptConnection();
|
attemptConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyboard->isCancelled()) {
|
||||||
|
state = WifiSelectionState::NETWORK_LIST;
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle save prompt state
|
// Handle save prompt state
|
||||||
if (state == WifiSelectionState::SAVE_PROMPT) {
|
if (state == WifiSelectionState::SAVE_PROMPT) {
|
||||||
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
|
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
|
||||||
@ -436,10 +441,6 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi
|
|||||||
|
|
||||||
void WifiSelectionActivity::displayTaskLoop() {
|
void WifiSelectionActivity::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (subActivity) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
@ -460,6 +461,9 @@ void WifiSelectionActivity::render() const {
|
|||||||
case WifiSelectionState::NETWORK_LIST:
|
case WifiSelectionState::NETWORK_LIST:
|
||||||
renderNetworkList();
|
renderNetworkList();
|
||||||
break;
|
break;
|
||||||
|
case WifiSelectionState::PASSWORD_ENTRY:
|
||||||
|
renderPasswordEntry();
|
||||||
|
break;
|
||||||
case WifiSelectionState::CONNECTING:
|
case WifiSelectionState::CONNECTING:
|
||||||
renderConnecting();
|
renderConnecting();
|
||||||
break;
|
break;
|
||||||
@ -557,6 +561,23 @@ void WifiSelectionActivity::renderNetworkList() const {
|
|||||||
renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", "");
|
renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WifiSelectionActivity::renderPasswordEntry() const {
|
||||||
|
// Draw header
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD);
|
||||||
|
|
||||||
|
// Draw network name with good spacing from header
|
||||||
|
std::string networkInfo = "Network: " + selectedSSID;
|
||||||
|
if (networkInfo.length() > 30) {
|
||||||
|
networkInfo.replace(27, networkInfo.length() - 27, "...");
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
// Draw keyboard
|
||||||
|
if (subActivity) {
|
||||||
|
static_cast<KeyboardEntryActivity*>(subActivity.get())->render(58);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::renderConnecting() const {
|
void WifiSelectionActivity::renderConnecting() const {
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||||
|
|||||||
@ -227,51 +227,23 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
SETTINGS.extraParagraphSpacing)) {
|
SETTINGS.extraParagraphSpacing)) {
|
||||||
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
||||||
|
|
||||||
// Progress bar dimensions
|
|
||||||
constexpr int barWidth = 200;
|
|
||||||
constexpr int barHeight = 10;
|
|
||||||
constexpr int boxMargin = 20;
|
|
||||||
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
|
|
||||||
const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2;
|
|
||||||
const int boxWidthNoBar = textWidth + boxMargin * 2;
|
|
||||||
const int boxHeightWithBar = renderer.getLineHeight(READER_FONT_ID) + barHeight + boxMargin * 3;
|
|
||||||
const int boxHeightNoBar = renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2;
|
|
||||||
const int boxXWithBar = (GfxRenderer::getScreenWidth() - boxWidthWithBar) / 2;
|
|
||||||
const int boxXNoBar = (GfxRenderer::getScreenWidth() - boxWidthNoBar) / 2;
|
|
||||||
constexpr int boxY = 50;
|
|
||||||
const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2;
|
|
||||||
const int barY = boxY + renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2;
|
|
||||||
|
|
||||||
// Always show "Indexing..." text first
|
|
||||||
{
|
{
|
||||||
renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false);
|
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
|
||||||
renderer.drawText(READER_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing...");
|
constexpr int margin = 20;
|
||||||
renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10);
|
const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2;
|
||||||
|
constexpr int y = 50;
|
||||||
|
const int w = textWidth + margin * 2;
|
||||||
|
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
|
||||||
|
renderer.fillRect(x, y, w, h, false);
|
||||||
|
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
|
||||||
|
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
pagesUntilFullRefresh = 0;
|
pagesUntilFullRefresh = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section->setupCacheDir();
|
section->setupCacheDir();
|
||||||
|
|
||||||
// Setup callback - only called for chapters >= 50KB, redraws with progress bar
|
|
||||||
auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, boxMargin, barX, barY, barWidth,
|
|
||||||
barHeight]() {
|
|
||||||
renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false);
|
|
||||||
renderer.drawText(READER_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing...");
|
|
||||||
renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10);
|
|
||||||
renderer.drawRect(barX, barY, barWidth, barHeight);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Progress callback to update progress bar
|
|
||||||
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
|
|
||||||
const int fillWidth = (barWidth - 2) * progress / 100;
|
|
||||||
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
|
|
||||||
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
||||||
marginLeft, SETTINGS.extraParagraphSpacing, progressSetup, progressCallback)) {
|
marginLeft, SETTINGS.extraParagraphSpacing)) {
|
||||||
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
||||||
section.reset();
|
section.reset();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -57,9 +57,9 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
|
|
||||||
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
|
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
|
||||||
|
|
||||||
if (inputManager.wasReleased(InputManager::BTN_CONFIRM)) {
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
onSelectSpineIndex(selectorIndex);
|
onSelectSpineIndex(selectorIndex);
|
||||||
} else if (inputManager.wasReleased(InputManager::BTN_BACK)) {
|
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
} else if (prevReleased) {
|
||||||
if (skipPage) {
|
if (skipPage) {
|
||||||
|
|||||||
@ -10,55 +10,41 @@ const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = {
|
|||||||
|
|
||||||
// Keyboard layouts - uppercase/symbols
|
// Keyboard layouts - uppercase/symbols
|
||||||
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
|
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
|
||||||
"ZXCVBNM<>?", "SPECIAL ROW"};
|
"ZXCVBNM<>?", "^ _____<OK"};
|
||||||
|
|
||||||
void KeyboardEntryActivity::taskTrampoline(void* param) {
|
void KeyboardEntryActivity::setText(const std::string& newText) {
|
||||||
auto* self = static_cast<KeyboardEntryActivity*>(param);
|
text = newText;
|
||||||
self->displayTaskLoop();
|
if (maxLength > 0 && text.length() > maxLength) {
|
||||||
|
text.resize(maxLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::displayTaskLoop() {
|
void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string& newInitialText) {
|
||||||
while (true) {
|
if (!newTitle.empty()) {
|
||||||
if (updateRequired) {
|
title = newTitle;
|
||||||
updateRequired = false;
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
render();
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
}
|
}
|
||||||
|
text = newInitialText;
|
||||||
|
selectedRow = 0;
|
||||||
|
selectedCol = 0;
|
||||||
|
shiftActive = false;
|
||||||
|
complete = false;
|
||||||
|
cancelled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::onEnter() {
|
void KeyboardEntryActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
// Reset state when entering the activity
|
||||||
|
complete = false;
|
||||||
// Trigger first update
|
cancelled = false;
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
xTaskCreate(&KeyboardEntryActivity::taskTrampoline, "KeyboardEntryActivity",
|
|
||||||
2048, // Stack size
|
|
||||||
this, // Parameters
|
|
||||||
1, // Priority
|
|
||||||
&displayTaskHandle // Task handle
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::onExit() {
|
void KeyboardEntryActivity::loop() {
|
||||||
Activity::onExit();
|
handleInput();
|
||||||
|
render(10);
|
||||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
|
||||||
vTaskDelete(displayTaskHandle);
|
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int KeyboardEntryActivity::getRowLength(const int row) const {
|
int KeyboardEntryActivity::getRowLength(int row) const {
|
||||||
if (row < 0 || row >= NUM_ROWS) return 0;
|
if (row < 0 || row >= NUM_ROWS) return 0;
|
||||||
|
|
||||||
// Return actual length of each row based on keyboard layout
|
// Return actual length of each row based on keyboard layout
|
||||||
@ -72,7 +58,7 @@ int KeyboardEntryActivity::getRowLength(const int row) const {
|
|||||||
case 3:
|
case 3:
|
||||||
return 10; // zxcvbnm,./
|
return 10; // zxcvbnm,./
|
||||||
case 4:
|
case 4:
|
||||||
return 10; // caps (2 wide), space (5 wide), backspace (2 wide), OK
|
return 10; // ^, space (5 wide), backspace, OK (2 wide)
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -89,8 +75,8 @@ char KeyboardEntryActivity::getSelectedChar() const {
|
|||||||
|
|
||||||
void KeyboardEntryActivity::handleKeyPress() {
|
void KeyboardEntryActivity::handleKeyPress() {
|
||||||
// Handle special row (bottom row with shift, space, backspace, done)
|
// Handle special row (bottom row with shift, space, backspace, done)
|
||||||
if (selectedRow == SPECIAL_ROW) {
|
if (selectedRow == SHIFT_ROW) {
|
||||||
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
|
if (selectedCol == SHIFT_COL) {
|
||||||
// Shift toggle
|
// Shift toggle
|
||||||
shiftActive = !shiftActive;
|
shiftActive = !shiftActive;
|
||||||
return;
|
return;
|
||||||
@ -104,7 +90,7 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) {
|
if (selectedCol == BACKSPACE_COL) {
|
||||||
// Backspace
|
// Backspace
|
||||||
if (!text.empty()) {
|
if (!text.empty()) {
|
||||||
text.pop_back();
|
text.pop_back();
|
||||||
@ -114,6 +100,7 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
|
|
||||||
if (selectedCol >= DONE_COL) {
|
if (selectedCol >= DONE_COL) {
|
||||||
// Done button
|
// Done button
|
||||||
|
complete = true;
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete(text);
|
onComplete(text);
|
||||||
}
|
}
|
||||||
@ -122,11 +109,8 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Regular character
|
// Regular character
|
||||||
const char c = getSelectedChar();
|
char c = getSelectedChar();
|
||||||
if (c == '\0') {
|
if (c != '\0' && c != '^' && c != '_' && c != '<') {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxLength == 0 || text.length() < maxLength) {
|
if (maxLength == 0 || text.length() < maxLength) {
|
||||||
text += c;
|
text += c;
|
||||||
// Auto-disable shift after typing a letter
|
// Auto-disable shift after typing a letter
|
||||||
@ -134,49 +118,33 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
shiftActive = false;
|
shiftActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::loop() {
|
bool KeyboardEntryActivity::handleInput() {
|
||||||
|
if (complete || cancelled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
if (inputManager.wasPressed(InputManager::BTN_UP)) {
|
if (inputManager.wasPressed(InputManager::BTN_UP)) {
|
||||||
if (selectedRow > 0) {
|
if (selectedRow > 0) {
|
||||||
selectedRow--;
|
selectedRow--;
|
||||||
// Clamp column to valid range for new row
|
// Clamp column to valid range for new row
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
int maxCol = getRowLength(selectedRow) - 1;
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
handled = true;
|
||||||
}
|
} else if (inputManager.wasPressed(InputManager::BTN_DOWN)) {
|
||||||
|
|
||||||
if (inputManager.wasPressed(InputManager::BTN_DOWN)) {
|
|
||||||
if (selectedRow < NUM_ROWS - 1) {
|
if (selectedRow < NUM_ROWS - 1) {
|
||||||
selectedRow++;
|
selectedRow++;
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
int maxCol = getRowLength(selectedRow) - 1;
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
handled = true;
|
||||||
}
|
} else if (inputManager.wasPressed(InputManager::BTN_LEFT)) {
|
||||||
|
|
||||||
if (inputManager.wasPressed(InputManager::BTN_LEFT)) {
|
|
||||||
// Special bottom row case
|
|
||||||
if (selectedRow == SPECIAL_ROW) {
|
|
||||||
// Bottom row has special key widths
|
|
||||||
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
|
|
||||||
// In shift key, do nothing
|
|
||||||
} else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
|
|
||||||
// In space bar, move to shift
|
|
||||||
selectedCol = SHIFT_COL;
|
|
||||||
} else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) {
|
|
||||||
// In backspace, move to space
|
|
||||||
selectedCol = SPACE_COL;
|
|
||||||
} else if (selectedCol >= DONE_COL) {
|
|
||||||
// At done button, move to backspace
|
|
||||||
selectedCol = BACKSPACE_COL;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol > 0) {
|
if (selectedCol > 0) {
|
||||||
selectedCol--;
|
selectedCol--;
|
||||||
} else if (selectedRow > 0) {
|
} else if (selectedRow > 0) {
|
||||||
@ -184,31 +152,9 @@ void KeyboardEntryActivity::loop() {
|
|||||||
selectedRow--;
|
selectedRow--;
|
||||||
selectedCol = getRowLength(selectedRow) - 1;
|
selectedCol = getRowLength(selectedRow) - 1;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
handled = true;
|
||||||
}
|
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT)) {
|
||||||
|
int maxCol = getRowLength(selectedRow) - 1;
|
||||||
if (inputManager.wasPressed(InputManager::BTN_RIGHT)) {
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
|
|
||||||
// Special bottom row case
|
|
||||||
if (selectedRow == SPECIAL_ROW) {
|
|
||||||
// Bottom row has special key widths
|
|
||||||
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
|
|
||||||
// In shift key, move to space
|
|
||||||
selectedCol = SPACE_COL;
|
|
||||||
} else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
|
|
||||||
// In space bar, move to backspace
|
|
||||||
selectedCol = BACKSPACE_COL;
|
|
||||||
} else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) {
|
|
||||||
// In backspace, move to done
|
|
||||||
selectedCol = DONE_COL;
|
|
||||||
} else if (selectedCol >= DONE_COL) {
|
|
||||||
// At done button, do nothing
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol < maxCol) {
|
if (selectedCol < maxCol) {
|
||||||
selectedCol++;
|
selectedCol++;
|
||||||
} else if (selectedRow < NUM_ROWS - 1) {
|
} else if (selectedRow < NUM_ROWS - 1) {
|
||||||
@ -216,34 +162,35 @@ void KeyboardEntryActivity::loop() {
|
|||||||
selectedRow++;
|
selectedRow++;
|
||||||
selectedCol = 0;
|
selectedCol = 0;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
handleKeyPress();
|
handleKeyPress();
|
||||||
updateRequired = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel
|
// Cancel
|
||||||
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
cancelled = true;
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::render() const {
|
void KeyboardEntryActivity::render(int startY) const {
|
||||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
|
|
||||||
renderer.clearScreen();
|
|
||||||
|
|
||||||
// Draw title
|
// Draw title
|
||||||
renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR);
|
renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR);
|
||||||
|
|
||||||
// Draw input field
|
// Draw input field
|
||||||
const int inputY = startY + 22;
|
int inputY = startY + 22;
|
||||||
renderer.drawText(UI_FONT_ID, 10, inputY, "[");
|
renderer.drawText(UI_FONT_ID, 10, inputY, "[");
|
||||||
|
|
||||||
std::string displayText;
|
std::string displayText;
|
||||||
@ -257,9 +204,9 @@ void KeyboardEntryActivity::render() const {
|
|||||||
displayText += "_";
|
displayText += "_";
|
||||||
|
|
||||||
// Truncate if too long for display - use actual character width from font
|
// Truncate if too long for display - use actual character width from font
|
||||||
int approxCharWidth = renderer.getSpaceWidth(UI_FONT_ID);
|
int charWidth = renderer.getSpaceWidth(UI_FONT_ID);
|
||||||
if (approxCharWidth < 1) approxCharWidth = 8; // Fallback to approximate width
|
if (charWidth < 1) charWidth = 8; // Fallback to approximate width
|
||||||
const int maxDisplayLen = (pageWidth - 40) / approxCharWidth;
|
int maxDisplayLen = (pageWidth - 40) / charWidth;
|
||||||
if (displayText.length() > static_cast<size_t>(maxDisplayLen)) {
|
if (displayText.length() > static_cast<size_t>(maxDisplayLen)) {
|
||||||
displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3);
|
displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3);
|
||||||
}
|
}
|
||||||
@ -268,22 +215,22 @@ void KeyboardEntryActivity::render() const {
|
|||||||
renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]");
|
renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]");
|
||||||
|
|
||||||
// Draw keyboard - use compact spacing to fit 5 rows on screen
|
// Draw keyboard - use compact spacing to fit 5 rows on screen
|
||||||
const int keyboardStartY = inputY + 25;
|
int keyboardStartY = inputY + 25;
|
||||||
constexpr int keyWidth = 18;
|
const int keyWidth = 18;
|
||||||
constexpr int keyHeight = 18;
|
const int keyHeight = 18;
|
||||||
constexpr int keySpacing = 3;
|
const int keySpacing = 3;
|
||||||
|
|
||||||
const char* const* layout = shiftActive ? keyboardShift : keyboard;
|
const char* const* layout = shiftActive ? keyboardShift : keyboard;
|
||||||
|
|
||||||
// Calculate left margin to center the longest row (13 keys)
|
// Calculate left margin to center the longest row (13 keys)
|
||||||
constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
|
int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
|
||||||
const int leftMargin = (pageWidth - maxRowWidth) / 2;
|
int leftMargin = (pageWidth - maxRowWidth) / 2;
|
||||||
|
|
||||||
for (int row = 0; row < NUM_ROWS; row++) {
|
for (int row = 0; row < NUM_ROWS; row++) {
|
||||||
const int rowY = keyboardStartY + row * (keyHeight + keySpacing);
|
int rowY = keyboardStartY + row * (keyHeight + keySpacing);
|
||||||
|
|
||||||
// Left-align all rows for consistent navigation
|
// Left-align all rows for consistent navigation
|
||||||
const int startX = leftMargin;
|
int startX = leftMargin;
|
||||||
|
|
||||||
// Handle bottom row (row 4) specially with proper multi-column keys
|
// Handle bottom row (row 4) specially with proper multi-column keys
|
||||||
if (row == 4) {
|
if (row == 4) {
|
||||||
@ -293,37 +240,64 @@ void KeyboardEntryActivity::render() const {
|
|||||||
int currentX = startX;
|
int currentX = startX;
|
||||||
|
|
||||||
// CAPS key (logical col 0, spans 2 key widths)
|
// CAPS key (logical col 0, spans 2 key widths)
|
||||||
const bool capsSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
|
int capsWidth = 2 * keyWidth + keySpacing;
|
||||||
renderItemWithSelector(currentX + 2, rowY, shiftActive ? "CAPS" : "caps", capsSelected);
|
bool capsSelected = (selectedRow == 4 && selectedCol == SHIFT_COL);
|
||||||
currentX += 2 * (keyWidth + keySpacing);
|
if (capsSelected) {
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX + capsWidth - 4, rowY, "]");
|
||||||
|
}
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX + 2, rowY, shiftActive ? "CAPS" : "caps");
|
||||||
|
currentX += capsWidth + keySpacing;
|
||||||
|
|
||||||
// Space bar (logical cols 2-6, spans 5 key widths)
|
// Space bar (logical cols 2-6, spans 5 key widths)
|
||||||
const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
|
int spaceWidth = 5 * keyWidth + 4 * keySpacing;
|
||||||
const int spaceTextWidth = renderer.getTextWidth(UI_FONT_ID, "_____");
|
bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
|
||||||
const int spaceXWidth = 5 * (keyWidth + keySpacing);
|
if (spaceSelected) {
|
||||||
const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2;
|
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
|
||||||
renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected);
|
renderer.drawText(UI_FONT_ID, currentX + spaceWidth - 4, rowY, "]");
|
||||||
currentX += spaceXWidth;
|
}
|
||||||
|
// Draw centered underscores for space bar
|
||||||
|
int spaceTextX = currentX + (spaceWidth / 2) - 12;
|
||||||
|
renderer.drawText(UI_FONT_ID, spaceTextX, rowY, "_____");
|
||||||
|
currentX += spaceWidth + keySpacing;
|
||||||
|
|
||||||
// Backspace key (logical col 7, spans 2 key widths)
|
// Backspace key (logical col 7, spans 2 key widths)
|
||||||
const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL);
|
int bsWidth = 2 * keyWidth + keySpacing;
|
||||||
renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected);
|
bool bsSelected = (selectedRow == 4 && selectedCol == BACKSPACE_COL);
|
||||||
currentX += 2 * (keyWidth + keySpacing);
|
if (bsSelected) {
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX + bsWidth - 4, rowY, "]");
|
||||||
|
}
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX + 6, rowY, "<-");
|
||||||
|
currentX += bsWidth + keySpacing;
|
||||||
|
|
||||||
// OK button (logical col 9, spans 2 key widths)
|
// OK button (logical col 9, spans 2 key widths)
|
||||||
const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL);
|
int okWidth = 2 * keyWidth + keySpacing;
|
||||||
renderItemWithSelector(currentX + 2, rowY, "OK", okSelected);
|
bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL);
|
||||||
|
if (okSelected) {
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX + okWidth - 4, rowY, "]");
|
||||||
|
}
|
||||||
|
renderer.drawText(UI_FONT_ID, currentX + 8, rowY, "OK");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Regular rows: render each key individually
|
// Regular rows: render each key individually
|
||||||
for (int col = 0; col < getRowLength(row); col++) {
|
for (int col = 0; col < getRowLength(row); col++) {
|
||||||
// Get the character to display
|
int keyX = startX + col * (keyWidth + keySpacing);
|
||||||
const char c = layout[row][col];
|
|
||||||
std::string keyLabel(1, c);
|
|
||||||
const int charWidth = renderer.getTextWidth(UI_FONT_ID, keyLabel.c_str());
|
|
||||||
|
|
||||||
const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2;
|
// Get the character to display
|
||||||
const bool isSelected = row == selectedRow && col == selectedCol;
|
char c = layout[row][col];
|
||||||
renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected);
|
std::string keyLabel(1, c);
|
||||||
|
|
||||||
|
// Draw selection highlight
|
||||||
|
bool isSelected = (row == selectedRow && col == selectedCol);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
renderer.drawText(UI_FONT_ID, keyX - 2, rowY, "[");
|
||||||
|
renderer.drawText(UI_FONT_ID, keyX + keyWidth - 4, rowY, "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawText(UI_FONT_ID, keyX + 2, rowY, keyLabel.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,15 +305,4 @@ void KeyboardEntryActivity::render() const {
|
|||||||
// Draw help text at absolute bottom of screen (consistent with other screens)
|
// Draw help text at absolute bottom of screen (consistent with other screens)
|
||||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||||
renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK");
|
renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK");
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item,
|
|
||||||
const bool isSelected) const {
|
|
||||||
if (isSelected) {
|
|
||||||
const int itemWidth = renderer.getTextWidth(UI_FONT_ID, item);
|
|
||||||
renderer.drawText(UI_FONT_ID, x - 6, y, "[");
|
|
||||||
renderer.drawText(UI_FONT_ID, x + itemWidth, y, "]");
|
|
||||||
}
|
|
||||||
renderer.drawText(UI_FONT_ID, x, y, item);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/semphr.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
|
||||||
@ -34,44 +30,80 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
* @param inputManager Reference to InputManager for handling input
|
* @param inputManager Reference to InputManager for handling input
|
||||||
* @param title Title to display above the keyboard
|
* @param title Title to display above the keyboard
|
||||||
* @param initialText Initial text to show in the input field
|
* @param initialText Initial text to show in the input field
|
||||||
* @param startY Y position to start rendering the keyboard
|
|
||||||
* @param maxLength Maximum length of input text (0 for unlimited)
|
* @param maxLength Maximum length of input text (0 for unlimited)
|
||||||
* @param isPassword If true, display asterisks instead of actual characters
|
* @param isPassword If true, display asterisks instead of actual characters
|
||||||
* @param onComplete Callback invoked when input is complete
|
|
||||||
* @param onCancel Callback invoked when input is cancelled
|
|
||||||
*/
|
*/
|
||||||
explicit KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, std::string title = "Enter Text",
|
KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text",
|
||||||
std::string initialText = "", const int startY = 10, const size_t maxLength = 0,
|
const std::string& initialText = "", const size_t maxLength = 0, const bool isPassword = false)
|
||||||
const bool isPassword = false, OnCompleteCallback onComplete = nullptr,
|
|
||||||
OnCancelCallback onCancel = nullptr)
|
|
||||||
: Activity("KeyboardEntry", renderer, inputManager),
|
: Activity("KeyboardEntry", renderer, inputManager),
|
||||||
title(std::move(title)),
|
title(title),
|
||||||
text(std::move(initialText)),
|
text(initialText),
|
||||||
startY(startY),
|
|
||||||
maxLength(maxLength),
|
maxLength(maxLength),
|
||||||
isPassword(isPassword),
|
isPassword(isPassword) {}
|
||||||
onComplete(std::move(onComplete)),
|
|
||||||
onCancel(std::move(onCancel)) {}
|
/**
|
||||||
|
* Handle button input. Call this in your main loop.
|
||||||
|
* @return true if input was handled, false otherwise
|
||||||
|
*/
|
||||||
|
bool handleInput();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the keyboard at the specified Y position.
|
||||||
|
* @param startY Y-coordinate where keyboard rendering starts (default 10)
|
||||||
|
*/
|
||||||
|
void render(int startY = 10) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current text entered by the user.
|
||||||
|
*/
|
||||||
|
const std::string& getText() const { return text; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current text.
|
||||||
|
*/
|
||||||
|
void setText(const std::string& newText);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has completed text entry (pressed OK on Done).
|
||||||
|
*/
|
||||||
|
bool isComplete() const { return complete; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has cancelled text entry.
|
||||||
|
*/
|
||||||
|
bool isCancelled() const { return cancelled; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the keyboard state for reuse.
|
||||||
|
*/
|
||||||
|
void reset(const std::string& newTitle = "", const std::string& newInitialText = "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set callback for when input is complete.
|
||||||
|
*/
|
||||||
|
void setOnComplete(OnCompleteCallback callback) { onComplete = callback; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set callback for when input is cancelled.
|
||||||
|
*/
|
||||||
|
void setOnCancel(OnCancelCallback callback) { onCancel = callback; }
|
||||||
|
|
||||||
// Activity overrides
|
// Activity overrides
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string title;
|
std::string title;
|
||||||
int startY;
|
|
||||||
std::string text;
|
std::string text;
|
||||||
size_t maxLength;
|
size_t maxLength;
|
||||||
bool isPassword;
|
bool isPassword;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
|
||||||
bool updateRequired = false;
|
|
||||||
|
|
||||||
// Keyboard state
|
// Keyboard state
|
||||||
int selectedRow = 0;
|
int selectedRow = 0;
|
||||||
int selectedCol = 0;
|
int selectedCol = 0;
|
||||||
bool shiftActive = false;
|
bool shiftActive = false;
|
||||||
|
bool complete = false;
|
||||||
|
bool cancelled = false;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
OnCompleteCallback onComplete;
|
OnCompleteCallback onComplete;
|
||||||
@ -84,17 +116,16 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
static const char* const keyboardShift[NUM_ROWS];
|
static const char* const keyboardShift[NUM_ROWS];
|
||||||
|
|
||||||
// Special key positions (bottom row)
|
// Special key positions (bottom row)
|
||||||
static constexpr int SPECIAL_ROW = 4;
|
static constexpr int SHIFT_ROW = 4;
|
||||||
static constexpr int SHIFT_COL = 0;
|
static constexpr int SHIFT_COL = 0;
|
||||||
|
static constexpr int SPACE_ROW = 4;
|
||||||
static constexpr int SPACE_COL = 2;
|
static constexpr int SPACE_COL = 2;
|
||||||
|
static constexpr int BACKSPACE_ROW = 4;
|
||||||
static constexpr int BACKSPACE_COL = 7;
|
static constexpr int BACKSPACE_COL = 7;
|
||||||
|
static constexpr int DONE_ROW = 4;
|
||||||
static constexpr int DONE_COL = 9;
|
static constexpr int DONE_COL = 9;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
char getSelectedChar() const;
|
char getSelectedChar() const;
|
||||||
void handleKeyPress();
|
void handleKeyPress();
|
||||||
int getRowLength(int row) const;
|
int getRowLength(int row) const;
|
||||||
void render() const;
|
|
||||||
void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user