diff --git a/src/apps/fmanager/fmanager.cpp b/src/apps/fmanager/fmanager.cpp index 479162f..786bba7 100644 --- a/src/apps/fmanager/fmanager.cpp +++ b/src/apps/fmanager/fmanager.cpp @@ -1,5 +1,5 @@ #include "fmanager.h" - +#include "keira/keira_macro.h" void FileManagerApp::alert(const String& title, const String& message) { lilka::Alert alert(title, message); alert.draw(canvas); @@ -105,6 +105,14 @@ FileManagerApp::FileManagerApp(const String& path) : FM_CALLBACK_CAST(onFileOpenWithMODPlayer), FM_CALLBACK_PTHIS ); + fileOpenWithMenu.addItem( + K_S_FMANAGER_TXT_VIEWER, + 0, + lilka::colors::White, + "", + FM_CALLBACK_CAST(onFileOpenWithTextViewer), + FM_CALLBACK_PTHIS + ); fileOpenWithMenu.addItem( K_S_MENU_BACK, 0, lilka::colors::White, "", FM_CALLBACK_CAST(onAnyMenuBack), FM_CALLBACK_PTHIS ); @@ -288,6 +296,10 @@ FMEntry FileManagerApp::pathToEntry(const String& path) { newEntry.type = FT_LT; newEntry.icon = FT_LT_ICON; newEntry.color = FT_LT_COLOR; + } else if (lowerCasedPath.endsWith(".txt")) { + newEntry.type = FT_TXT; + newEntry.icon = FT_TXT_ICON; + newEntry.color = FT_TXT_COLOR; } else { newEntry.type = FT_OTHER; newEntry.icon = FT_OTHER_ICON; @@ -327,6 +339,9 @@ void FileManagerApp::openCurrentEntry() { case FT_LT: K_FT_LT_HANDLER(path); break; + case FT_TXT: + K_FT_TXT_HANDLER(path); + break; case FT_DIR: FT_DEFAULT_DIR_HANDLER; break; @@ -412,7 +427,7 @@ void FileManagerApp::fileOpenWithMenuShow() { } void FileManagerApp::onFileOpenWithNESEmulator() { - FM_DBG lilka::serial.log("Enter onFileOpenWithNESEmulator"); + FM_DBG LEP; auto button = fileOpenWithMenu.getButton(); if (button == FM_EXIT_BUTTON) return; // Exit @@ -420,7 +435,7 @@ void FileManagerApp::onFileOpenWithNESEmulator() { } void FileManagerApp::onFileOpenWithMultiBootLoader() { - FM_DBG lilka::serial.log("Enter onFileOpenWithMultiBootLoader"); + FM_DBG LEP; auto button = fileOpenWithMenu.getButton(); if (button == FM_EXIT_BUTTON) return; // Exit @@ -428,7 +443,7 @@ void FileManagerApp::onFileOpenWithMultiBootLoader() { } void FileManagerApp::onFileOpenWithLua() { - FM_DBG lilka::serial.log("Enter onFileOpenWithLua"); + FM_DBG LEP; auto button = fileOpenWithMenu.getButton(); if (button == FM_EXIT_BUTTON) return; // Exit @@ -436,7 +451,7 @@ void FileManagerApp::onFileOpenWithLua() { } void FileManagerApp::onFileOpenWithMJS() { - FM_DBG lilka::serial.log("Enter onFileOpenWithMJS"); + FM_DBG LEP; auto button = fileOpenWithMenu.getButton(); if (button == FM_EXIT_BUTTON) return; // Exit @@ -444,7 +459,7 @@ void FileManagerApp::onFileOpenWithMJS() { } void FileManagerApp::onFileOpenWithLilTracker() { - FM_DBG lilka::serial.log("Enter onFileOpenWithLilTracker"); + FM_DBG LEP; auto button = fileOpenWithMenu.getButton(); if (button == FM_EXIT_BUTTON) return; // Exit @@ -452,12 +467,19 @@ void FileManagerApp::onFileOpenWithLilTracker() { } void FileManagerApp::onFileOpenWithMODPlayer() { - FM_DBG lilka::serial.log("Enter onFileOpenWithMODPlayer"); + FM_DBG LEP; auto button = fileOpenWithMenu.getButton(); if (button == FM_EXIT_BUTTON) return; // Exit K_FT_MOD_HANDLER(lilka::fileutils.joinPath(currentEntry.path, currentEntry.name)); } +void FileManagerApp::onFileOpenWithTextViewer() { + FM_DBG LEP; + auto button = fileOpenWithMenu.getButton(); + if (button == FM_EXIT_BUTTON) return; // Exit + + K_FT_TXT_HANDLER(lilka::fileutils.joinPath(currentEntry.path, currentEntry.name)); +} // FILE SELECTION MENU BELOW: void FileManagerApp::onFileSelectionOptionsMenuCopy() { @@ -528,7 +550,7 @@ void FileManagerApp::onFileSelectionOptionsMenuClearSelection() { // FILE OPTIONS MENU BELOW: void FileManagerApp::fileOptionsMenuShow() { - FM_DBG lilka::serial.log("Enter fileOptionsMenuShow"); + FM_DBG LEP; fileOptionsMenu.setCursor(0); while (!fileOptionsMenu.isFinished()) { fileOptionsMenu.update(); @@ -539,7 +561,7 @@ void FileManagerApp::fileOptionsMenuShow() { void FileManagerApp::onFileOptionsMenuOpen() { auto button = fileOptionsMenu.getButton(); - FM_DBG lilka::serial.log("Enter onFileOptionsMenuOpen"); + FM_DBG LEP; if (button == FM_OKAY_BUTTON) { if (isCurrentDirSelected()) { FM_UI_CANT_DO_OP; @@ -551,8 +573,8 @@ void FileManagerApp::onFileOptionsMenuOpen() { } void FileManagerApp::onFileOptionsMenuOpenWith() { + FM_DBG LEP; auto button = fileOptionsMenu.getButton(); - FM_DBG lilka::serial.log("Enter onFileOptionsMenuOpenWith"); if (button == FM_OKAY_BUTTON) { if (isCurrentDirSelected()) { FM_UI_CANT_DO_OP; @@ -565,8 +587,8 @@ void FileManagerApp::onFileOptionsMenuOpenWith() { } void FileManagerApp::onFileOptionsMenuMKDir() { + FM_DBG LEP; auto button = fileOptionsMenu.getButton(); - FM_DBG lilka::serial.log("Enter onFileOptionsMenuMKDir"); if (button == FM_OKAY_BUTTON) { mkdirInput.setValue(FM_DEFAULT_NEW_FOLDER_NAME); while (!mkdirInput.isFinished()) { @@ -595,8 +617,8 @@ void FileManagerApp::onFileOptionsMenuMKDir() { } void FileManagerApp::onFileOptionsMenuDelete() { + FM_DBG LEP; auto button = fileOptionsMenu.getButton(); - FM_DBG lilka::serial.log("Enter onFileOptionsMenuDelete"); if (button == FM_OKAY_BUTTON) { if (mode == FM_MODE_SELECT) { lilka::Alert checkAlert( @@ -630,8 +652,8 @@ void FileManagerApp::onFileOptionsMenuDelete() { } void FileManagerApp::onFileOptionsMenuRename() { + FM_DBG LEP; auto button = fileOptionsMenu.getButton(); - FM_DBG lilka::serial.log("Enter onFileOptionsMenuRename"); if (button == FM_OKAY_BUTTON) { if (isCurrentDirSelected()) { FM_UI_CANT_DO_OP; @@ -673,8 +695,8 @@ void FileManagerApp::onFileOptionsMenuRename() { } void FileManagerApp::onFileOptionsMenuInfo() { + FM_DBG LEP; auto button = fileOptionsMenu.getButton(); - FM_DBG lilka::serial.log("Enter onFileOptionsMenuRename"); if (button == FM_OKAY_BUTTON) { fileInfoShowAlert(); exitChildDialogs = true; @@ -791,7 +813,7 @@ void FileManagerApp::fileLoadAsRom(const String& path) { } void FileManagerApp::onAnyMenuBack() { - FM_DBG lilka::serial.log("Enter onAnyMenuBack"); + FM_DBG LEP; } void FileManagerApp::fileSelectionOptionsMenuShow() { @@ -925,9 +947,9 @@ void FileManagerApp::fileListMenuShow() { } void FileManagerApp::onFileListMenuItem() { + FM_DBG LEP; auto button = fileListMenu.getButton(); auto index = fileListMenu.getCursor(); - FM_DBG lilka::serial.log("Enter onFileListMenuItem"); if (fileListMenu.getCursor() == currentDirEntries.size()) { currentEntry = pathToEntry(lilka::fileutils.joinPath(currentPath, ".")); diff --git a/src/apps/fmanager/fmanager.h b/src/apps/fmanager/fmanager.h index ff486be..10dea63 100644 --- a/src/apps/fmanager/fmanager.h +++ b/src/apps/fmanager/fmanager.h @@ -32,6 +32,7 @@ #define FT_JS_SCRIPT_COLOR lilka::colors::Butterscotch #define FT_MOD_COLOR lilka::colors::Plum_web #define FT_LT_COLOR lilka::colors::Pink_lace +#define FT_TXT_COLOR lilka::colors::Cedar_chest #define FT_DIR_COLOR lilka::colors::Arylide_yellow #define FT_OTHER_COLOR lilka::colors::Light_gray /////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -48,6 +49,7 @@ #define FT_JS_SCRIPT_ICON &js_img #define FT_MOD_ICON &music_img #define FT_LT_ICON &music_img +#define FT_TXT_ICON &textfile_img #define FT_DIR_ICON &folder_img #define FT_OTHER_ICON &normalfile_img #define FM_SELECTED_FOLDER_ICON &selectedfolder_img @@ -108,6 +110,7 @@ #include "../icons/music.h" #include "../icons/selectedfile.h" #include "../icons/selectedfolder.h" +#include "../icons/textfile.h" // very bad test // /sd/1 => /sd/1122/1 @@ -121,7 +124,18 @@ # define FM_DBG if (0) #endif -typedef enum { FT_NONE, FT_NES_ROM, FT_BIN, FT_LUA_SCRIPT, FT_JS_SCRIPT, FT_MOD, FT_LT, FT_DIR, FT_OTHER } FileType; +typedef enum { + FT_NONE, + FT_NES_ROM, + FT_BIN, + FT_LUA_SCRIPT, + FT_JS_SCRIPT, + FT_MOD, + FT_LT, + FT_TXT, + FT_DIR, + FT_OTHER +} FileType; typedef enum { FM_MODE_VIEW, // Standard mode FM_MODE_SELECT, // if selectedEntries contain something @@ -293,6 +307,7 @@ class FileManagerApp : public App { void onFileOpenWithMJS(); void onFileOpenWithLilTracker(); void onFileOpenWithMODPlayer(); + void onFileOpenWithTextViewer(); // Callbacks [fileListMenu]: void onFileListMenuItem(); diff --git a/src/apps/icons/textfile.h b/src/apps/icons/textfile.h new file mode 100644 index 0000000..587c6ad --- /dev/null +++ b/src/apps/icons/textfile.h @@ -0,0 +1,585 @@ +#pragma once +// This is a generated file, do not edit. +// clang-format off +#include +const uint16_t textfile_img_width = 24; +const uint16_t textfile_img_height = 24; +const uint16_t textfile_img[] = { + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0xffff, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0xffff, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0xffff, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0x7bcf, + 0x7bcf, + 0xffff, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0xffff, + 0xffff, + 0xffff, + 0xffff, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x7bcf, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, + 0x0000, +}; +// clang-format on diff --git a/src/apps/icons/textfile.png b/src/apps/icons/textfile.png new file mode 100644 index 0000000..13598f9 Binary files /dev/null and b/src/apps/icons/textfile.png differ diff --git a/src/apps/txtviewer/abstractwidget.cpp b/src/apps/txtviewer/abstractwidget.cpp new file mode 100644 index 0000000..36022d9 --- /dev/null +++ b/src/apps/txtviewer/abstractwidget.cpp @@ -0,0 +1,32 @@ +#include "abstractwidget.h" + +bool AbstractWidget::isFinished() { + if (done) { + done = false; + return true; + } + return false; +} +void AbstractWidget::addActivationButton(lilka::Button activationButton) { + if (std::find(activationButtons.begin(), activationButtons.end(), activationButton) != activationButtons.end()) + return; + + activationButtons.push_back(activationButton); +} +void AbstractWidget::update() { + // we can find a caller through std, but it's heavy, so better not to + lilka::serial.log("Update method not implemented yet"); +} +void AbstractWidget::draw(Arduino_GFX* canvas) { + lilka::serial.log("Draw method not implemented yet"); +} + +void AbstractWidget::removeActivationButton(lilka::Button activationButton) { + activationButtons.erase( + std::remove(activationButtons.begin(), activationButtons.end(), activationButton), activationButtons.end() + ); +} + +lilka::Button AbstractWidget::getButton() { + return button; +} diff --git a/src/apps/txtviewer/abstractwidget.h b/src/apps/txtviewer/abstractwidget.h new file mode 100644 index 0000000..d7be882 --- /dev/null +++ b/src/apps/txtviewer/abstractwidget.h @@ -0,0 +1,20 @@ +#pragma once +#include +// I feel like finally done repeating same stuff +// TODO: to be moved in sdk +// TODO: make the docs +class AbstractWidget { +public: + bool isFinished(); + void addActivationButton(lilka::Button activationButton); + void removeActivationButton(lilka::Button activationButton); + lilka::Button getButton(); + // each widget have to provide own way to deal with that + virtual void update(); + virtual void draw(Arduino_GFX* canvas); + +protected: + std::vector activationButtons; + lilka::Button button; + bool done = false; +}; diff --git a/src/apps/txtviewer/toolbar.cpp b/src/apps/txtviewer/toolbar.cpp new file mode 100644 index 0000000..735e237 --- /dev/null +++ b/src/apps/txtviewer/toolbar.cpp @@ -0,0 +1,81 @@ +#include "toolbar.h" + +// TODO: millis() calls all around keira code use different data types to store time, potential problems expected +// right one unsigned long +void ToolBarWidget::update() { + updatePage(); +} + +void ToolBarWidget::updatePage() { + if (pages.empty()) return; +} + +void ToolBarWidget::draw(Arduino_GFX* canvas) { + if (pages.empty()) return; // nothing to draw + auto& curPage = pages[cursor]; + + unsigned long curTime = millis(); + + bool updateNeeded = (curTime - curPage.lastUpdateTime > curPage.updateInterval); + // TBR_DBG lilka::serial.log( + // "cur time %d, lastUpdateTime %d, interval %d ", curTime, curPage.lastUpdateTime, curPage.updateInterval + // ); + if (updateNeeded) { + curPage.strVal = curPage.clbkGetStr(curPage.clbkGetStrData); + curPage.lastUpdateTime = curTime; + TBR_DBG lilka::serial.log("Updating str data for page %d", cursor); + } + + canvas->setCursor(TOOLBAR_SAFE_DISTANCE, canvas->height() - 20 / 2); // FONT_Y / 2 + canvas->setFont(FONT_8x13); + canvas->setTextSize(1); + + canvas->setTextBound(TOOLBAR_SAFE_DISTANCE, canvas->height() - TOOLBAR_HEIGHT, TOOLBAR_WIDTH, TOOLBAR_HEIGHT); + canvas->setTextColor(lilka::colors::White); + + canvas->printf(curPage.strVal.c_str()); +} + +void ToolBarWidget::nextPage() { + if (pages.empty()) return; + + auto nextPageIndex = cursor + 1; + cursor = (nextPageIndex == pages.size()) ? 0 : nextPageIndex; +} + +void ToolBarWidget::prevPage() { + if (pages.empty()) return; + + auto nextPageIndex = cursor - 1; + cursor = (nextPageIndex < 0) ? pages.size() - 1 : nextPageIndex; +} + +void ToolBarWidget::forceUpdate(bool forceCurrentPage) { + if (pages.empty()) return; + + if (forceCurrentPage) { + auto& curPage = pages[cursor]; + + curPage.lastUpdateTime = 0; + } else { + for (auto& page : pages) { + page.lastUpdateTime = 0; + } + } +} + +size_t ToolBarWidget::getCursor() { + return cursor; +} + +void ToolBarWidget::addPage(PGetStrCallback clbkGetStr, void* clbkGetStrData, unsigned long updateInterval) { + ToolBarPage newPage; + + newPage.clbkGetStrData = clbkGetStrData; + newPage.clbkGetStr = clbkGetStr; + newPage.lastUpdateTime = 0; + newPage.updateInterval = updateInterval; + newPage.strVal = ""; + pages.push_back(newPage); + TBR_DBG lilka::serial.log("Toolbar: Adding a new page. New page count : %d", pages.size()); +} \ No newline at end of file diff --git a/src/apps/txtviewer/toolbar.h b/src/apps/txtviewer/toolbar.h new file mode 100644 index 0000000..074092d --- /dev/null +++ b/src/apps/txtviewer/toolbar.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include "abstractwidget.h" +// To be reused everywhere + +#define TOOLBAR_DEBUG + +#ifdef TOOLBAR_DEBUG +# define TBR_DBG if (1) +#else +# define TBR_DBG if (0) +#endif + +#define TOOLBAR_HEIGHT 30 +#define TOOLBAR_SAFE_DISTANCE 38 +#define TOOLBAR_WIDTH canvas->width() - TOOLBAR_SAFE_DISTANCE * 2 +#define TOOLBAR_UPDATE_INTERVAL 1000 + +typedef String (*PGetStrCallback)(void*); + +typedef struct { + // getStr hook + // activation buttons hook + PGetStrCallback clbkGetStr; + void* clbkGetStrData; + unsigned long lastUpdateTime; + unsigned long updateInterval; + String strVal; +} ToolBarPage; + +// we can't reuse buttons here, cause default widget would peek state, and +// we would have no access to buttons, sucks but yup + +class ToolBarWidget { +public: + + void update(); + void draw(Arduino_GFX* canvas); + // params -> callback to get str function, data to pass, update str time + void addPage(PGetStrCallback clbkGetStr, void* clbkGetStrData, unsigned long updateInterval = 1000); + void nextPage(); + void prevPage(); + void forceUpdate(bool forceCurrentPage = true); + size_t getCursor(); +private: + size_t cursor = 0; + void updatePage(); + std::vector pages; +}; diff --git a/src/apps/txtviewer/txtview.cpp b/src/apps/txtviewer/txtview.cpp new file mode 100644 index 0000000..3d21c0b --- /dev/null +++ b/src/apps/txtviewer/txtview.cpp @@ -0,0 +1,757 @@ +#include "txtview.h" +#include "keira/keira_macro.h" +#include +// ===== MEMORY +static inline FileEncoding detectEncodingByFileBlock(const char* block, size_t len) { + if (len == 0) return TXT_EMPTY; + + const unsigned char* ptr = reinterpret_cast(block); + const unsigned char* end = ptr + len; + bool hasHighBit = false; + bool isValidUtf8 = true; + bool hasNonWhitespace = false; + + while (ptr < end) { + unsigned char c = *ptr; + + // Check for null bytes (binary indicator) + if (c == 0) return TXT_BIN; + + // Track non-whitespace content + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { + hasNonWhitespace = true; + } + + // Check for other common binary indicators + if (c < 0x20 && c != '\t' && c != '\n' && c != '\r') { + return TXT_BIN; + } + + // ASCII range + if (c < 0x80) { + ptr++; + continue; + } + + hasHighBit = true; + + // UTF-8 validation + int extraBytes = 0; + if ((c & 0xE0) == 0xC0) extraBytes = 1; // 110xxxxx + else if ((c & 0xF0) == 0xE0) extraBytes = 2; // 1110xxxx + else if ((c & 0xF8) == 0xF0) extraBytes = 3; // 11110xxx + else { + isValidUtf8 = false; + break; + } + + // Check if we have enough bytes left for the sequence + if (ptr + extraBytes >= end) { + // Incomplete sequence at end - not an error, just stop checking + break; + } + + // Check continuation bytes + for (int i = 0; i < extraBytes; i++) { + ptr++; + if ((*ptr & 0xC0) != 0x80) { + isValidUtf8 = false; + break; + } + } + + if (!isValidUtf8) break; + ptr++; + } + + // If only whitespace found, treat as empty + if (!hasNonWhitespace) return TXT_EMPTY; + + if (!hasHighBit) return TXT_UTF8; // Pure ASCII is valid UTF-8 + return isValidUtf8 ? TXT_UTF8 : TXT_LEGACY; +} + +size_t shiftMemLeft(char* block, const size_t shiftAmount, const size_t contentLength, const size_t blockSize) { + if (!block || blockSize == 0 || contentLength == 0) { + return 0; + } + + // Cap toShift to contentLength + size_t toShift = (shiftAmount > contentLength) ? contentLength : shiftAmount; + + // Calculate new content length after shift + size_t newContentLength = contentLength - toShift; + + // Move memory left (memmove handles overlapping regions) + memmove(block, block + toShift, newContentLength); + + // Zero out the freed space at the end + if (newContentLength < blockSize) { + memset(block + newContentLength, 0, blockSize - newContentLength); + } + + return toShift; +} + +// Shift memory to the right (for scrolling up - adds space at start) +// Returns the actual number of bytes shifted +size_t shiftMemRight(char* block, const size_t shiftAmount, const size_t contentLength, const size_t blockSize) { + if (!block || blockSize == 0) { + return 0; + } + + // Cap toShift to available space + size_t availableSpace = blockSize - contentLength; + size_t toShift = (shiftAmount > availableSpace) ? availableSpace : shiftAmount; + + if (toShift == 0) { + return 0; + } + + // Move memory right (memmove handles overlapping regions) + memmove(block + toShift, block, contentLength); + + // Zero out the space at the beginning + memset(block, 0, toShift); + + return toShift; +} + +// ===== END MEMORY + +// ===== UNICODE + +// move to next Unicode character +static inline char* uforward(char* cstr) { + // TXT_DBG LEP; + if (!cstr || !*cstr) return cstr; + cstr++; + while ((*cstr & 0xC0) == 0x80) + cstr++; // skip continuation bytes + return cstr; +} + +// move to beginning of previous Unicode character +static inline char* ubackward(char* cstr) { + // TXT_DBG LEP; + if (!cstr) return cstr; + char* p = cstr - 1; + while ((*(reinterpret_cast(p)) & 0xC0) == 0x80) + --p; // skip continuation bytes + return p; +} +// get length of a first Unicode character +static inline size_t ulen(char* cstr) { + const char* nextchar = uforward(cstr); + return nextchar - cstr; +} + +// Get length in Unicode characters +size_t ulength(char* from, const char* to = 0) { + if (!from) return 0; + + size_t len = 0; + char* ptr = from; + + if (!to) { + while (*ptr) { + ptr = uforward(ptr); + len++; + } + } else { + if (to - from <= 0) return 0; + while (ptr < to && *ptr) { + char* next = uforward(ptr); + if (next > to) break; // don't go past 'to' + ptr = next; + len++; + } + } + + return len; +} +// ===== END UNICODE + +// ===== CANVAS +// expects bounds and cursor already set +static inline uint16_t getStringWidth(const char* pLine, Arduino_GFX* canvas) { + if (!pLine || !canvas) return 0; + + int16_t x = canvas->getCursorX(); + int16_t y = canvas->getCursorY(); + int16_t bx, by; + uint16_t w, h; + + canvas->getTextBounds(pLine, x, y, &bx, &by, &w, &h); + return w; +} + +// expects bounds and cursor already set +bool isLineWithinCanvas(const char* pLine, Arduino_GFX* canvas) { + return getStringWidth(pLine, canvas) < canvas->width() - TXT_MARGIN_LEFT * 2; +} + +long flineback(FILE* fp, char* buffer, size_t blength) { + if (!fp || !buffer || blength == 0) return 0; + // TODO: add caching to this one, store values in noffs_cache, implement in a form of macro, + // stick back to original implementation in case nothing found in cache + long initialPos = ftell(fp); + if (initialPos <= 0) return 0; + + long curPos = initialPos; + int newLineCount = 0; + + while (curPos > 0) { + // Move back by up to blength bytes + long blockStart = curPos - (long)blength; + if (blockStart < 0) blockStart = 0; + + size_t toRead = curPos - blockStart; + fseek(fp, blockStart, SEEK_SET); + size_t readBytes = fread(buffer, 1, toRead, fp); + if (readBytes == 0) break; + + // Scan backward through buffer + for (char* pC = buffer + readBytes - 1; pC >= buffer; pC--) { + if (*pC == '\n') { + newLineCount++; + if (newLineCount == 2) { + curPos = blockStart + (pC - buffer) + 1; + fseek(fp, initialPos, SEEK_SET); + return curPos; + } + } + } + + curPos = blockStart; + } + + fseek(fp, initialPos, SEEK_SET); + return 0; // reached file start +} + +static inline int16_t getLineHeight(Arduino_GFX* canvas) { + int16_t x, y; + uint16_t w, h; + canvas->getTextBounds("Ay", 0, 0, &x, &y, &w, &h); + return h; +} + +// ===== END CANVAS + +// ===== TXT VIEW +TxtView::TxtView() { + TXT_DBG LEP; +} + +// TODO: PSRAM VFS for other cases? +void TxtView::setTextFile(const String& fPath) { + TXT_DBG LEP; + // TODO: reset all used vars, caches, offsets here + if (fp) { // close old file + fclose(fp); + fp = NULL; + } + fp = fopen(fPath.c_str(), "r"); + if (fp == NULL) { + lilka::serial.err("Tried to open file %s. Errno %d", fPath.c_str(), errno); + } + fseek(fp, 0, SEEK_END); + fSize = ftell(fp); + fseek(fp, 0, SEEK_SET); + + TXT_DBG lilka::serial.log("Set file to %s", fPath.c_str()); + tBlockRefreshRequired = true; // to be done in update() +} + +void TxtView::update() { + // TXT_DBG LEP; + updateButtons(); + // We've to check if we actually can prepare data + // Cause we've no real idea about canvas dimensions + // it could be impossible to do + // We still can read file though, but for simplification we + // just wait a few cycles + if (lastCanvas && tBlockRefreshRequired) { + tBlockRefresh(); + nPtrRefresh(); + dPtrRefresh(); + } + vTaskDelay(pdMS_TO_TICKS(10)); +} + +void TxtView::updateButtons() { + // TXT_DBG LEP; + auto state = lilka::controller.getState(); + + /* + TODO: to be restored after large single line blocks optimizations + BY LINE/BY PAGE SCROLL + if (state.up.justPressed) scrollUp(); + else if (state.down.justPressed) scrollDown(); + else if (state.left.justPressed) scrollPageUp(); + else if (state.right.justPressed) scrollPageDown(); + */ + + // BY Page only scroll + if (state.up.justPressed) scrollPageUp(); + else if (state.down.justPressed) scrollPageDown(); + else if (state.b.justPressed) done = true; + + for (lilka::Button activationButton : activationButtons) { + const lilka::_StateButtons& buttonsArray = *reinterpret_cast(&state); + if (buttonsArray[activationButton].justPressed) { + button = activationButton; + done = true; + // Should be made after done flag setup to allow to clear it by isFinished() call + if (clbk) clbk(clbkData); + } + } + vTaskDelay(LILKA_UI_UPDATE_DELAY_MS / portTICK_PERIOD_MS); +} + +void TxtView::draw(Arduino_GFX* canvas) { + // TXT_DBG LEP; + lastCanvas = canvas; + + // Doesn't look like we've something to display + // could be a first run, so we have no data prepared + if (!fp) return; + + // Setup canvas options + setCanvasOptions(canvas); + + // Determine line height + int16_t lineHeight = getLineHeight(canvas); + int16_t y = lineHeight; // counter + + // TODO: determine good font/size for this + // Check if encoding supported + // TODO: fix first few frames problem, displays this stuff when nothing loaded yet + if (encoding == TXT_EMPTY) { + auto w = getStringWidth(TXT_S_EMPTY, canvas); + canvas->setCursor((canvas->width() - w) / 2, (canvas->height() / 2) - lineHeight); + canvas->print(TXT_S_EMPTY); + return; + } else if (encoding != TXT_UTF8) { + auto w = getStringWidth(TXT_S_ENC_UNKNOWN, canvas); + canvas->setCursor((canvas->width() - w) / 2, (canvas->height() / 2) - lineHeight); + canvas->print(TXT_S_ENC_UNKNOWN); + return; + } + + // display dptrs + int availableHeight = canvas->height() - TXT_MARGIN_BOTTOM; + + // Calculate the number of lines that can be displayed + maxLines = availableHeight / (lineHeight + spacing); + + // Adjust if there is leftover space for a partial line + if (availableHeight % (lineHeight + spacing) >= lineHeight) { + maxLines++; + } + + size_t countDisplayedLines = 0; + size_t countDisplayedBytes = 0; + + for (size_t i = 0; i < dptrs.size(); i++) { + char* lineStart = dptrs[i]; + char* lineEnd = (i + 1 == dptrs.size()) ? tBlock + tLen : dptrs[i + 1]; // last line check + + char backup = *lineEnd; + *lineEnd = '\0'; + + canvas->setCursor(TXT_MARGIN_LEFT, y); + // TODO: add ANSI colors support here, would be fun + canvas->print(lineStart); + // TXT_DBG lilka::serial.log("drawing %s, Length %d, ULength %d", lineStart, strlen(lineStart), ulength(lineStart)); + *lineEnd = backup; + + y += lineHeight + spacing; + countDisplayedLines++; + countDisplayedBytes += (lineEnd - lineStart); + if (y > canvas->height() - TXT_MARGIN_BOTTOM) break; + } + lastDisplayedLines = countDisplayedLines; + lastDisplayedBytes = countDisplayedBytes; + + // sleep +} + +void TxtView::tBlockRefresh() { + // TXT_DBG LEP; + if (fp == NULL) { + TXT_DBG lilka::serial.log("File not open. Skiping"); + return; // nothing to refresh. Wait till next update + } + + long toReadFrom = ftell(fp); + + // check if we read it already + bool alreadyRead = tOffset == toReadFrom; + if (alreadyRead) { + tBlockRefreshRequired = false; + return; + } + + // should be read anyways in this scenario + bool initalOffsetOrCantBeReadLazy = (tOffset == -1) || (labs(toReadFrom - tOffset) >= TXT_MAX_BLOCK_SIZE / 2); + if (initalOffsetOrCantBeReadLazy) { + // Zero mem + memset(tBlock, 0, TXT_MAX_BLOCK_SIZE); + tLen = fread(tBlock, 1, TXT_MAX_BLOCK_SIZE, fp); + } else { // do the lazy read + long offsetSignedDelta = toReadFrom - tOffset; + if (offsetSignedDelta > 0) { + // Moving forward: shift buffer left, refill at end + size_t shifted = shiftMemLeft(tBlock, offsetSignedDelta, tLen, TXT_MAX_BLOCK_SIZE); + + // After shift, we have (tLen - shifted) bytes of old content + // We need to read new data starting from the end of remaining content + size_t remainingInBuffer = tLen - shifted; // This is safe, shifted <= tLen + size_t spaceAvailable = TXT_MAX_BLOCK_SIZE - remainingInBuffer; + + // File position to read from: we've advanced by 'shifted' bytes + // Old buffer started at tOffset, now it starts at (tOffset + shifted) + // We need to read starting from (tOffset + tLen) which = toReadFrom + remainingInBuffer + long filePos = tOffset + tLen; // or: toReadFrom + remainingInBuffer + fseek(fp, filePos, SEEK_SET); + + // Read into end of buffer + size_t actuallyRead = fread(tBlock + remainingInBuffer, 1, spaceAvailable, fp); + tLen = remainingInBuffer + actuallyRead; + + } else if (offsetSignedDelta < 0) { + size_t shiftAmount = (size_t)(-offsetSignedDelta); + + // Clamp shift amount first + size_t actualShift = (shiftAmount > TXT_MAX_BLOCK_SIZE) ? TXT_MAX_BLOCK_SIZE : shiftAmount; + + // Clamp content to fit after shift + size_t clampedContentLength = (actualShift >= TXT_MAX_BLOCK_SIZE) ? 0 : (TXT_MAX_BLOCK_SIZE - actualShift); + + if (clampedContentLength > tLen) { + clampedContentLength = tLen; + } + + size_t shifted = shiftMemRight(tBlock, actualShift, clampedContentLength, TXT_MAX_BLOCK_SIZE); + + fseek(fp, toReadFrom, SEEK_SET); + size_t actuallyRead = fread(tBlock, 1, shifted, fp); + + tLen = actuallyRead + clampedContentLength; + } + } + + fseek(fp, toReadFrom, SEEK_SET); // save original file offset, could be useful for refresh + tBlockRefreshRequired = false; + tOffset = toReadFrom; // save current position + + // Detect enoding + if (encoding == TXT_UNKNOWN) { + encoding = detectEncodingByFileBlock(tBlock, tLen); + } + // TXT_DBG lilka::serial.log("Read %d bytes from %ld Position", tLen, curPos); + // TXT_DBG lilka::serial.log("tBlock full content:\n%.*s", tLen, tBlock); + // TXT_DBG lilka::serial.log("Encoding %d", encoding); +} + +void TxtView::nPtrRefresh(long maxoffset) { + // TXT_DBG LEP; + nptrs.clear(); + if (!lastCanvas) { + TXT_DBG lilka::serial.err("No access to lastCanvas, can't calc dptrs"); + tBlockRefreshRequired = true; + return; + } + if (tBlockRefreshRequired) { + TXT_DBG lilka::serial.err("Txt block refresh requied, skiping..."); + return; + } + char* pCurrentChar = tBlock; + const char* pEndBlock = tBlock + tLen; + nptrs.push_back(pCurrentChar); + // do not check last character here + for (; pCurrentChar < pEndBlock - 1; pCurrentChar++) { + if (*pCurrentChar == '\n') { + if (maxoffset == -1 || maxoffset <= TADDR2OFF(pCurrentChar + 1)) nptrs.push_back(pCurrentChar + 1); + // TXT_DBG lilka::serial.log("Adding dptr at %p\n", nptrs.back()); + } + } + // TXT_DBG lilka::serial.log("Added %d nptrs \n", nptrs.size()); +} +void TxtView::dPtrRefresh(long maxoffset) { + // TXT_DBG LEP; + + if (!lastCanvas) { + TXT_DBG lilka::serial.err("No access to lastCanvas, can't calc dptrs"); + tBlockRefreshRequired = true; + return; + } + if (tBlockRefreshRequired) { + TXT_DBG lilka::serial.err("Txt block refresh requied, skiping..."); + return; + } + + // readjust canvas options if something changed + setCanvasOptions(lastCanvas); + + dptrs.clear(); + + for (auto dptr : nptrs) { + char* pLineStart = dptr; + + // add a bit of caching here + // mostly lines would fit + - 1 character, except + // really funky fonts, so maybe cache a bit and shift + // or maybe leave it this way to make it universal enough + + // skip if dptr points past the block + if (pLineStart >= tBlock + tLen) continue; + + char* pLineEnd = uforward(pLineStart); // first letter + if (pLineEnd > tBlock + tLen) pLineEnd = tBlock + tLen; // clamp + + while (pLineEnd < tBlock + tLen) { + char backupChar = *pLineEnd; + *pLineEnd = '\0'; + + // reached newline + if (*(pLineEnd - 1) == '\n') { + if (maxoffset == -1 || maxoffset <= TADDR2OFF(pLineStart)) dptrs.push_back(pLineStart); + // TXT_DBG lilka::serial.log("Adding dptr at %p\n", dptrs.back()); + *pLineEnd = backupChar; + break; + } + + // check if line fits + if (!isLineWithinCanvas(pLineStart, lastCanvas)) { + if (maxoffset == -1 || maxoffset <= TADDR2OFF(pLineStart)) dptrs.push_back(pLineStart); + // TXT_DBG lilka::serial.log("Adding dptr at %p\n", dptrs.back()); + *pLineEnd = backupChar; + + // move backward safely + char* prev = ubackward(pLineEnd); + if (prev == pLineEnd || prev < tBlock) break; // avoid infinite loop + pLineEnd = prev; + + pLineStart = pLineEnd; + continue; + } + + *pLineEnd = backupChar; + + // advance safely + char* next = uforward(pLineEnd); + if (next <= pLineEnd || next > tBlock + tLen) break; + pLineEnd = next; + } + // Add the final line + if (pLineStart < tBlock + tLen) { + if (dptrs.empty() || dptrs.back() != pLineStart) { + if (maxoffset == -1 || maxoffset <= TADDR2OFF(pLineStart)) { + dptrs.push_back(pLineStart); + } + } + } + // TXT_DBG { + // if (!dptrs.empty()) { + // lilka::serial.log("Last dptr: %s", dptrs[dptrs.size() - 1]); + // } + // lilka::serial.log("pLineStart: %p (%s)", pLineStart, pLineStart); + // lilka::serial.log("pLineEnd: %p (%s)", pLineEnd, pLineEnd); + // lilka::serial.log("tBlock: %p", tBlock); + // lilka::serial.log("tBlock+tLen: %p", tBlock + tLen); + // lilka::serial.log("tLen: %zu", tLen); + // } + // TXT_DBG for (auto dptr:dptrs){ // AM HERE + // lilka::serial.log("dptr addr %x", TADDR2OFF(dptr)); + // } + } +} + +void TxtView::scrollUp(size_t linesToScroll) { + TXT_DBG LEP; + if (!fp || !lastCanvas) return; + + long anchorOffset = ftell(fp); + // Can't go back + if (anchorOffset == 0) return; + + TxtScrollDirection direction = TXT_BACKWARD; + + size_t skipLeft = linesToScroll; + + long currBlockOffset; + + while (1) { + // go back a line + if (direction == TXT_BACKWARD) { + currBlockOffset = flineback(fp, tBuffer, TXT_BUFFER_SIZE); + + } else // carefully seek for an anchor cause it's lost(extremely long line found) + { + if (!dptrs.empty()) currBlockOffset = TADDR2OFF(dptrs[1]); + else { + TXT_DBG lilka::serial.err("oh fuck.. dptrs empty"); + } + } + + // reload block + fseek(fp, currBlockOffset, SEEK_SET); + tBlockRefresh(); + + // refresh constraints + nPtrRefresh(); + dPtrRefresh(); + + // not actually needed but let's say, why not check + if (dptrs.empty()) { + TXT_DBG lilka::serial.err("No dptrs found? Impossible"); + return; + } + + // seek the anchor + bool anchorFound = false; + size_t anchorOffsetIndex = 0; + for (const auto dptr : dptrs) { + TXT_DBG lilka::serial.log("dptr off %x, anchor %x", TADDR2OFF(dptr), anchorOffset); + if (TADDR2OFF(dptr) >= anchorOffset) { + anchorFound = true; + break; + } + anchorOffsetIndex++; + } + + // captain, there's no anchor, we've to do find it or we gonna + // swim in an ocean for all ethernity + if (!anchorFound) { + direction = TXT_FORWARD; // too much backward + TXT_DBG lilka::serial.log("Anchor not found"); + continue; // skip the fuck + } + + TXT_DBG lilka::serial.log( + "Anchor [ index %d, offset %x ] Skip left [ %d ]", anchorOffsetIndex, anchorOffset, skipLeft + ); + + // our ship lays on a piece of land, there's no water around + if (anchorOffsetIndex == 0) { + TXT_DBG lilka::serial.log("what"); + break; + } + + if (skipLeft > anchorOffsetIndex) { // we're close, but not yet + TXT_DBG lilka::serial.log("wee, go next iteration"); + skipLeft = skipLeft - anchorOffsetIndex; + anchorOffset = TADDR2OFF(dptrs[0]); + fseek(fp, anchorOffset, SEEK_SET); + direction = TXT_BACKWARD; + } else { // yeh we did it :D + TXT_DBG lilka::serial.log("all good, we know where to jump"); + anchorOffset = TADDR2OFF(dptrs[anchorOffsetIndex - skipLeft]); + fseek(fp, anchorOffset, SEEK_SET); + break; + } + } + + tBlockRefreshRequired = true; +} + +void TxtView::scrollDown() { + TXT_DBG LEP; + // lock scrolling in case we've nothing new to display + if (!fp || dptrs.empty() || lastDisplayedLines < maxLines) return; + + if (dptrs.size() > 1) { + fseek(fp, TADDR2OFF(dptrs[1]), SEEK_SET); + } else { + fseek(fp, 0, SEEK_SET); // go begining + } + tBlockRefreshRequired = true; // to be done in update() +} + +void TxtView::scrollPageUp() { + TXT_DBG LEP; + if (!fp || !lastCanvas) return; + long maxoffset = ftell(fp); // stick to current file position + if (maxoffset == 0) return; + TXT_DBG lilka::serial.log("Max lines = %d", maxLines); + scrollUp(maxLines); +} + +void TxtView::scrollPageDown() { + TXT_DBG LEP; + TXT_DBG lilka::serial.log("displayed lines = %d max lines = %d", lastDisplayedLines, maxLines); + if (!fp || dptrs.empty() || lastDisplayedLines < maxLines) return; + + // TODO: determine end of file reached + // NOTE: test on empty file + fseek(fp, TADDR2OFF(dptrs[lastDisplayedLines]), SEEK_SET); + tBlockRefreshRequired = true; // to be done in update() +} + +TxtView::~TxtView() { + TXT_DBG LEP; + // Never forget to close file + if (fp) fclose(fp); +} + +void TxtView::setColor(uint16_t color) { + this->color = color; +} + +void TxtView::setBackgroundColor(uint16_t bgColor) { + this->bgColor = bgColor; +}; + +void TxtView::setFont(const uint8_t* font) { + this->font = font; + tBlockRefreshRequired = true; // to be done in update() +} + +void TxtView::setSpacing(uint16_t spacing) { + this->spacing = spacing; + tBlockRefreshRequired = true; // to be done in update() +} + +void TxtView::setTextSize(uint8_t textSize) { + this->textSize = textSize; + tBlockRefreshRequired = true; // to be done in update() +} + +void TxtView::jumpToOffset(long offset) { + // TODO: jumpToOffset() implementation + // jump in the middle, recalc nearest dptr, jump here, refresh block +} + +long TxtView::getFileSize() { + return fSize; +} + +long TxtView::getOffset() { + return fp ? ftell(fp) : 0; +} + +size_t TxtView::getCountDisplayedBytes(){ + return lastDisplayedBytes; +} + +void TxtView::setCallback(PTXTViewCallback clbk, void* clbkData) { + this->clbk = clbk; + this->clbkData = clbkData; +} + +void TxtView::setCanvasOptions(Arduino_GFX* canvas) { + canvas->setTextBound( + TXT_MARGIN_LEFT, 0, canvas->width() - 2 * TXT_MARGIN_LEFT, canvas->height() - TXT_MARGIN_BOTTOM + ); + canvas->setCursor(TXT_MARGIN_LEFT, 0); + canvas->fillScreen(bgColor); + canvas->setTextSize(textSize); + canvas->setTextColor(color); + canvas->setTextWrap(false); + canvas->setFont(font); +} + +// ===== END TXT VIEW \ No newline at end of file diff --git a/src/apps/txtviewer/txtview.h b/src/apps/txtviewer/txtview.h new file mode 100644 index 0000000..4fd0020 --- /dev/null +++ b/src/apps/txtviewer/txtview.h @@ -0,0 +1,103 @@ +#pragma once +#include "abstractwidget.h" + +#define TXT_MAX_BLOCK_SIZE 2048 +#define TXT_BUFFER_SIZE 512 +// To be moved in lilka sdk + +// + +#ifdef TXT_VIEWER_DEBUG +# define TXT_DBG if (1) +#else +# define TXT_DBG if (0) +#endif + +#define TXT_MARGIN_LEFT 28 // as well as right :D +#define TXT_MARGIN_BOTTOM 25 // margin from the bottom, actually maybe name it toolbar + +// tBlockAddrToOffset +#define TADDR2OFF(X) X - tBlock + ftell(fp) + +typedef enum { TXT_FORWARD, TXT_BACKWARD } TxtScrollDirection; + +// To be moved in K_S_STRINGS, but cause expected movage to sdk, 've no idea +#define TXT_S_EMPTY "== EMPTY ==" +#define TXT_S_ENC_UNKNOWN "== UNKNOWN ENCODING ==" + +typedef enum { TXT_UNKNOWN, TXT_EMPTY, TXT_UTF8, TXT_BIN, TXT_LEGACY } FileEncoding; +typedef void (*PTXTViewCallback)(void*); +class TxtView : public AbstractWidget { +public: + TxtView(); + void setTextFile(const String& fPath); + void update() override; + void draw(Arduino_GFX* canvas) override; + + // Display options + void setColor(uint16_t color); + void setBackgroundColor(uint16_t bgColor); + void setFont(const uint8_t* font); + void setSpacing(uint16_t spacing); // distance between lines in pixels + void setTextSize(uint8_t textSize); // actually is scale, but in GFX is named this way + + // Misc options + void jumpToOffset(long offset); // jump to nearest offset + long getFileSize(); + long getOffset(); + size_t getCountDisplayedBytes(); + + // Callback setup. To be triggered on any activation button + void setCallback(PTXTViewCallback clbk, void* clbkData); + + ~TxtView(); + +private: + // Positioning and file read + void tBlockRefresh(); // read text block, prepare noffs and doffs + void nPtrRefresh(long maxoffset = -1); + void dPtrRefresh(long maxoffset = -1); + + // Scrolling + void scrollUp(size_t linesToScroll = 1); + void scrollDown(); + void scrollPageUp(); + void scrollPageDown(); + + // Input + void updateButtons(); + // Canvas setup + void setCanvasOptions(Arduino_GFX* canvas); + + // File stuff + FILE* fp = NULL; + long tOffset = -1; // to be used ONLY in tBlockRefresh! + long fSize = 0; + + // Text data + char tBlock[TXT_MAX_BLOCK_SIZE] = {}; + char tBuffer[TXT_BUFFER_SIZE] = {}; // to be used by flineback() + size_t tLen = 0; // text block length + bool tBlockRefreshRequired = true; // inital value + size_t lastDisplayedLines = 0; + size_t lastDisplayedBytes = 0; + size_t maxLines = 0; + std::vector nptrs; // ptrs to \n separated lines withing tBlock + std::vector dptrs; // ptrs to displayed lines withing tBlock + FileEncoding encoding = TXT_UNKNOWN; + + // UI options + uint16_t color = lilka::colors::White; + uint16_t bgColor = lilka::colors::Black; + const uint8_t* font = FONT_8x13; + uint16_t spacing = 1; + uint8_t textSize = 1; + + // we need somehow calculate how much lines/characters fit display + // so we store last canvas pointer to be acessible in dOffsRefresh + Arduino_GFX* lastCanvas = NULL; + + // Callback + PTXTViewCallback clbk = nullptr; + void* clbkData = nullptr; +}; diff --git a/src/apps/txtviewer/txtviewer.cpp b/src/apps/txtviewer/txtviewer.cpp new file mode 100644 index 0000000..e716fdd --- /dev/null +++ b/src/apps/txtviewer/txtviewer.cpp @@ -0,0 +1,166 @@ +#include "txtviewer.h" + +// ===== CONFIGURATION +TxtViewerApp::TxtViewerApp(String fPath) : App("Text Viewer") { + setStackSize(8192); + fontSetSize(fontSize); + + // Configure TxtViewer + tView.setCallback(TXTV_CALLBACK_CAST(onTxtViewerButton), TXTV_CALLBACK_PTHIS); + + // Configure Toolbar + // Note: should be in order specified in TxtToolBarPage enum + toolBar.addPage(TXTV_TOOLBAR_CALLBACK(onGetStrProgress), TXTV_CALLBACK_PTHIS); + toolBar.addPage(TXTV_TOOLBAR_CALLBACK(onGetStrFontSize), TXTV_CALLBACK_PTHIS); + toolBar.addPage(TXTV_TOOLBAR_CALLBACK(onGetStrFontSpacing), TXTV_CALLBACK_PTHIS); + + tView.setTextFile(fPath); + tView.addActivationButton(lilka::Button::D); + tView.addActivationButton(lilka::Button::A); + tView.addActivationButton(lilka::Button::C); +} +// ===== END CONFIGURATION + +void TxtViewerApp::onTxtViewerButton() { + auto aButton = tView.getButton(); + if (aButton == K_BTN_BACK) return; + + if (aButton == lilka::Button::C) toolBar.nextPage(); + + // Inc + if (aButton == lilka::Button::A) { + auto cur = toolBar.getCursor(); + switch (cur) { + case TXTV_PROGRESS: + + break; + case TXTV_FONT_SIZE: + fontSizeInc(); + break; + case TXTV_FONT_SPACING: + fontSpacingInc(); + break; + } + } + // Dec + if (aButton == lilka::Button::D) { + auto cur = toolBar.getCursor(); + switch (cur) { + case TXTV_PROGRESS: + + break; + case TXTV_FONT_SIZE: + fontSizeDec(); + break; + case TXTV_FONT_SPACING: + fontSpacingDec(); + break; + } + } + + tView.isFinished(); // clear finished flag +} + +// ===== REDRAW +void TxtViewerApp::run() { + while (!tView.isFinished()) { + tView.update(); + tView.draw(canvas); + queueDraw(); + } +} + +void TxtViewerApp::queueDraw() { + // show control buttons + toolBar.update(); + toolBar.draw(canvas); + App::queueDraw(); +} +// ===== END REDRAW + +// ===== FONT SETTINGS + +void TxtViewerApp::fontSetSize(int8_t newSize) { + auto scale = (newSize / TXTV_FONT_COUNT) + 1; + auto fontIndex = newSize % TXTV_FONT_COUNT; + + tView.setFont(fonts[fontIndex]); + tView.setTextSize(scale); + toolBar.forceUpdate(); +} + +void TxtViewerApp::fontSetSpacing(uint8_t newSpacing) { + if (newSpacing > TXTV_MAX_SPACING) return; + + tView.setSpacing(newSpacing); + toolBar.forceUpdate(); +} + +void TxtViewerApp::fontSizeInc() { + if (fontSize >= TXTV_FONT_MAX_SIZE) return; + + fontSize++; + + fontSetSize(fontSize); +} + +void TxtViewerApp::fontSizeDec() { + if (fontSize <= 0) return; + + fontSize--; + + fontSetSize(fontSize); +} + +void TxtViewerApp::fontSpacingInc() { + if (fontSpacing >= TXTV_MAX_SPACING) return; + + fontSpacing++; + + fontSetSpacing(fontSpacing); +} + +void TxtViewerApp::fontSpacingDec() { + if (fontSpacing == 0) return; + + fontSpacing--; + + fontSetSpacing(fontSpacing); +} + +// ===== END FONT SETTINGS + +// ===== TOOLBAR + +// char * maybe? and static buffer, should be cool :) +String TxtViewerApp::onGetStrProgress() { + // [=========== ] 80% + auto max = tView.getFileSize(); + auto curPos = tView.getOffset()+tView.getCountDisplayedBytes(); + + // TODO: detemine TXTV_PROGRESS_LEN from toolbar width in characters + size_t countSegments = curPos * TXTV_PROGRESS_LEN / max; + String progStr = "["; + + for (size_t i = 0; i < countSegments; i++) + progStr += "="; + + size_t countSpaces = TXTV_PROGRESS_LEN - countSegments; + for (size_t i = 0; i < countSpaces; i++) + progStr += " "; + + progStr += "]"; + + String offStr = lilka::fileutils.getHumanFriendlySize(curPos, true) + "/" + lilka::fileutils.getHumanFriendlySize(max, true); + progStr += offStr; + + return progStr; +} + +String TxtViewerApp::onGetStrFontSize() { + return StringFormat("Font SIZE %d C(-) A(+)", fontSize); +} +String TxtViewerApp::onGetStrFontSpacing() { + return StringFormat("Font SPACE %d C(-) A(+)", fontSpacing); +} +// ===== END TOOLBAR \ No newline at end of file diff --git a/src/apps/txtviewer/txtviewer.h b/src/apps/txtviewer/txtviewer.h new file mode 100644 index 0000000..29dea70 --- /dev/null +++ b/src/apps/txtviewer/txtviewer.h @@ -0,0 +1,58 @@ +#pragma once +#include "keira/app.h" +#include "keira/keira.h" +#include "txtview.h" +#include "toolbar.h" + +#define TXTV_CALLBACK_CAST(X) reinterpret_cast(&TxtViewerApp::X) +#define TXTV_CALLBACK_PTHIS reinterpret_cast(this) // K_CLBK_PTHIS? +#define TXTV_TOOLBAR_CALLBACK(X) reinterpret_cast(&TxtViewerApp::X) +#define TXTV_PROGRESS_LEN 10 +#define TXTV_FONT_COUNT 5 +#define TXTV_FONT_MAX_SIZE 15 +#define TXTV_MAX_SPACING 10 // px + + + +typedef enum{ + TXTV_PROGRESS, + TXTV_FONT_SIZE, + TXTV_FONT_SPACING +} TxtToolBarPage; + +class TxtViewerApp : public App { +public: + explicit TxtViewerApp(String fPath); + TxtView tView; + ToolBarWidget toolBar; + void queueDraw(); + +private: + TxtViewerApp(); + void run() override; + + // font settings routines + + void fontSetSize(int8_t newSize); + void fontSetSpacing(uint8_t newSpacing); + + void fontSizeInc(); + void fontSizeDec(); + void fontSpacingInc(); + void fontSpacingDec(); + + // tView callbacks: + void onTxtViewerButton(); + + // toolbar callbacks: + String onGetStrProgress(); + String onGetStrFontSize(); + String onGetStrFontSpacing(); + + + // scale = (font size / font index) + // font index should be a list of fonts + int8_t fontSize = 1; + uint8_t fontSpacing = 2; + const uint8_t* fonts[TXTV_FONT_COUNT] = {FONT_5x8, FONT_6x12, FONT_7x13, FONT_8x13, FONT_9x15}; +}; diff --git a/src/keira/keira.h b/src/keira/keira.h index b49e75e..f7cbe1b 100644 --- a/src/keira/keira.h +++ b/src/keira/keira.h @@ -6,6 +6,7 @@ /////////////////////////////////////////////////////////////////////////////////////////////////////// #include "keira_lang.h" +#include "keira_macro.h" // BUTTONS: ////////////////////////////////////////////////////////////////////////////////////////// #define K_BTN_BACK lilka::Button::B @@ -28,6 +29,7 @@ #include "apps/lua/luarunner.h" #include "apps/mjs/mjsrunner.h" #include "apps/nes/nesapp.h" +#include "apps/txtviewer/txtviewer.h" /////////////////////////////////////////////////////////////////////////////////////////////////////// // FILETYPE HANDLERS: //////////////////////////////////////////////////////////////////////////////// #define K_FT_NES_HANDLER(X) AppManager::getInstance()->runApp(new NesApp(X)) @@ -35,6 +37,7 @@ #define K_FT_JS_SCRIPT_HANDLER(X) AppManager::getInstance()->runApp(new MJSApp(X)) #define K_FT_MOD_HANDLER(X) AppManager::getInstance()->runApp(new ModPlayerApp(X)) #define K_FT_LT_HANDLER(X) AppManager::getInstance()->runApp(new LilTrackerApp(X)) +#define K_FT_TXT_HANDLER(X) AppManager::getInstance()->runApp(new TxtViewerApp(X)) /////////////////////////////////////////////////////////////////////////////////////////////////////// // GUIDELINE: Use Keira global filetype handlers(K_FT_) if possible /////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/keira/keira_macro.h b/src/keira/keira_macro.h new file mode 100644 index 0000000..0816159 --- /dev/null +++ b/src/keira/keira_macro.h @@ -0,0 +1,69 @@ +#pragma once +/////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Keira OS Header file +// +/////////////////////////////////////////////////////////////////////////////////////////////////////// + +// +// A Place to store all wonderful macro magic for further reuse +// and tips of it's usage +// + +//============================================= DEBUG ============================================= + +// So far Keira OS supports various features to provide a bit easier debugging for custom apps. +// Most of those come in a form of togglable macro, which you can use together with +// PLATFORMIO_BUILD_FLAGS env variable, to set those options before Keira OS buildage + +// example ============================================================================================ +// PLATFORMIO_BUILD_FLAGS='-DFMANAGER_DEBUG' pio run -t upload +// would define macro FMANAGER_DEBUG and build a firmware with it +//===================================================================================================== + +// Same thing could be also set directly in platformio.ini recipe file using build_flags option + +// Watchdog service + +// KEIRA_WATCHDOG allow u to run Watchdog serice to output reports about stack/memory usage of all +// services/apps +// WATCHDOG_UPDATE_TIME controls amount of time between reports(in milliseconds) + +// Custom app autorun +// KEIRA_DEBUG_APP and KEIRA_DEBUG_APP_PARAMS allow you to control app to be run first, before +// default launcher + +// example ============================================================================================ +// PLATFORMIO_BUILD_FLAGS='-DKEIRA_DEBUG_APP=FileManagerApp -DKEIRA_DEBUG_APP_PARAMS=\"/sd\"' +// would allow u to autorun FileManagerApp and pass to it path to /sd folder +//===================================================================================================== + +// Exit/Entry point loggage, add it to begin/end of method/function to get a comprehensive log +// about calls +#define LEP lilka::serial.log("==> %s:%d %s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__); +#define LEXP lilka::serial.log("<== %s:%d %s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__); + +// +// Note: each app/context have to provide own way to make it togglable +// and never to be used in production build +// + +// example ============================================================================================ + +// #ifdef TXT_VIEWER_DEBUG +// # define TXT_DBG if (1) +// #else +// # define TXT_DBG if (0) +// #endif + +// Usage: +// +// void TxtView::setFont(const uint8_t* font) { +// TXT_DBG LEP; +// this->font = font; +// } + +// Usage it everywhere isn't guaranteed, simply cause some functions may be run too often +//===================================================================================================== + +//=========================================== END DEBUG =========================================== \ No newline at end of file diff --git a/src/keira/localizations/lang_en.h b/src/keira/localizations/lang_en.h index b8ea1cf..1f2f6f6 100644 --- a/src/keira/localizations/lang_en.h +++ b/src/keira/localizations/lang_en.h @@ -179,6 +179,7 @@ #define K_S_FMANAGER_MJS "mJS" #define K_S_FMANAGER_LILTRACKER K_S_LAUNCHER_LILTRACKER #define K_S_FMANAGER_MOD_PLAYER "MOD Player" +#define K_S_FMANAGER_TXT_VIEWER "Text Viewer" #define K_S_FMANAGER_ACTIONS_ON_SELECTED "Actions on selected" #define K_S_FMANAGER_COPY_SELECTED "Copy selected" #define K_S_FMANAGER_MOVE_SELECTED "Move selected" diff --git a/src/keira/localizations/lang_uk.h b/src/keira/localizations/lang_uk.h index d101346..2c6af9c 100644 --- a/src/keira/localizations/lang_uk.h +++ b/src/keira/localizations/lang_uk.h @@ -178,6 +178,7 @@ #define K_S_FMANAGER_MJS "mJS" #define K_S_FMANAGER_LILTRACKER K_S_LAUNCHER_LILTRACKER #define K_S_FMANAGER_MOD_PLAYER "Програвач MOD" +#define K_S_FMANAGER_TXT_VIEWER "Текстовий переглядач" #define K_S_FMANAGER_ACTIONS_ON_SELECTED "Дії над вибраним" #define K_S_FMANAGER_COPY_SELECTED "Копіювати вибране" #define K_S_FMANAGER_MOVE_SELECTED "Перемістити вибране"