This commit completes a series of fixes addressing dictionary crashes,
memory issues, and UI/UX improvements.
Memory & Stability (from previous checkpoints):
- Add uncompressed dictionary (.dict) support to avoid decompression
memory issues with large dictzip chunks (58KB -> direct read)
- Implement chunked on-demand HTML parsing for large definitions,
parsing pages as user navigates rather than all at once
- Refactor TextBlock/ParsedText from std::list to std::vector,
reducing heap allocations by ~12x per TextBlock and eliminating
crashes from repeated page navigation due to heap fragmentation
- Limit cached pages to MAX_CACHED_PAGES (4) with re-parse capability
for backward navigation beyond the cache window
UI/Layout Fixes (this commit):
- Restore DictionaryMargins.h for proper orientation-aware button
hint space (front buttons: 45px, side buttons: 50px)
- Add side button hints to definition screen with proper "<" / ">"
labels for page navigation
- Add side button hints to word selection screen ("UP"/"DOWN" labels,
borderless, small font, 2px edge margin)
- Add side button hints to dictionary menu ("< Prev", "Next >")
- Fix double-button press bug when loading new chunks by checking
forward navigation availability after parsing instead of page count
- Add drawSideButtonHints() drawBorder parameter for minimal hints
- Add drawTextRotated90CCW() for LandscapeCCW text orientation
- Move page indicator up to avoid bezel cutoff
263 lines
7.3 KiB
C++
263 lines
7.3 KiB
C++
#include "DictionarySearchActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HardwareSerial.h>
|
|
#include <StarDict.h>
|
|
|
|
#include "DictionaryMargins.h"
|
|
#include "DictionaryResultActivity.h"
|
|
#include "MappedInputManager.h"
|
|
#include "activities/util/KeyboardEntryActivity.h"
|
|
#include "fontIds.h"
|
|
|
|
namespace {
|
|
// Dictionary path on SD card
|
|
constexpr const char* DICT_BASE_PATH = "/dictionaries/dict-data";
|
|
|
|
// Global dictionary instance (lazy initialized)
|
|
StarDict* g_dictionary = nullptr;
|
|
|
|
StarDict& getDictionary() {
|
|
if (!g_dictionary) {
|
|
g_dictionary = new StarDict(DICT_BASE_PATH);
|
|
if (!g_dictionary->begin()) {
|
|
Serial.printf("[%lu] [DICT] Failed to initialize dictionary\n", millis());
|
|
}
|
|
}
|
|
return *g_dictionary;
|
|
}
|
|
} // namespace
|
|
|
|
void DictionarySearchActivity::taskTrampoline(void* param) {
|
|
auto* self = static_cast<DictionarySearchActivity*>(param);
|
|
self->displayTaskLoop();
|
|
}
|
|
|
|
void DictionarySearchActivity::onEnter() {
|
|
ActivityWithSubactivity::onEnter();
|
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
|
isSearching = false;
|
|
keyboardShown = false;
|
|
searchStatus = "";
|
|
updateRequired = true;
|
|
|
|
xTaskCreate(&DictionarySearchActivity::taskTrampoline, "DictSearchTask",
|
|
4096, // Stack size (needs more for dictionary operations)
|
|
this, // Parameters
|
|
1, // Priority
|
|
&displayTaskHandle // Task handle
|
|
);
|
|
|
|
// If no initial word provided, show keyboard
|
|
if (searchWord.empty()) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
keyboardShown = true;
|
|
enterNewActivity(new KeyboardEntryActivity(
|
|
renderer, mappedInput, "Enter Word", "", 10,
|
|
64, // maxLength
|
|
false, // not password
|
|
[this](const std::string& word) {
|
|
// User entered a word
|
|
exitActivity();
|
|
searchWord = word;
|
|
keyboardShown = false;
|
|
if (!word.empty()) {
|
|
performSearch(word);
|
|
} else {
|
|
onBack();
|
|
}
|
|
},
|
|
[this]() {
|
|
// User cancelled keyboard
|
|
exitActivity();
|
|
keyboardShown = false;
|
|
onBack();
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
} else {
|
|
// Perform search with provided word
|
|
performSearch(searchWord);
|
|
}
|
|
}
|
|
|
|
void DictionarySearchActivity::onExit() {
|
|
ActivityWithSubactivity::onExit();
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
if (displayTaskHandle) {
|
|
vTaskDelete(displayTaskHandle);
|
|
displayTaskHandle = nullptr;
|
|
vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack
|
|
}
|
|
vSemaphoreDelete(renderingMutex);
|
|
renderingMutex = nullptr;
|
|
}
|
|
|
|
void DictionarySearchActivity::loop() {
|
|
if (subActivity) {
|
|
subActivity->loop();
|
|
return;
|
|
}
|
|
|
|
// Handle back button - use wasReleased to consume the full button event
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
onBack();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void DictionarySearchActivity::performSearch(const std::string& word) {
|
|
isSearching = true;
|
|
searchStatus = "Searching...";
|
|
updateRequired = true;
|
|
|
|
// Small delay to allow render
|
|
vTaskDelay(50 / portTICK_PERIOD_MS);
|
|
|
|
// Initialize dictionary if needed
|
|
StarDict& dict = getDictionary();
|
|
|
|
if (!dict.isReady()) {
|
|
searchStatus = "Dictionary not found";
|
|
isSearching = false;
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// Perform lookup
|
|
const auto result = dict.lookup(word);
|
|
|
|
if (result.found) {
|
|
showResult(result.word, result.definition);
|
|
} else {
|
|
showNotFound(word);
|
|
}
|
|
}
|
|
|
|
void DictionarySearchActivity::showResult(const std::string& word, const std::string& definition) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
isSearching = false;
|
|
exitActivity();
|
|
enterNewActivity(new DictionaryResultActivity(
|
|
renderer, mappedInput, word, definition,
|
|
[this]() {
|
|
// Back from result
|
|
exitActivity();
|
|
onBack();
|
|
},
|
|
[this]() {
|
|
// Search another word
|
|
exitActivity();
|
|
searchWord = "";
|
|
keyboardShown = true;
|
|
enterNewActivity(new KeyboardEntryActivity(
|
|
renderer, mappedInput, "Enter Word", "", 10, 64, false,
|
|
[this](const std::string& newWord) {
|
|
exitActivity();
|
|
keyboardShown = false;
|
|
if (!newWord.empty()) {
|
|
performSearch(newWord);
|
|
} else {
|
|
onBack();
|
|
}
|
|
},
|
|
[this]() {
|
|
exitActivity();
|
|
keyboardShown = false;
|
|
onBack();
|
|
}));
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
|
|
void DictionarySearchActivity::showNotFound(const std::string& word) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
isSearching = false;
|
|
exitActivity();
|
|
enterNewActivity(new DictionaryResultActivity(
|
|
renderer, mappedInput, word, "", // Empty definition = not found
|
|
[this]() {
|
|
// Back from result
|
|
exitActivity();
|
|
onBack();
|
|
},
|
|
[this]() {
|
|
// Search another word
|
|
exitActivity();
|
|
searchWord = "";
|
|
keyboardShown = true;
|
|
enterNewActivity(new KeyboardEntryActivity(
|
|
renderer, mappedInput, "Enter Word", "", 10, 64, false,
|
|
[this](const std::string& newWord) {
|
|
exitActivity();
|
|
keyboardShown = false;
|
|
if (!newWord.empty()) {
|
|
performSearch(newWord);
|
|
} else {
|
|
onBack();
|
|
}
|
|
},
|
|
[this]() {
|
|
exitActivity();
|
|
keyboardShown = false;
|
|
onBack();
|
|
}));
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
|
|
void DictionarySearchActivity::displayTaskLoop() {
|
|
int animationCounter = 0;
|
|
constexpr int ANIMATION_INTERVAL = 30; // ~300ms at 10ms per tick
|
|
|
|
while (true) {
|
|
// Handle animation updates when searching
|
|
if (isSearching && !subActivity) {
|
|
animationCounter++;
|
|
if (animationCounter >= ANIMATION_INTERVAL) {
|
|
animationCounter = 0;
|
|
animationFrame = (animationFrame + 1) % 3;
|
|
updateRequired = true;
|
|
}
|
|
}
|
|
|
|
if (updateRequired && !subActivity) {
|
|
updateRequired = false;
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
render();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void DictionarySearchActivity::render() const {
|
|
renderer.clearScreen();
|
|
|
|
// Get margins with button hint space for all orientations
|
|
int marginTop, marginRight, marginBottom, marginLeft;
|
|
getDictionaryContentMargins(renderer, &marginTop, &marginRight, &marginBottom, &marginLeft);
|
|
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
|
|
// Draw header
|
|
renderer.drawCenteredText(UI_12_FONT_ID, marginTop + 5, "Dictionary", true, EpdFontFamily::BOLD);
|
|
|
|
if (isSearching) {
|
|
// Show searching status with word and animated ellipsis
|
|
// Center in content area (accounting for margins)
|
|
const int centerY = marginTop + (pageHeight - marginTop - marginBottom) / 2;
|
|
|
|
// Build animated ellipsis
|
|
const char* dots = (animationFrame == 0) ? "." : (animationFrame == 1) ? ".." : "...";
|
|
|
|
// Show "Searching for 'word'..."
|
|
char statusText[128];
|
|
snprintf(statusText, sizeof(statusText), "Searching for '%s'%s", searchWord.c_str(), dots);
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, statusText);
|
|
}
|
|
|
|
renderer.displayBuffer();
|
|
}
|