diff --git a/.github/workflows/compile-arduino.yml b/.github/workflows/compile-arduino.yml index c3400bd8..e5d573c2 100644 --- a/.github/workflows/compile-arduino.yml +++ b/.github/workflows/compile-arduino.yml @@ -1,5 +1,5 @@ name: Compile Examples -on: [pull_request_target] +on: [pull_request_target, pull_request] jobs: compile: diff --git a/docs/classes.puml b/docs/classes.puml new file mode 100644 index 00000000..45334625 --- /dev/null +++ b/docs/classes.puml @@ -0,0 +1,49 @@ +@startuml +' https://www.plantuml.com/plantuml/uml/ + +hide empty members + +class LcdMenu { + +virtual bool handle(char c) + #virtual bool up() + #virtual bool down() + #virtual bool enter() + #virtual bool back() +} + +class MenuItem { + +virtual bool handle(char c) +} + +class ItemCommand { + +bool handle(char c) override + #bool enter() override +} + +class ItemList { + +bool handle(char c) override + #bool up() override + #bool down() override + #bool enter() override + #bool back() override +} + +class ItemInput { + +bool handle(char c) override + #bool up() override + #bool down() override + #bool enter() override + #bool back() override + #bool left() override + #bool right() override + #bool backspace() override + #bool typeChar(char c) override + #bool clear() override +} + +LcdMenu::handle -r-> MenuItem::handle +ItemCommand -u-|> MenuItem +ItemList -u-|> MenuItem +ItemInput -u-|> MenuItem + +@enduml \ No newline at end of file diff --git a/examples/KeyboardAdapter/KeyboardAdapter.ino b/examples/KeyboardAdapter/KeyboardAdapter.ino new file mode 100644 index 00000000..0bd03571 --- /dev/null +++ b/examples/KeyboardAdapter/KeyboardAdapter.ino @@ -0,0 +1,45 @@ +/* + * Usage example of `KeyboardAdapter`. + */ +#define DEBUG + +#include +#include +#include +#include + +#define LCD_ROWS 2 +#define LCD_COLS 16 + +// Create your charset +const char* charset = "0123456789"; + +// Declare the call back function +void inputCallback(char* value); + +MAIN_MENU( + ITEM_INPUT_CHARSET("Con", "0123456", charset, inputCallback), + ITEM_BASIC("Connect to WiFi"), + ITEM_BASIC("Blink SOS"), + ITEM_BASIC("Blink random")); + +LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); +LcdMenu menu(lcdAdapter); +KeyboardAdapter keyboard(&menu, &Serial); + +void setup() { + Serial.begin(9600); + menu.initialize(mainMenu); +} + +void loop() { + keyboard.observe(); +} +/** + * Define callback + */ +void inputCallback(char* value) { + // Do stuff with value + Serial.print(F("# ")); + Serial.println(value); +} diff --git a/examples/SimpleInput/SimpleInput.ino b/examples/SimpleInput/SimpleInput.ino index 87f02ad4..7b771ea1 100644 --- a/examples/SimpleInput/SimpleInput.ino +++ b/examples/SimpleInput/SimpleInput.ino @@ -47,7 +47,7 @@ void loop() { char command = Serial.read(); if (!processWithSimpleCommand(&navConfig, command) && command != '\n') { - menu.type(command); + menu.process(command); } } /** @@ -56,4 +56,4 @@ void loop() { void inputCallback(char* value) { Serial.print(F("# ")); Serial.println(value); -} \ No newline at end of file +} diff --git a/src/ItemCommand.h b/src/ItemCommand.h index c7d28ef4..ea178fd4 100644 --- a/src/ItemCommand.h +++ b/src/ItemCommand.h @@ -48,10 +48,19 @@ class ItemCommand : public MenuItem { */ void setCallBack(fptr callback) { this->callback = callback; }; - void enter() override { + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + default: return false; + } + } + + protected: + bool enter() { if (callback != NULL) { callback(); } + return true; }; }; diff --git a/src/ItemInput.h b/src/ItemInput.h index dc480ce9..0eb06e7d 100644 --- a/src/ItemInput.h +++ b/src/ItemInput.h @@ -18,9 +18,7 @@ * @brief Item that allows user to input string information. * * ┌────────────────────────────┐ - * │ . . . │ * │ > T E X T : V A L U E │ - * │ . . . │ * └────────────────────────────┘ * * Additionally to `text` this item has string `value`. @@ -141,13 +139,34 @@ class ItemInput : public MenuItem { */ fptrStr getCallbackStr() { return callback; } - void up() override {} + bool process(const unsigned char c) override { + if (isprint(c)) { + return typeChar(c); + } + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return display->getEditModeEnabled(); + case DOWN: return display->getEditModeEnabled(); + case LEFT: return left(); + case RIGHT: return right(); + case BACKSPACE: return backspace(); + case CLEAR: return clear(); + default: return false; + } + } - void down() override {} + void draw(uint8_t row) override { + uint8_t maxCols = display->getMaxCols(); + static char* buf = new char[maxCols]; + substring(value, view, viewSize, buf); + display->drawItem(row, text, ':', buf); + } - void enter() override { + protected: + bool enter() { if (display->getEditModeEnabled()) { - return; + return false; } // Move cursor to the latest index uint8_t length = strlen(value); @@ -161,11 +180,11 @@ class ItemInput : public MenuItem { display->setEditModeEnabled(true); display->resetBlinker(constrainBlinkerPosition(strlen(text) + 2 + cursor - view)); display->drawBlinker(); + return true; }; - - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } display->clearBlinker(); display->setEditModeEnabled(false); @@ -176,14 +195,14 @@ class ItemInput : public MenuItem { if (callback != NULL) { callback(value); } + return true; }; - - void left() override { + bool left() { if (!display->getEditModeEnabled()) { - return; + return false; } if (cursor == 0) { - return; + return true; } cursor--; if (cursor < view) { @@ -193,14 +212,14 @@ class ItemInput : public MenuItem { display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() - 1)); // Log printCmd(F("LEFT"), value[display->getBlinkerPosition() - (strlen(text) + 2)]); + return true; }; - - void right() override { + bool right() { if (!display->getEditModeEnabled()) { - return; + return false; } if (cursor == strlen(value)) { - return; + return true; } cursor++; if (cursor > (view + viewSize - 1)) { @@ -210,15 +229,8 @@ class ItemInput : public MenuItem { display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() + 1)); // Log printCmd(F("RIGHT"), value[display->getBlinkerPosition() - (strlen(text) + 2)]); + return true; }; - - void draw(uint8_t row) override { - uint8_t maxCols = display->getMaxCols(); - static char* buf = new char[maxCols]; - substring(value, view, viewSize, buf); - display->drawItem(row, text, ':', buf); - } - /** * Execute a "backspace cmd" on menu * @@ -226,12 +238,12 @@ class ItemInput : public MenuItem { * * Removes the character at the current cursor position. */ - void backspace() override { + bool backspace() { if (!display->getEditModeEnabled()) { - return; + return false; } if (strlen(value) == 0 || cursor == 0) { - return; + return true; } remove(value, cursor - 1, 1); printCmd(F("BACKSPACE"), value); @@ -241,16 +253,16 @@ class ItemInput : public MenuItem { } MenuItem::draw(); display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() - 1)); + return true; } - /** * Display text at the cursor position * used for `Input` type menu items * @param character character to append */ - void typeChar(const char character) override { + bool typeChar(const char character) { if (!display->getEditModeEnabled()) { - return; + return false; } uint8_t length = strlen(value); if (cursor < length) { @@ -274,14 +286,14 @@ class ItemInput : public MenuItem { display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() + 1)); // Log printCmd(F("TYPE-CHAR"), character); + return true; } - /** * Clear the value of the input field */ - void clear() override { + bool clear() { if (!display->getEditModeEnabled()) { - return; + return false; } // // set the value @@ -294,6 +306,7 @@ class ItemInput : public MenuItem { // MenuItem::draw(); display->resetBlinker(constrainBlinkerPosition(strlen(text) + 2)); + return true; } }; diff --git a/src/ItemInputCharset.h b/src/ItemInputCharset.h index e423ed9a..ae9dd116 100644 --- a/src/ItemInputCharset.h +++ b/src/ItemInputCharset.h @@ -1,9 +1,8 @@ #ifndef ItemInputCharset_H #define ItemInputCharset_H -#include - #include "ItemInput.h" +#include class ItemInputCharset : public ItemInput { private: @@ -48,13 +47,27 @@ class ItemInputCharset : public ItemInput { ItemInputCharset(const char* text, const char* charset, fptrStr callback) : ItemInputCharset(text, (char*)"", charset, callback) {} - void enter() override { + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return up(); + case DOWN: return down(); + case LEFT: return left(); + case RIGHT: return right(); + case BACKSPACE: return ItemInput::backspace(); + case CLEAR: return ItemInput::clear(); + default: return false; + } + } + + protected: + bool enter() { if (!display->getEditModeEnabled()) { - ItemInput::enter(); - return; + return ItemInput::enter(); } if (!charEditMode) { - return; + return true; } uint8_t length = strlen(value); if (cursor < length) { @@ -67,70 +80,62 @@ class ItemInputCharset : public ItemInput { printCmd(F("CHARSET"), value); stopCharsetEditMode(); ItemInput::right(); + return true; } - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } if (!charEditMode) { - ItemInput::back(); - return; + return ItemInput::back(); } stopCharsetEditMode(); + return true; }; - void left() override { + bool left() { if (!display->getEditModeEnabled()) { - return; + return false; } if (charEditMode) { stopCharsetEditMode(); } - ItemInput::left(); + return ItemInput::left(); } - void right() override { + bool right() { if (!display->getEditModeEnabled()) { - return; + return false; } if (charEditMode) { stopCharsetEditMode(); } - ItemInput::right(); + return ItemInput::right(); } - void down() override { + bool down() { if (!display->getEditModeEnabled()) { - return; + return false; } if (!charEditMode) { initCharsetEditMode(); } charsetPosition = (charsetPosition + 1) % charsetSize; display->drawChar(charset[charsetPosition]); + return true; } - void up() override { + bool up() { if (!display->getEditModeEnabled()) { - return; + return false; } if (!charEditMode) { initCharsetEditMode(); } charsetPosition = constrain(charsetPosition - 1, 0, charsetSize); display->drawChar(charset[charsetPosition]); - } - - void clear() override { - if (!display->getEditModeEnabled()) { - return; - } - ItemInput::clear(); - } - - void typeChar(const char character) override { - // Do nothing + return true; } }; diff --git a/src/ItemList.h b/src/ItemList.h index 3188e874..f02037f0 100644 --- a/src/ItemList.h +++ b/src/ItemList.h @@ -85,26 +85,46 @@ class ItemList : public MenuItem { */ String* getItems() { return items; } - void enter() override { + void draw(uint8_t row) override { + uint8_t maxCols = display->getMaxCols(); + static char* buf = new char[maxCols]; + substring(items[itemIndex].c_str(), 0, maxCols - strlen(text) - 2, buf); + display->drawItem(row, text, ':', buf); + } + + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return up(); + case DOWN: return down(); + default: return false; + } + } + + protected: + bool enter() { if (display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(true); + return true; }; - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(false); if (callback != NULL) { callback(itemIndex); } + return true; }; - void down() override { + bool down() { if (!display->getEditModeEnabled()) { - return; + return false; } uint8_t previousIndex = itemIndex; itemIndex = constrain(itemIndex - 1, 0, (uint16_t)(itemCount)-1); @@ -112,11 +132,12 @@ class ItemList : public MenuItem { printCmd(F("LEFT"), items[itemIndex].c_str()); MenuItem::draw(); } + return true; }; - void up() override { + bool up() { if (!display->getEditModeEnabled()) { - return; + return false; } uint8_t previousIndex = itemIndex; itemIndex = constrain((itemIndex + 1) % itemCount, 0, (uint16_t)(itemCount)-1); @@ -124,14 +145,8 @@ class ItemList : public MenuItem { printCmd(F("RIGHT"), items[itemIndex].c_str()); MenuItem::draw(); } + return true; }; - - void draw(uint8_t row) override { - uint8_t maxCols = display->getMaxCols(); - static char* buf = new char[maxCols]; - substring(items[itemIndex].c_str(), 0, maxCols - strlen(text) - 2, buf); - display->drawItem(row, text, ':', buf); - } }; #define ITEM_STRING_LIST(...) (new ItemList(__VA_ARGS__)) diff --git a/src/ItemProgress.h b/src/ItemProgress.h index e73f6dbb..f101320f 100644 --- a/src/ItemProgress.h +++ b/src/ItemProgress.h @@ -103,26 +103,46 @@ class ItemProgress : public MenuItem { return mapping(progress); } - void enter() override { + void draw(uint8_t row) override { + uint8_t maxCols = display->getMaxCols(); + static char* buf = new char[maxCols]; + substring(getValue(), 0, maxCols - strlen(text) - 2, buf); + display->drawItem(row, text, ':', buf); + } + + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return up(); + case DOWN: return down(); + default: return false; + } + } + + protected: + bool enter() { if (display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(true); + return true; }; - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(false); if (callback != NULL) { callback(progress); } + return true; }; - void down() override { + bool down() { if (!display->getEditModeEnabled()) { - return; + return false; } uint16_t oldProgress = progress; decrement(); @@ -130,11 +150,12 @@ class ItemProgress : public MenuItem { printCmd(F("LEFT"), getValue()); MenuItem::draw(); } + return true; }; - void up() override { + bool up() { if (!display->getEditModeEnabled()) { - return; + return false; } uint16_t oldProgress = progress; increment(); @@ -142,14 +163,8 @@ class ItemProgress : public MenuItem { printCmd(F("RIGHT"), getValue()); MenuItem::draw(); } + return true; }; - - void draw(uint8_t row) override { - uint8_t maxCols = display->getMaxCols(); - static char* buf = new char[maxCols]; - substring(getValue(), 0, maxCols - strlen(text) - 2, buf); - display->drawItem(row, text, ':', buf); - } }; #define ITEM_PROGRESS(...) (new ItemProgress(__VA_ARGS__)) diff --git a/src/ItemToggle.h b/src/ItemToggle.h index 221ef63d..2d3aba55 100644 --- a/src/ItemToggle.h +++ b/src/ItemToggle.h @@ -16,9 +16,7 @@ * @brief Item that allows user to toggle between ON/OFF states. * * ┌────────────────────────────┐ - * │ . . . │ * │ > T E X T : O F F │ - * │ . . . │ * └────────────────────────────┘ * * Additionally to `text` this item has ON/OFF `enabled` state. @@ -78,20 +76,29 @@ class ItemToggle : public MenuItem { const char* getTextOff() { return this->textOff; } - void enter() override { - enabled = !enabled; - if (callback != NULL) { - callback(enabled); - } - MenuItem::draw(); - }; - void draw(uint8_t row) override { uint8_t maxCols = display->getMaxCols(); static char* buf = new char[maxCols]; substring(enabled ? textOn : textOff, 0, maxCols - strlen(text) - 2, buf); display->drawItem(row, text, ':', buf); }; + + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + default: return false; + } + }; + + protected: + bool enter() { + enabled = !enabled; + if (callback != NULL) { + callback(enabled); + } + MenuItem::draw(); + return true; + }; }; #define ITEM_TOGGLE(...) (new ItemToggle(__VA_ARGS__)) diff --git a/src/LcdMenu.h b/src/LcdMenu.h index e195b4b4..74a9e6d7 100644 --- a/src/LcdMenu.h +++ b/src/LcdMenu.h @@ -26,6 +26,7 @@ #pragma once #include "interface/DisplayInterface.h" +#include "utils/constants.h" #include #include @@ -83,6 +84,36 @@ class LcdMenu { lcd.drawCursor(); // In case if currentPosition was not changed between screens } + /* + * Draw the cursor + */ + void updateOnlyCursor() { + if (!enableUpdate) { + return; + } + lcd.moveCursor(constrain(cursorPosition - top, 0, maxRows - 1)); + } + + void drawMenu() { + for (uint8_t i = top; i <= bottom; i++) { + MenuItem* item = currentMenuTable[i]; + if (currentMenuTable[i]->getType() == MENU_ITEM_END_OF_MENU) { + return; + } + item->draw(i - top); + } + if (isAtTheStart(top)) { + lcd.clearUpIndicator(); + } else { + lcd.drawUpIndicator(); + } + if (isAtTheEnd(bottom)) { + lcd.clearDownIndicator(); + } else { + lcd.drawDownIndicator(); + } + } + public: /** * ## Public Fields @@ -95,7 +126,6 @@ class LcdMenu { /** * # Constructor */ - LcdMenu(DisplayInterface& display) : lcd(display) { bottom = lcd.getMaxRows(); maxRows = lcd.getMaxRows(); @@ -111,6 +141,7 @@ class LcdMenu { drawMenu(); lcd.drawCursor(); } + /* * Draw the menu items and cursor */ @@ -121,62 +152,111 @@ class LcdMenu { drawMenu(); lcd.drawCursor(); } - /* - * Draw the menu items and cursor - */ - void updateOnlyCursor() { - if (!enableUpdate) { - return; + + bool process(const unsigned char c) { + if (currentMenuTable[cursorPosition]->process(c)) { + return true; } - lcd.moveCursor(constrain(cursorPosition - top, 0, maxRows - 1)); - } + switch (c) { + case UP: return up(); + case DOWN: return down(); + case ENTER: return enter(); + case BACK: return back(); + default: return false; + } + }; + /** + * When you want to display any other content on the screen then + * call this function then display your content, later call + * `show()` to show the menu + */ + void hide() { + enableUpdate = false; + lcd.clear(); + } + /** + * Show the menu + */ + void show() { + enableUpdate = true; + update(); + } + /** + * @brief Checks if the given position is at the start. + * @param position The position to check. + * @return true if the position is at the start (i.e., equal to 1), + * false otherwise. + */ inline bool isAtTheStart(uint8_t position) { return position == 1; } - + /** + * @brief Checks if the specified position is at the end of the menu. + * @param position The index of the item to check. + * @return true if the next item is the end of the menu; false otherwise. + */ inline bool isAtTheEnd(uint8_t position) { return currentMenuTable[position + 1]->getType() == MENU_ITEM_END_OF_MENU; } + /** + * Get the current cursor position + * @return `cursorPosition` e.g. 1, 2, 3... + */ + uint8_t getCursorPosition() { return this->cursorPosition; } + /** + * Set the current cursor position + * @param position + */ + void setCursorPosition(uint8_t position) { + uint8_t bottom = position; + bool isNotEnd = false; - void drawMenu() { - for (uint8_t i = top; i <= bottom; i++) { - MenuItem* item = currentMenuTable[i]; - if (currentMenuTable[i]->getType() == MENU_ITEM_END_OF_MENU) { - return; - } - item->draw(i - top); - } - if (isAtTheStart(top)) { - lcd.clearUpIndicator(); - } else { - lcd.drawUpIndicator(); - } - if (isAtTheEnd(bottom)) { - lcd.clearDownIndicator(); - } else { - lcd.drawDownIndicator(); - } - } + do { + isNotEnd = currentMenuTable[++bottom]->getType() != MENU_ITEM_END_OF_MENU && bottom < maxRows + 20; + } while (isNotEnd); + uint8_t max = maxRows - 1; + + this->cursorPosition = position + 1; + this->bottom = constrain(cursorPosition + max, max, bottom - 1); + this->top = this->bottom - max; + } /** - * Reset the display + * Check if currently displayed menu is a sub menu. */ - void resetMenu() { this->reset(false); } + bool isSubMenu() { + byte menuItemType = currentMenuTable[0]->getType(); + return menuItemType == MENU_ITEM_SUB_MENU_HEADER; + } + /** + * Get a `MenuItem` at position + * @return `MenuItem` - item at `position` + */ + MenuItem* getItemAt(uint8_t position) { return currentMenuTable[position]; } + /** + * Get a `MenuItem` at position using operator function + * e.g `menu[menu.getCursorPosition()]` will return the item at the + * current cursor position NB: This is relative positioning (i.e. if a + * submenu is currently being displayed, menu[1] will return item 1 in + * the current menu) + * @return `MenuItem` - item at `position` + */ + MenuItem* operator[](const uint8_t position) { + return currentMenuTable[position]; + } + + protected: /** * Execute an "up press" on menu * When edit mode is enabled, this action is skipped */ - void up() { - if (lcd.getEditModeEnabled()) { - currentMenuTable[cursorPosition]->up(); - return; - } + bool up() { // // determine if cursor ia at start of menu items // if (isAtTheStart(cursorPosition)) { - return; + return false; } cursorPosition--; // Log @@ -194,21 +274,18 @@ class LcdMenu { } else { updateOnlyCursor(); } + return true; } /** * Execute a "down press" on menu * When edit mode is enabled, this action is skipped */ - void down() { - if (lcd.getEditModeEnabled()) { - currentMenuTable[cursorPosition]->down(); - return; - } + bool down() { // // determine if cursor has passed the end // if (isAtTheEnd(cursorPosition)) { - return; + return false; } cursorPosition++; // Log @@ -226,6 +303,7 @@ class LcdMenu { } else { updateOnlyCursor(); } + return true; } /** * Execute an "enter" action on menu. @@ -236,7 +314,7 @@ class LcdMenu { * - Execute a callback action. * - Toggle the state of an item. */ - void enter() { + bool enter() { MenuItem* item = currentMenuTable[cursorPosition]; // Log printCmd(F("ENTER"), item->getType()); @@ -252,137 +330,24 @@ class LcdMenu { // display the sub menu // reset(false); - return; } - item->enter(); + return true; } /** * Execute a "backpress" action on menu. * * Navigates up once. */ - void back() { + bool back() { // Log printCmd(F("BACK")); // - // Back action different when on ItemInput - // - if (lcd.getEditModeEnabled()) { - currentMenuTable[cursorPosition]->back(); - return; - } - // // check if this is a sub menu, if so go back to its parent // if (isSubMenu()) { currentMenuTable = currentMenuTable[0]->getSubMenu(); reset(true); } - } - /** - * Execute a "left press" on menu - * - * *NB: Works only for `ItemInput` and `ItemList` types* - * - * Moves the cursor one step to the left. - */ - void left() { - currentMenuTable[cursorPosition]->left(); - } - /** - * Execute a "right press" on menu - * - * *NB: Works only for `ItemInput` and `ItemList` types* - * - * Moves the cursor one step to the right. - */ - void right() { - currentMenuTable[cursorPosition]->right(); - } - /** - * Execute a "backspace cmd" on menu - * - * *NB: Works only for `ItemInput` type* - * - * Removes the character at the current cursor position. - */ - void backspace() { - currentMenuTable[cursorPosition]->backspace(); - } - /** - * Display text at the cursor position - * used for `Input` type menu items - * @param character character to append - */ - void type(const char character) { - currentMenuTable[cursorPosition]->typeChar(character); - } - /** - * Clear the value of the input field - */ - void clear() { - currentMenuTable[cursorPosition]->clear(); - } - /** - * When you want to display any other content on the screen then - * call this function then display your content, later call - * `show()` to show the menu - */ - void hide() { - enableUpdate = false; - lcd.clear(); - } - /** - * Show the menu - */ - void show() { - enableUpdate = true; - update(); - } - /** - * Get the current cursor position - * @return `cursorPosition` e.g. 1, 2, 3... - */ - uint8_t getCursorPosition() { return this->cursorPosition; } - /** - * Set the current cursor position - * @param position - */ - void setCursorPosition(uint8_t position) { - uint8_t bottom = position; - bool isNotEnd = false; - - do { - isNotEnd = currentMenuTable[++bottom]->getType() != MENU_ITEM_END_OF_MENU && bottom < maxRows + 20; - } while (isNotEnd); - - uint8_t max = maxRows - 1; - - this->cursorPosition = position + 1; - this->bottom = constrain(cursorPosition + max, max, bottom - 1); - this->top = this->bottom - max; - } - /** - * Check if currently displayed menu is a sub menu. - */ - bool isSubMenu() { - byte menuItemType = currentMenuTable[0]->getType(); - return menuItemType == MENU_ITEM_SUB_MENU_HEADER; - } - /** - * Get a `MenuItem` at position - * @return `MenuItem` - item at `position` - */ - MenuItem* getItemAt(uint8_t position) { return currentMenuTable[position]; } - /** - * Get a `MenuItem` at position using operator function - * e.g `menu[menu.getCursorPosition()]` will return the item at the - * current cursor position NB: This is relative positioning (i.e. if a - * submenu is currently being displayed, menu[1] will return item 1 in - * the current menu) - * @return `MenuItem` - item at `position` - */ - MenuItem* operator[](const uint8_t position) { - return currentMenuTable[position]; + return true; } }; diff --git a/src/MenuItem.h b/src/MenuItem.h index 73097c26..096361d0 100644 --- a/src/MenuItem.h +++ b/src/MenuItem.h @@ -32,7 +32,12 @@ #include /** - * The MenuItem class + * @brief The MenuItem class. + * + * ┌────────────────────────────┐ + * │ > T E X T │ + * └────────────────────────────┘ + * */ class MenuItem { protected: @@ -75,16 +80,17 @@ class MenuItem { * @param text text to display for the item */ void setText(const char* text) { this->text = text; }; - - virtual void up() {} - virtual void down() {} - virtual void enter() {} - virtual void back() {} - virtual void left() {} - virtual void right() {} - virtual void backspace() {} - virtual void typeChar(const char character) {} - virtual void clear() {} + /** + * @brief Process a command decoded in 1 byte. + * It can be a printable character or a control command like `ENTER` or `LEFT`. + * Return value is used to determine operation was successful or ignored. + * If the parent of item received that handle was ignored it can execute it's own action on this command. + * Thus, the item always has priority in processing; if it is ignored, it is delegated to the parent element. + * Behaviour is very similar to Even Bubbling in JavaScript. + * @param c the character, can be a printable character or a control command + * @return true if command was successfully handled by item. + */ + virtual bool process(const unsigned char c) { return false; }; virtual void draw() { draw(display->getCursorRow()); diff --git a/src/input/InputInterface.h b/src/input/InputInterface.h new file mode 100644 index 00000000..0d7f94d7 --- /dev/null +++ b/src/input/InputInterface.h @@ -0,0 +1,19 @@ +#pragma once + +#include "LcdMenu.h" + +/** + * @brief Input device for menu. + * Main goal is to hide implementation of input device(s) and convert from their events + * into 1 byte command to `LcdMenu`. + */ +class InputInterface { + protected: + LcdMenu* menu = NULL; + + public: + InputInterface(LcdMenu* menu) + : menu(menu) { + } + virtual void observe() = 0; +}; diff --git a/src/input/KeyboardAdapter.h b/src/input/KeyboardAdapter.h new file mode 100644 index 00000000..b86844ea --- /dev/null +++ b/src/input/KeyboardAdapter.h @@ -0,0 +1,257 @@ +#pragma once + +#include "InputInterface.h" +#include "Stream.h" + +#define BS 8 // Backspace, \b +#define LF 10 // Line feed, \n +#define CR 13 // Carriage Return, \r +#define ESC 27 // Escape +#define DEL 127 // Del, Backspace on Mac +#define C2_CSI_TERMINAL_MIN 0x40 +#define C2_CSI_TERMINAL_MAX 0x7E +#define THRESHOLD 100 +#define CSI_BUFFER_SIZE 5 + +/** + * @brief Input interface to keyboard. + * Uses default buttons layout: + * - `Arrows` for `LEFT/RIGHT/UP/DOWN`; + * - `Enter` for `ENTER`; + * - `Escape` for `BACK`; + * - `Delete` for `CLEAR`; + * - `Backspace` for `BACKSPACE`; + * + * Keyboard can send multiple-bytes commands. + * Implementation should convert it to one byte command. + * + * Implementation details. Mapping: + * `First 128 of ASCII` -> as is + * `\r` -> `ENTER` + * `\n` -> `ENTER` + * `\r\n` -> `ENTER` + * `ESC` -> `BACK` + * `ESC [ A` (up arrow) -> `UP` + * `ESC [ B` (down arrow) -> `DOWN` + * `ESC [ C` (right arrow) -> `RIGHT` + * `ESC [ D` (left arrow) -> `LEFT` + * `ESC [ 3 ~` (Delete button) -> `CLEAR` + */ +class KeyboardAdapter : public InputInterface { + private: + /** + * @see https://en.wikipedia.org/wiki/ANSI_escape_code + * @see https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 + */ + enum class CodeSet { + /** + * @brief Main ASCII code set. + * Initially defined as part of ASCII, the default C0 control code set is now defined in ISO 6429 (ECMA-48). + */ + C0, + /** + * @brief Fe Escape sequences. + * Starts with ESC. + * Terminates with [0x40, 0x5F]. + */ + C1, + /** + * @brief Control Sequence Introducer. + * Starts with ESC [. + * Terminates with [0x40, 0x7E]. + */ + C2_CSI, + /** + * @brief Device Control String. + * Starts with ESC P. + * Terminates with ESC \. + */ + C2_DCS, + /** + * @brief Operating System Command. + * Starts with ESC ]. + * Terminates with ESC \. + * Currently not implemented. + */ + C2_OSC, + /** + * @brief Expect string terminator. + * Currently not implemented. + */ + C3, + }; + /** + * @brief Input stream. + */ + Stream* stream = NULL; + /** + * @brief Internal state of current code set. + * As stream receives bytes asynchronously, multiple bytes command can arrive + * in several calls. Need to store current state between calls. + */ + CodeSet codeSet = CodeSet::C0; + /** + * @brief Last received character. + * Used to detect 2 chars sequences, e.g. `\r\n`. + */ + unsigned char lastChar; + /** + * @brief Milliseconds timestamp of last received character. + * Used for detecting ESC with no chars next or single `\r` without `\n`. + */ + unsigned long lastCharTimestamp; + /** + * @brief Buffer to store already read values of "intermediate bytes". + * Multiple bytes command for CSI is in form of `ESC [ `. + */ + char csiBuffer[CSI_BUFFER_SIZE]; + /** + * @brief Points to next available byte in `csiBuffer`. + */ + uint8_t csiBufferCursor = 0; + /** + * @brief Reset to initial state. + */ + inline void reset() { + codeSet = CodeSet::C0; + csiBufferCursor = 0; + lastChar = 0; + lastCharTimestamp = 0; + } + inline bool hasLastChar() { + // TODO: Check millis() overflow situation + return lastChar != 0 && millis() > lastCharTimestamp + THRESHOLD; + } + inline void saveLastChar(unsigned char command) { + lastChar = command; + lastCharTimestamp = millis(); + } + /** + * @brief Handle idle state when there are no input for some time. + * @see THRESHOLD - timeout in ms. + */ + void handleIdle() { + switch (lastChar) { + case CR: // Received single `\r` + printCmd(F("Call ENTER from idle")); + menu->process(ENTER); + break; + case ESC: // Received single `ESC` + printCmd(F("Call BACK from idle")); + menu->process(BACK); + break; + } + } + /** + * @brief Handle received command. + * @param command the received command + */ + void handleReceived(unsigned char command) { + switch (codeSet) { + case CodeSet::C0: + switch (command) { + case BS: // 8. On Win + case DEL: // 127. On Mac + printCmd(F("Call BACKSPACE")); + menu->process(BACKSPACE); + break; + case LF: // 10, \n + printCmd(F("Call ENTER")); + menu->process(ENTER); + break; + case CR: // 13, \r + // Can be \r\n sequence, do nothing + break; + case ESC: // 27 + codeSet = CodeSet::C1; + break; + default: + printCmd(F("Call default"), (uint8_t)command); + menu->process(command); + break; + } + saveLastChar(command); + break; + case CodeSet::C1: + switch (command) { + case '[': + codeSet = CodeSet::C2_CSI; + break; + default: + reset(); // Reset after unsupported C1 command + break; + } + break; + case CodeSet::C2_CSI: + if (command >= C2_CSI_TERMINAL_MIN && command <= C2_CSI_TERMINAL_MAX) { + switch (command) { + case 'A': + printCmd(F("Call UP")); + menu->process(UP); + break; + case 'B': + printCmd(F("Call DOWN")); + menu->process(DOWN); + break; + case 'C': + printCmd(F("Call RIGHT")); + menu->process(RIGHT); + break; + case 'D': + printCmd(F("Call LEFT")); + menu->process(LEFT); + break; + case 'F': + printCmd(F("End")); + break; + case 'H': + printCmd(F("Home")); + break; + case '~': + if (csiBufferCursor > 0) { + switch (csiBuffer[0] - '0') { + case 2: // Insert + printCmd(F("Insert")); + break; + case 3: // Delete + printCmd(F("Call CLEAR")); + menu->process(CLEAR); + break; + case 5: // PgUp + printCmd(F("PgUp")); + break; + case 6: // PgDn + printCmd(F("PgDn")); + break; + } + } + } + reset(); // Reset after C2 terminal symbol + } else { + csiBuffer[csiBufferCursor] = command; + csiBufferCursor++; + } + break; + default: + reset(); // Reset after unknown code set + break; + } + }; + + public: + KeyboardAdapter(LcdMenu* menu, Stream* stream) + : InputInterface(menu), stream(stream) { + } + void observe() override { + if (!stream->available()) { + if (hasLastChar()) { + handleIdle(); + reset(); + } + return; + } + unsigned char command = stream->read(); + printCmd(F("INPUT"), (uint8_t)command); + handleReceived(command); + } +}; diff --git a/src/utils/RotaryNavConfig.h b/src/utils/RotaryNavConfig.h index 6ef6d5d6..4d01aa9b 100644 --- a/src/utils/RotaryNavConfig.h +++ b/src/utils/RotaryNavConfig.h @@ -31,9 +31,9 @@ void processWithRotaryEncoder(RotaryNavConfig* config) { // Handle rotary encoder rotation uint8_t rotation = config->encoder->rotate(); if (rotation == 1) { - config->menu->down(); // Call DOWN action + config->menu->process(DOWN); // Call DOWN action } else if (rotation == 2) { - config->menu->up(); // Call UP action + config->menu->process(UP); // Call UP action } // Handle button press (short, long, and double press) @@ -44,7 +44,7 @@ void processWithRotaryEncoder(RotaryNavConfig* config) { if (config->pendingEnter) { if (config->doublePressThreshold > 0 && currentTime - config->lastPressTime < config->doublePressThreshold) { - config->menu->backspace(); // Call BACKSPACE action (double press) + config->menu->process(BACKSPACE); // Call BACKSPACE action (double press) config->pendingEnter = false; } } else { @@ -52,14 +52,14 @@ void processWithRotaryEncoder(RotaryNavConfig* config) { config->lastPressTime = currentTime; } } else if (pressType == 2) { - config->menu->back(); // Call BACK action (long press) + config->menu->process(BACK); // Call BACK action (long press) config->pendingEnter = false; } // Check if the doublePressThreshold has elapsed for pending enter action if ((!config->menu->lcd.getEditModeEnabled() && config->pendingEnter) || (config->pendingEnter && (currentTime - config->lastPressTime >= config->doublePressThreshold))) { - config->menu->enter(); // Call ENTER action (short press) + config->menu->process(ENTER); // Call ENTER action (short press) config->pendingEnter = false; } } \ No newline at end of file diff --git a/src/utils/SimpleNavConfig.h b/src/utils/SimpleNavConfig.h index c971f7e6..00e769a2 100644 --- a/src/utils/SimpleNavConfig.h +++ b/src/utils/SimpleNavConfig.h @@ -18,24 +18,25 @@ struct SimpleNavConfig { bool processWithSimpleCommand(SimpleNavConfig* config, byte cmd) { if (config->up && cmd == config->up) { - config->menu->up(); + config->menu->process(UP); } else if (config->down && cmd == config->down) { - config->menu->down(); + config->menu->process(DOWN); } else if (config->left && cmd == config->left) { - config->menu->left(); + config->menu->process(LEFT); } else if (config->right && cmd == config->right) { - config->menu->right(); + config->menu->process(RIGHT); } else if (config->enter && cmd == config->enter) { - config->menu->enter(); + config->menu->process(ENTER); } else if (config->back && cmd == config->back) { - config->menu->back(); + config->menu->process(BACK); #ifdef ItemInput_H } else if (config->clear && cmd == config->clear) { - config->menu->clear(); + config->menu->process(CLEAR); } else if (config->backspace && cmd == config->backspace) { - config->menu->backspace(); + config->menu->process(BACKSPACE); #endif } else { + config->menu->process(cmd); return false; } return true; diff --git a/src/utils/constants.h b/src/utils/constants.h index 3a28cded..8c4a867b 100644 --- a/src/utils/constants.h +++ b/src/utils/constants.h @@ -19,6 +19,17 @@ const byte MENU_ITEM_END_OF_MENU = 8; const byte MENU_ITEM_LIST = 9; const byte MENU_ITEM_PROGRESS = 10; // +// Control codes +// +#define BACKSPACE 8 // Backspace +#define ENTER 10 // Enter +#define BACK 27 // Escape +#define UP 128 // >127 +#define DOWN 129 // >127 +#define RIGHT 130 // >127 +#define LEFT 131 // >127 +#define CLEAR 132 // >127 +// const byte DOWN_ARROW[8] = { 0b00100, // * 0b00100, // *