diff --git a/docs/source/overview/items/input-charset.rst b/docs/source/overview/items/input-charset.rst index 119dd58a..8bdc8ac0 100644 --- a/docs/source/overview/items/input-charset.rst +++ b/docs/source/overview/items/input-charset.rst @@ -29,10 +29,14 @@ The charset input item can be created using the following syntax: When the ``Pass`` menu item is selected, an input field will be displayed on the screen, allowing the user to enter a string value. The input value will be restricted to the characters specified in the charset. +With graphical renderers that expose ``GraphicalValueSelectionRenderer``, +character preview and selection are rendered through the value-area highlight, +so charset cycling remains visible without relying on a text cursor blinker. + .. image:: images/item-charset-input.gif :width: 400px :alt: Example of a charset input menu item You can create multiple charset input items in the same menu screen, each with its own label, default value, and charset. -For more information about the charset input item, check the :cpp:class:`API reference `. \ No newline at end of file +For more information about the charset input item, check the :cpp:class:`API reference `. diff --git a/docs/source/overview/items/input.rst b/docs/source/overview/items/input.rst index 64d5a655..9548c93e 100644 --- a/docs/source/overview/items/input.rst +++ b/docs/source/overview/items/input.rst @@ -24,6 +24,18 @@ You can create an input item by specifying the label and the default value: When the ``Name`` menu item is selected, an input field will be displayed on the screen, allowing the user to enter a string value. +Graphical rendering +~~~~~~~~~~~~~~~~~~~ + +``ItemInput`` exposes optional graphical capabilities through +``GraphicalMenuItem`` and reports its value width for right-aligned graphical +layouts. + +When a renderer exposes ``GraphicalValueSelectionRenderer`` via +``queryExtension()``, the input item highlights the active character instead of +using a blinking character cursor. Character-display renderers continue to use +the standard blinking cursor behavior. + .. image:: images/item-input.gif :width: 400px :alt: Example of an input menu item diff --git a/src/ItemInput.h b/src/ItemInput.h index 0627d3d7..556972fd 100644 --- a/src/ItemInput.h +++ b/src/ItemInput.h @@ -3,8 +3,13 @@ #include "LcdMenu.h" #include "MenuItem.h" +#include "display/GraphicalDisplayInterface.h" +#include "renderer/GraphicalMenuItem.h" +#include "renderer/GraphicalValueSelectionRenderer.h" #include +#include + /** * @brief Item that allows user to input string information. * @@ -18,7 +23,7 @@ * Has internal `edit` state. * Value area is scrollable, see `view`. */ -class ItemInput : public MenuItem { +class ItemInput : public MenuItem, public GraphicalMenuItem { protected: /** * @brief String value of item. @@ -62,6 +67,13 @@ class ItemInput : public MenuItem { */ fptrStr callback; + inline GraphicalValueSelectionRenderer* getGraphicalValueSelectionRenderer(MenuRenderer* renderer) const { + if (renderer == NULL) { + return NULL; + } + return static_cast(renderer->queryExtension(GraphicalValueSelectionRenderer::extensionId())); + } + public: /** * Construct a new ItemInput object with an initial value. @@ -105,6 +117,22 @@ class ItemInput : public MenuItem { } return false; } + + uint8_t measureGraphicalValueWidth(GraphicalDisplayInterface* display) const override { + return display == NULL ? 0 : display->getTextWidth(value); + } + + bool useTightGraphicalSelectionBox() const override { + return true; + } + + const void* queryCapability(uint8_t capabilityId) const override { + if (capabilityId == GraphicalMenuItem::capabilityId()) { + return static_cast(this); + } + return MenuItem::queryCapability(capabilityId); + } + /** * Get the callback function for this item. * @@ -114,9 +142,48 @@ class ItemInput : public MenuItem { protected: void draw(MenuRenderer* renderer) override { + GraphicalValueSelectionRenderer* selectionRenderer = getGraphicalValueSelectionRenderer(renderer); + if (selectionRenderer != NULL) { + char* graphicalValue = value; + char* insertionBuffer = NULL; + bool editing = MenuItem::isEditing(); + + if (editing) { + renderer->viewShift = 0; + uint8_t len = strlen(value); + uint8_t selectionStart = cursor > len ? len : cursor; + uint8_t selectionLength = 1; + + if (selectionStart >= len) { + insertionBuffer = new char[len + 2]; + memcpy(insertionBuffer, value, len); + insertionBuffer[len] = ' '; + insertionBuffer[len + 1] = '\0'; + graphicalValue = insertionBuffer; + } + + selectionRenderer->setValueSelection(selectionStart, selectionLength); + } else { + selectionRenderer->clearValueSelection(); + } + + renderer->drawItem(text, graphicalValue); + if (editing) { + selectionRenderer->clearValueSelection(); + } + + if (insertionBuffer != NULL) { + delete[] insertionBuffer; + } + + return; + } + const uint8_t viewSize = getViewSize(renderer); char* vbuf = new char[viewSize + 1]; - substring(value, view, viewSize, vbuf); + if (viewSize > 0) { + substring(value, view, viewSize, vbuf); + } vbuf[viewSize] = '\0'; renderer->drawItem(text, vbuf); delete[] vbuf; @@ -165,31 +232,54 @@ class ItemInput : public MenuItem { } } void enter(MenuRenderer* renderer) { - // Move cursor to the latest index + bool graphicalSelection = getGraphicalValueSelectionRenderer(renderer) != NULL; + + // Move cursor to the latest editable index uint8_t length = strlen(value); - cursor = length; - // Move view if needed - uint8_t viewSize = getViewSize(renderer); - if (cursor > viewSize) { - view = length - (viewSize - 1); + if (graphicalSelection && length > 0) { + cursor = length - 1; + } else { + cursor = length; + } + + if (graphicalSelection) { + view = 0; + renderer->viewShift = 0; + } else { + // Move view if needed + uint8_t viewSize = getViewSize(renderer); + if (viewSize == 0) { + viewSize = 1; + } + if (cursor > viewSize) { + view = length - (viewSize - 1); + } } + // Redraw MenuItem::beginEdit(); draw(renderer); - renderer->drawBlinker(); + if (!graphicalSelection) { + renderer->drawBlinker(); + } // Log LOG(F("ItemInput::enterEditMode"), value); }; void back(MenuRenderer* renderer) { renderer->clearBlinker(); MenuItem::endEdit(); + + renderer->viewShift = 0; + + if (callback != NULL) { + callback(value); + } + // Move view to 0 and redraw before exit cursor = 0; view = 0; draw(renderer); - if (callback != NULL) { - callback(value); - } + // Log LOG(F("ItemInput::exitEditMode"), value); }; @@ -198,8 +288,15 @@ class ItemInput : public MenuItem { return; } cursor--; + + if (getGraphicalValueSelectionRenderer(renderer) != NULL) { + draw(renderer); + LOG(F("ItemInput::left"), value); + return; + } + uint8_t cursorCol = renderer->getCursorCol(); - if (cursor <= view - 1) { + if (view > 0 && cursor < view) { view--; draw(renderer); } else { @@ -218,7 +315,17 @@ class ItemInput : public MenuItem { return; } cursor++; + + if (getGraphicalValueSelectionRenderer(renderer) != NULL) { + draw(renderer); + LOG(F("ItemInput::right"), value); + return; + } + uint8_t viewSize = getViewSize(renderer); + if (viewSize == 0) { + viewSize = 1; + } uint8_t cursorCol = renderer->getCursorCol(); if (cursor > (view + viewSize - 1)) { view++; @@ -240,6 +347,13 @@ class ItemInput : public MenuItem { } remove(value, cursor - 1, 1); cursor--; + + if (getGraphicalValueSelectionRenderer(renderer) != NULL) { + draw(renderer); + LOG(F("ItemInput::backspace"), value); + return; + } + uint8_t cursorCol = renderer->getCursorCol(); if (view > 0) { view--; @@ -279,7 +393,17 @@ class ItemInput : public MenuItem { delete[] value; value = buf; cursor++; + + if (getGraphicalValueSelectionRenderer(renderer) != NULL) { + draw(renderer); + LOG(F("ItemInput::typeChar"), character); + return; + } + uint8_t viewSize = getViewSize(renderer); + if (viewSize == 0) { + viewSize = 1; + } if (cursor > (view + viewSize - 1)) { view++; } @@ -296,7 +420,9 @@ class ItemInput : public MenuItem { cursor = 0; view = 0; draw(renderer); - renderer->drawBlinker(); + if (getGraphicalValueSelectionRenderer(renderer) == NULL) { + renderer->drawBlinker(); + } // Log LOG(F("ItemInput::clear"), value); } diff --git a/src/ItemInputCharset.h b/src/ItemInputCharset.h index a9ca07c7..693f399e 100644 --- a/src/ItemInputCharset.h +++ b/src/ItemInputCharset.h @@ -5,6 +5,8 @@ #include "LcdMenu.h" #include +#include + class ItemInputCharset : public ItemInput { private: const char* charset; @@ -117,6 +119,14 @@ class ItemInputCharset : public ItemInput { */ void abortCharEdit(MenuRenderer* renderer) { charEdit = false; + + if (getGraphicalValueSelectionRenderer(renderer) != NULL) { + renderer->viewShift = 0; + ItemInput::draw(renderer); + LOG(F("ItemInputCharset::abortCharEdit")); + return; + } + uint8_t cursorCol = renderer->getCursorCol(); if (cursor < strlen(value)) { renderer->draw(value[cursor]); @@ -167,6 +177,34 @@ class ItemInputCharset : public ItemInput { } void drawChar(MenuRenderer* renderer) { + if (getGraphicalValueSelectionRenderer(renderer) != NULL) { + renderer->viewShift = 0; + uint8_t length = strlen(value); + + // Update in place when cursor points to an existing character; when cursor + // is at the insertion position, render through a temporary preview buffer + // to avoid writing past the current string bounds. + if (cursor < length) { + char original = value[cursor]; + value[cursor] = charset[charsetPosition]; + ItemInput::draw(renderer); + value[cursor] = original; + } else { + char* preview = new char[length + 2]; + memcpy(preview, value, length); + preview[length] = charset[charsetPosition]; + preview[length + 1] = '\0'; + + char* originalValue = value; + value = preview; + ItemInput::draw(renderer); + value = originalValue; + + delete[] preview; + } + return; + } + renderer->moveCursor(renderer->getCursorCol(), renderer->getCursorRow()); renderer->draw(charset[charsetPosition]); renderer->moveCursor(renderer->getCursorCol(), renderer->getCursorRow()); diff --git a/test/LcdMenu.cpp b/test/LcdMenu.cpp index 30884892..809ac96f 100644 --- a/test/LcdMenu.cpp +++ b/test/LcdMenu.cpp @@ -9,8 +9,12 @@ #include #include #include +#include #include +#include +#include #include +#include #define LCD_ROWS 2 #define LCD_COLS 16 @@ -92,6 +96,79 @@ class TrackingRenderer : public MenuRenderer, public FrameLifecycleRenderer { } }; +class GraphicalMeasureDisplay : public GraphicalDisplayInterface { + public: + static const uint8_t kCharWidth = 6; + void begin() override {} + void clear() override {} + void show() override {} + void hide() override {} + void draw(uint8_t) override {} + void draw(const char*) override {} + void setCursor(uint8_t, uint8_t) override {} + void setBacklight(bool) override {} + void setFont(const uint8_t*) override {} + uint8_t getDisplayWidth() const override { return 128; } + uint8_t getDisplayHeight() const override { return 64; } + uint8_t getFontWidth() const override { return kCharWidth; } + uint8_t getFontHeight() const override { return 8; } + uint8_t getTextWidth(const char* text) override { + if (text == NULL) { + return 0; + } + return static_cast(strlen(text) * kCharWidth); + } + void setDrawColor(uint8_t) override {} + void clearBuffer() override {} + void sendBuffer() override {} + void drawBox(uint8_t, uint8_t, uint8_t, uint8_t) override {} + void drawFrame(uint8_t, uint8_t, uint8_t, uint8_t) override {} + void drawXbm(uint8_t, uint8_t, uint8_t, uint8_t, const uint8_t*) override {} +}; + +class SelectionTrackingRenderer : public MenuRenderer, public GraphicalValueSelectionRenderer { + public: + StubDisplay display; + uint8_t blinkerDrawCalls = 0; + uint8_t selectionStart = 0; + uint8_t selectionLength = 0; + bool hasSelection = false; + + SelectionTrackingRenderer() : MenuRenderer(&display, LCD_COLS, LCD_ROWS) {} + + void draw(uint8_t) override {} + void drawItem(const char*, const char*, bool) override {} + void clearBlinker() override {} + void drawBlinker() override { blinkerDrawCalls++; } + uint8_t getEffectiveCols() const override { return maxCols; } + + void setValueSelection(uint8_t start, uint8_t length) override { + selectionStart = start; + selectionLength = length; + hasSelection = length > 0; + } + + void clearValueSelection() override { + selectionStart = 0; + selectionLength = 0; + hasSelection = false; + } + + void* queryExtension(uint8_t extensionId) override { + if (extensionId == GraphicalValueSelectionRenderer::extensionId()) { + return static_cast(this); + } + return MenuRenderer::queryExtension(extensionId); + } + + const void* queryExtension(uint8_t extensionId) const override { + if (extensionId == GraphicalValueSelectionRenderer::extensionId()) { + return static_cast(this); + } + return MenuRenderer::queryExtension(extensionId); + } +}; + class PollingMenuItem : public MenuItem { public: bool wasDrawn = false; @@ -132,6 +209,35 @@ unittest(can_set_input_value) { assertEqual(expected, (static_cast(mainItems[ITEM_INPUT_INDEX]))->getValue()); } +unittest(input_item_exposes_graphical_capability) { + GraphicalMeasureDisplay display; + char value[] = "AB"; + ItemInput item("Name", value, NULL); + + const GraphicalMenuItem* capability = static_cast(item.queryCapability(GraphicalMenuItem::capabilityId())); + assertTrue(capability != NULL); + assertTrue(capability->useTightGraphicalSelectionBox()); + assertEqual((uint8_t)(2 * GraphicalMeasureDisplay::kCharWidth), capability->measureGraphicalValueWidth(&display)); +} + +unittest(input_item_uses_graphical_selection_extension_in_edit_mode) { + char value[] = "TEST"; + ItemInput item("Name", value, NULL); + SelectionTrackingRenderer renderer; + LcdMenu menu(renderer); + + assertTrue(item.process(&menu, ENTER)); + assertTrue(MenuItem::isEditing()); + assertEqual((uint8_t)3, item.cursor); + assertEqual((uint8_t)0, renderer.blinkerDrawCalls); + + assertTrue(item.process(&menu, LEFT)); + assertEqual((uint8_t)2, item.cursor); + + assertTrue(item.process(&menu, BACK)); + assertFalse(MenuItem::isEditing()); +} + unittest(cursor_clamped_when_out_of_range) { StubRenderer renderer; uint8_t outOfRange = 100;