├── .gitignore ├── LICENSING.txt ├── Menu.cpp ├── Menu.h ├── README.md └── examples ├── LCDMenuTest └── LCDMenuTest.pde └── TFTMenuTest ├── TFTMenuTest.ino ├── fixedmenu.xxx └── workingtftmenu.xxx /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | 4 | # Libraries 5 | *.lib 6 | *.a 7 | 8 | # Shared objects (inc. Windows DLLs) 9 | *.dll 10 | *.so 11 | *.so.* 12 | *.dylib 13 | 14 | # Executables 15 | *.exe 16 | *.out 17 | *.app 18 | -------------------------------------------------------------------------------- /LICENSING.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 karl@pitrich.com 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | ----------------------------------------------------------------------------- -------------------------------------------------------------------------------- /Menu.cpp: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MicroMenu 3 | // Copyright (c) 2014 karl@pitrich.com 4 | // All rights reserved. 5 | // License: MIT 6 | // ---------------------------------------------------------------------------- 7 | 8 | #include 9 | 10 | // ---------------------------------------------------------------------------- 11 | 12 | namespace Menu { 13 | 14 | // ---------------------------------------------------------------------------- 15 | 16 | const Item_t NullItem PROGMEM = { (const Menu::Item_s *)NULL, (const Menu::Item_s *)NULL, (const Menu::Item_s *)NULL, (const Menu::Item_s *)NULL, (const Menu::Callback_t)NULL, (const char *)NULL }; 17 | 18 | // ---------------------------------------------------------------------------- 19 | 20 | Engine::Engine() 21 | : currentItem(&Menu::NullItem), previousItem(&Menu::NullItem), lastInvokedItem(&Menu::NullItem) 22 | { 23 | } 24 | 25 | Engine::Engine(const Item_t *initialItem) 26 | : currentItem(initialItem), previousItem(&Menu::NullItem), lastInvokedItem(&Menu::NullItem) 27 | { 28 | } 29 | 30 | // ---------------------------------------------------------------------------- 31 | 32 | const char * Engine::getLabel(const Item_t * item) const { 33 | return (const char *)pgm_read_word((item == NULL) ? ¤tItem->Label : &item->Label); 34 | } 35 | 36 | const Item_t * Engine::getPrev(const Item_t * item) const { 37 | return (const Item_t *)pgm_read_word((item == NULL) ? ¤tItem->Previous : &item->Previous); 38 | } 39 | 40 | const Item_t * Engine::getNext(const Item_t * item) const { 41 | return (const Item_t *)pgm_read_word((item == NULL) ? ¤tItem->Next : &item->Next); 42 | } 43 | 44 | const Item_t * Engine::getParent(const Item_t * item) const { 45 | return (const Item_t *)pgm_read_word((item == NULL) ? ¤tItem->Parent : &item->Parent); 46 | } 47 | 48 | const Item_t * Engine::getChild(const Item_t * item) const { 49 | return (const Item_t *)pgm_read_word((item == NULL) ? ¤tItem->Child : &item->Child); 50 | } 51 | 52 | // ---------------------------------------------------------------------------- 53 | 54 | void Engine::navigate(const Item_t * targetItem) { 55 | uint8_t commit = true; 56 | if (targetItem && targetItem != &Menu::NullItem) { 57 | if (targetItem == getParent(currentItem)) { // navigating back to parent 58 | commit = executeCallbackAction(actionParent); // exit/save callback 59 | lastInvokedItem = &Menu::NullItem; 60 | } 61 | if (commit) { 62 | previousItem = currentItem; 63 | currentItem = targetItem; 64 | executeCallbackAction(actionLabel); 65 | } 66 | } 67 | } 68 | 69 | // ---------------------------------------------------------------------------- 70 | 71 | void Engine::invoke(void) { 72 | bool preventTrigger = false; 73 | 74 | if (lastInvokedItem != currentItem) { // prevent 'invoke' twice in a row 75 | lastInvokedItem = currentItem; 76 | preventTrigger = true; // don't invoke 'trigger' at first Display event 77 | executeCallbackAction(actionDisplay); 78 | } 79 | 80 | const Item_t *child = getChild(); 81 | if (child && child != &Menu::NullItem) { // navigate to registered submenuitem 82 | navigate(child); 83 | } 84 | else { // call trigger in already selected item that has no child 85 | if (!preventTrigger) { 86 | executeCallbackAction(actionTrigger); 87 | } 88 | } 89 | } 90 | 91 | // ---------------------------------------------------------------------------- 92 | 93 | bool Engine::executeCallbackAction(const Action_t action) const { 94 | if (currentItem && currentItem != NULL) { 95 | Callback_t callback = (Callback_t)pgm_read_word(¤tItem->Callback); 96 | 97 | if (callback != NULL) { 98 | return (*callback)(action); 99 | } 100 | } 101 | return true; 102 | } 103 | 104 | // ---------------------------------------------------------------------------- 105 | 106 | Info_t Engine::getItemInfo(const Item_t * item) const { 107 | Info_t result = { 0, 0 }; 108 | const Item_t * i = getChild(getParent()); 109 | for (; i && i != &Menu::NullItem && &i->Next && i->Next != &Menu::NullItem; i = getNext(i)) { 110 | result.siblings++; 111 | if (i == item) { 112 | result.position = result.siblings; 113 | } 114 | } 115 | 116 | return result; 117 | } 118 | 119 | // ---------------------------------------------------------------------------- 120 | 121 | void Engine::render(const RenderCallback_t render, uint8_t maxDisplayedMenuItems) const { 122 | if (!currentItem || currentItem == &Menu::NullItem) { 123 | return; 124 | } 125 | 126 | uint8_t start = 0; 127 | uint8_t itemCount = 0; 128 | const uint8_t center = maxDisplayedMenuItems >> 1; 129 | Info_t mi = getItemInfo(currentItem); 130 | 131 | if (mi.position >= (mi.siblings - center)) { // at end 132 | start = mi.siblings - maxDisplayedMenuItems; 133 | } 134 | else { 135 | start = mi.position - center; 136 | if (maxDisplayedMenuItems & 0x01) start--; // center if odd 137 | } 138 | 139 | if (start & 0x80) start = 0; // prevent overflow 140 | 141 | // first item in current menu level 142 | const Item_t * i = getChild(getParent()); 143 | for (; i && i != &Menu::NullItem && &i->Next && i->Next != &Menu::NullItem; i = getNext(i)) { 144 | if (itemCount - start >= maxDisplayedMenuItems) break; 145 | if (itemCount >= start) render(i, itemCount - start); 146 | itemCount++; 147 | } 148 | } 149 | 150 | // ---------------------------------------------------------------------------- 151 | 152 | }; // end namespace 153 | 154 | // ---------------------------------------------------------------------------- 155 | -------------------------------------------------------------------------------- /Menu.h: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MicroMenu 3 | // Copyright (c) 2014 karl@pitrich.com 4 | // All rights reserved. 5 | // License: MIT 6 | // ---------------------------------------------------------------------------- 7 | 8 | #ifndef __have_menu_h__ 9 | #define __have_menu_h__ 10 | 11 | #ifdef ARDUINO 12 | # include 13 | #else 14 | # include 15 | # include 16 | # include 17 | #endif 18 | 19 | namespace Menu 20 | { 21 | typedef enum Action_s { 22 | actionNone = 0, 23 | actionLabel = (1<<0), // render label when user scrolls through menu items 24 | actionDisplay = (1<<1), // display menu, after user selected a menu item 25 | actionTrigger = (1<<2), // trigger was pressed while menue was already active 26 | actionParent = (1<<3), // before moving to parent, useful for e.g. "save y/n?" or autosave 27 | actionCustom = (1<<7) 28 | } Action_t; 29 | 30 | typedef bool (*Callback_t)(Action_t); 31 | 32 | typedef struct Info_s { 33 | uint8_t siblings; 34 | uint8_t position; 35 | } Info_t; 36 | 37 | typedef struct Item_s { 38 | struct Item_s const * Next; 39 | struct Item_s const * Previous; 40 | struct Item_s const * Parent; 41 | struct Item_s const * Child; 42 | const Callback_t Callback; 43 | const char * Label; 44 | } Item_t; 45 | 46 | typedef void (*RenderCallback_t)(const Item_t *, uint8_t); 47 | 48 | // a typesafe null item 49 | extern const Item_t NullItem; 50 | 51 | class Engine { 52 | public: 53 | const Item_t * currentItem; 54 | const Item_t * previousItem; 55 | const Item_t * lastInvokedItem; 56 | 57 | public: 58 | Engine(); 59 | Engine(const Item_t * initialItem); 60 | 61 | public: 62 | void navigate(const Item_t * targetItem); 63 | void invoke(void); 64 | bool executeCallbackAction(const Action_t action) const; 65 | void render(const RenderCallback_t render, uint8_t maxDisplayedMenuItems) const; 66 | 67 | public: 68 | Info_t getItemInfo(const Item_t * item) const; 69 | const char * getLabel (const Item_t * item = NULL) const; 70 | const Item_t * getPrev (const Item_t * item = NULL) const; 71 | const Item_t * getNext (const Item_t * item = NULL) const; 72 | const Item_t * getParent(const Item_t * item = NULL) const; 73 | const Item_t * getChild (const Item_t * item = NULL) const; 74 | }; 75 | }; // end namespace Menu 76 | 77 | // ---------------------------------------------------------------------------- 78 | 79 | #define MenuItem(Name, Label, Next, Previous, Parent, Child, Callback) \ 80 | extern const Menu::Item_t Next, Previous, Parent, Child; \ 81 | const Menu::Item_t PROGMEM Name = { \ 82 | &Next, &Previous, &Parent, &Child, \ 83 | &Callback, \ 84 | Label \ 85 | } 86 | 87 | // ---------------------------------------------------------------------------- 88 | 89 | #endif // __have_menu_h__ 90 | 91 | // ---------------------------------------------------------------------------- 92 | 93 | /*! 94 | template for callback handler 95 | 96 | void menuCallback(menuAction_t action) { 97 | if (action == Menu::actionDisplay) { 98 | // initialy entering this menu item 99 | } 100 | 101 | if (action == Menu::actionTrigger) { 102 | // click on already active item 103 | } 104 | 105 | if (action == Menu::actionLabel) { 106 | // show thy label but don't do anything yet 107 | } 108 | 109 | if (action == Menu::actionParent) { 110 | // navigating to self->parent 111 | } 112 | } 113 | */ 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MicroMenu 2 | ========= 3 | Copyright (c) 2014 karl@pitrich.com 4 | 5 | 6 | Simple and concise Embedded Menu System for Arduino. 7 | 8 | **Please check out the comprehensive example applications.** 9 | 10 | 11 | *Note: This has been developed and tested with Arduino 1.5.x only.* 12 | 13 | ## Demo Videos 14 | 15 | **TODO** 16 | 17 | ## Feature Set 18 | 19 | #### Simple API 20 | - Only three core methods, `navigate()`, `invoke()` and `render()`, 21 | - plus simple **helpers** for navigation: `next()`, `prev()`, `parent()` and `child()`. 22 | 23 | #### Any hardware 24 | - You simply implement a handler for your 25 | - Clickwheels, Encoders (see examples) 26 | - < > Buttons 27 | - that will call `navigate()` with any of the *helpers* as an argument, depending on direction intended (e.g. `navigate(child())` or `navigate(next())`). 28 | 29 | #### Great display flexibility 30 | - Usable with any display: Text LCD, Graphic LCD, Serial Port, ... 31 | - Any menu type: vertical or horizontal, circular, ... 32 | - you only need to implement one method capable of rendering a single menu item 33 | 34 | #### Single callback per menu item, multiple actions 35 | Handles all events, eg. 36 | - Show the menu item label 37 | - can also be used to display current sensor data instead of the static text label 38 | - Display the menu widget 39 | - this is where you'd implement your 'widget', the actual funcktionality associate with a menu item 40 | - Trigger at the menu item 41 | - invoked when the user selects the item again, if already active 42 | - useful for: 43 | - save 44 | - accept calibration data when displaying live values 45 | - switching between two options or functions within a single menu item 46 | - e.g. to configure *precision* and *unit* for numeric values in a single menu (000.0 -> 00.00 -> 0.000 mV -> V -> kV 47 | - so you see a live preview 00.00kW or 000.0mV 48 | - Before Move to Parent (or exit the menu) 49 | - for instance to call a save method when you exit 50 | - the return value determines if the user can move to the parent or not, useful to handle unsave data 51 | - Stores menu data in Flash, saving RAM 52 | 53 | 54 | ## Installation 55 | 56 | [Clone] this repository or [download] the current version as an archive, copy or link it to your Arduino libraries folder, e.g. `~/Documents/Arduino/libraries` on a Mac, then restart the Arduino IDE. 57 | 58 | [download]:https://github.com/0xPIT/menu/archive/master.zip 59 | [Clone]:git@github.com:0xPIT/menu.git 60 | 61 | 62 | ## Usage and Concept 63 | 64 | The code base resides in it's own namespace `Menu`. So you need to either prefix all methods with `Menu::` or add `using namespace Menu;` somewhere in your code. I prefer to be explicit by opting for the prefix. 65 | 66 | #### 1) Define menu an structure 67 | Begin with a dummy `exit` menu item. With it, you can utilize all events and functions, e.g. the parent event will be invoked, allowing you to save all data when you leave the menu or even prevent to leave. 68 | Also, you can simply clear the display when the user leaves the menu. 69 | The arguments to the macro `MenuItem` are: Name, Label, Next, Previous, Parent, Child, Callback. If you have no child or sibling or parent, substitute with `Menu::NullItem`, this is a typesafe replacement for `NULL` 70 | 71 | MenuItem(miExit, "", Menu::NullItem, Menu::NullItem, Menu::NullItem, miSettings, menuExit); 72 | MenuItem(miSettings, "Settings", miTest1, Menu::NullItem, miExit, miCalibrateLo, menuDummy); 73 | MenuItem(miCalibrateLo, "Calibrate Lo", miCalibrateHi, Menu::NullItem, miSettings, Menu::NullItem, menuDummy); 74 | MenuItem(miCalibrateHi, "Calibrate Hi", miChannel0, miCalibrateLo, miSettings, Menu::NullItem, menuDummy); 75 | 76 | 77 | #### 2) Implement a callback to render a single menu item 78 | This example is for a 4-line text LCD, usable with the LiquidCrystal library shipped with Arduino. 79 | 80 | void renderMenuItem(const Menu::Item_t *mi, uint8_t pos) { 81 | lcd.setCursor(0, pos); 82 | lcd.print(engine->label(mi)); 83 | } 84 | 85 | #### 3) Handle menu events 86 | 87 | bool myMenuItemCallback(Menu::Action_t act) { 88 | switch (act) { 89 | case actionParent: 90 | saveAll(); 91 | return true; // return false to prevent navigation to parent 92 | break; 93 | case actionTrigger: 94 | adcSaveCalibrationData(); 95 | break; 96 | } 97 | } 98 | 99 | ## Example Hardware Configuration 100 | 101 | TODO: Image LCD and TFT Boards 102 | 103 | ## Licensing 104 | 105 | ``` 106 | The MIT License (MIT) 107 | 108 | Copyright (c) 2014 karl@pitrich.com 109 | All rights reserved. 110 | 111 | Permission is hereby granted, free of charge, to any person obtaining a copy 112 | of this software and associated documentation files (the "Software"), to deal 113 | in the Software without restriction, including without limitation the rights 114 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 115 | copies of the Software, and to permit persons to whom the Software is 116 | furnished to do so, subject to the following conditions: 117 | 118 | The above copyright notice and this permission notice shall be included in 119 | all copies or substantial portions of the Software. 120 | 121 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 122 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 123 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 124 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 125 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 126 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 127 | THE SOFTWARE. 128 | ``` 129 | -------------------------------------------------------------------------------- /examples/LCDMenuTest/LCDMenuTest.pde: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // ---------------------------------------------------------------------------- 9 | 10 | #define LCD_RS 8 11 | #define LCD_RW 9 12 | #define LCD_EN 10 13 | #define LCD_D4 4 14 | #define LCD_D5 5 15 | #define LCD_D6 6 16 | #define LCD_D7 7 17 | #define LCD_CHARS 20 18 | #define LCD_LINES 4 19 | 20 | enum Icons { 21 | IconLeft = 0, 22 | IconRight = 1, 23 | IconBack = 2, 24 | IconBlock = 3 25 | }; 26 | 27 | // ---------------------------------------------------------------------------- 28 | // helpers 29 | 30 | #define clampValue(val, lo, hi) if (val > hi) val = hi; if (val < lo) val = lo; 31 | #define maxValue(a, b) ((a > b) ? a : b) 32 | #define minValue(a, b) ((a < b) ? a : b) 33 | 34 | // ---------------------------------------------------------------------------- 35 | // display 36 | LiquidCrystal lcd(LCD_RS, 37 | #if LCD_RW >= 0 // RW is not necessary if lcd is on dedicated pins 38 | LCD_RW, 39 | #endif 40 | LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7 41 | ); 42 | 43 | // ---------------------------------------------------------------------------- 44 | // encoder 45 | 46 | ClickEncoder Encoder(A0, A1, A2, 2); 47 | 48 | void timerIsr(void) { 49 | Encoder.service(); 50 | } 51 | 52 | // ---------------------------------------------------------------------------- 53 | 54 | Menu::Engine *engine; 55 | 56 | // ---------------------------------------------------------------------------- 57 | 58 | namespace State { 59 | typedef enum SystemMode_e { 60 | None = 0, 61 | Default = (1<<0), 62 | Settings = (1<<1), 63 | Edit = (1<<2) 64 | } SystemMode; 65 | }; 66 | 67 | uint8_t systemState = State::Default; 68 | bool lastEncoderAccelerationState = true; 69 | uint8_t previousSystemState = State::None; 70 | 71 | // ---------------------------------------------------------------------------- 72 | 73 | bool menuExit(const Menu::Action_t a) { 74 | Encoder.setAccelerationEnabled(lastEncoderAccelerationState); 75 | systemState = State::Default; 76 | return true; 77 | } 78 | 79 | bool menuDummy(const Menu::Action_t a) { 80 | return true; 81 | } 82 | 83 | bool menuBack(const Menu::Action_t a) { 84 | if (a == Menu::actionDisplay) { 85 | engine->navigate(engine->getParent(engine->getParent())); 86 | } 87 | return true; 88 | } 89 | 90 | // ---------------------------------------------------------------------------- 91 | 92 | uint8_t menuItemsVisible = LCD_LINES; 93 | 94 | void renderMenuItem(const Menu::Item_t *mi, uint8_t pos) { 95 | lcd.setCursor(0, pos); 96 | 97 | // cursor 98 | if (engine->currentItem == mi) { 99 | lcd.write((uint8_t)IconBlock); 100 | } 101 | else { 102 | lcd.write(20); // space 103 | } 104 | 105 | lcd.print(engine->getLabel(mi)); 106 | 107 | // mark items that have children 108 | if (engine->getChild(mi) != &Menu::NullItem) { 109 | lcd.write(20); 110 | lcd.write((uint8_t)IconRight); 111 | lcd.print(" "); 112 | } 113 | } 114 | 115 | // ---------------------------------------------------------------------------- 116 | // Name, Label, Next, Previous, Parent, Child, Callback 117 | 118 | MenuItem(miExit, "", Menu::NullItem, Menu::NullItem, Menu::NullItem, miSettings, menuExit); 119 | 120 | MenuItem(miSettings, "Settings", miTest1, Menu::NullItem, miExit, miCalibrateLo, menuDummy); 121 | 122 | MenuItem(miCalibrateLo, "Calibrate Lo", miCalibrateHi, Menu::NullItem, miSettings, Menu::NullItem, menuDummy); 123 | MenuItem(miCalibrateHi, "Calibrate Hi", miChannel0, miCalibrateLo, miSettings, Menu::NullItem, menuDummy); 124 | 125 | MenuItem(miChannel0, "Channel 0", miChannel1, miCalibrateHi, miSettings, miChView0, menuDummy); 126 | MenuItem(miChView0, "Ch0:View", miChScale0, Menu::NullItem, miChannel0, Menu::NullItem, menuDummy); 127 | MenuItem(miChScale0, "Ch0:Scale", Menu::NullItem, miChView0, miChannel0, Menu::NullItem, menuDummy); 128 | 129 | MenuItem(miChannel1, "Channel 1", Menu::NullItem, miChannel0, miSettings, miChView1, menuDummy); 130 | MenuItem(miChView1, "Ch1:View", miChScale1, Menu::NullItem, miChannel1, Menu::NullItem, menuDummy); 131 | MenuItem(miChScale1, "Ch1:Scale", miChBack1, miChView1, miChannel1, Menu::NullItem, menuDummy); 132 | MenuItem(miChBack1, "Back \02", Menu::NullItem, miChScale1, miChannel1, Menu::NullItem, menuBack); 133 | 134 | MenuItem(miTest1, "Test 1 Menu", miTest2, miSettings, miExit, Menu::NullItem, menuDummy); 135 | MenuItem(miTest2, "Test 2 Menu", miTest3, miTest1, miExit, Menu::NullItem, menuDummy); 136 | MenuItem(miTest3, "Test 3 Menu", miTest4, miTest2, miExit, Menu::NullItem, menuDummy); 137 | MenuItem(miTest4, "Test 4 Menu", miTest5, miTest3, miExit, Menu::NullItem, menuDummy); 138 | MenuItem(miTest5, "Test 5 Menu", miTest6, miTest4, miExit, Menu::NullItem, menuDummy); 139 | MenuItem(miTest6, "Test 6 Menu", miTest7, miTest5, miExit, Menu::NullItem, menuDummy); 140 | MenuItem(miTest7, "Test 7 Menu", miTest8, miTest6, miExit, Menu::NullItem, menuDummy); 141 | MenuItem(miTest8, "Test 8 Menu", Menu::NullItem, miTest7, miExit, Menu::NullItem, menuDummy); 142 | 143 | // ---------------------------------------------------------------------------- 144 | 145 | byte left[8] = { 146 | 0b00010, 147 | 0b00110, 148 | 0b01110, 149 | 0b11110, 150 | 0b01110, 151 | 0b00110, 152 | 0b00010, 153 | 0b00000 154 | }; 155 | 156 | byte right[8] = { 157 | 0b01000, 158 | 0b01100, 159 | 0b01110, 160 | 0b01111, 161 | 0b01110, 162 | 0b01100, 163 | 0b01000, 164 | 0b00000 165 | }; 166 | 167 | byte back[8] = { 168 | 0b00000, 169 | 0b00100, 170 | 0b01100, 171 | 0b11111, 172 | 0b01101, 173 | 0b00101, 174 | 0b00001, 175 | 0b00000 176 | }; 177 | 178 | byte block[8] = { 179 | 0b00000, 180 | 0b11110, 181 | 0b11110, 182 | 0b11110, 183 | 0b11110, 184 | 0b11110, 185 | 0b11110, 186 | 0b00000 187 | }; 188 | 189 | // ---------------------------------------------------------------------------- 190 | 191 | void setup() { 192 | Serial.begin(57600); 193 | Serial.print("Starting..."); 194 | 195 | lcd.begin(LCD_CHARS, LCD_LINES); 196 | lcd.clear(); 197 | lcd.setCursor(0, 0); 198 | 199 | lcd.createChar(IconLeft, left); 200 | lcd.createChar(IconRight, right); 201 | lcd.createChar(IconBack, back); 202 | lcd.createChar(IconBlock, block); 203 | 204 | Timer1.initialize(1000); 205 | Timer1.attachInterrupt(timerIsr); 206 | 207 | engine = new Menu::Engine(&Menu::NullItem); 208 | menuExit(Menu::actionNone); // reset to initial state 209 | } 210 | 211 | // ---------------------------------------------------------------------------- 212 | 213 | int16_t encMovement; 214 | int16_t encAbsolute; 215 | int16_t encLastAbsolute = -1; 216 | bool updateMenu = false; 217 | 218 | // ---------------------------------------------------------------------------- 219 | 220 | void loop() { 221 | // handle encoder 222 | encMovement = Encoder.getValue(); 223 | if (encMovement) { 224 | encAbsolute += encMovement; 225 | 226 | if (systemState == State::Settings) { 227 | engine->navigate((encMovement > 0) ? engine->getNext() : engine->getPrev()); 228 | updateMenu = true; 229 | } 230 | } 231 | 232 | // handle button 233 | switch (Encoder.getButton()) { 234 | 235 | case ClickEncoder::Clicked: 236 | if (systemState == State::Settings) { 237 | engine->invoke(); 238 | updateMenu = true; 239 | } 240 | break; 241 | 242 | case ClickEncoder::DoubleClicked: 243 | if (systemState == State::Settings) { 244 | engine->navigate(engine->getParent()); 245 | updateMenu = true; 246 | } 247 | 248 | if (systemState == State::Default) { 249 | Encoder.setAccelerationEnabled(!Encoder.getAccelerationEnabled()); 250 | 251 | if (LCD_LINES > 2) { 252 | lcd.setCursor(0, 2); 253 | lcd.print("Acceleration: "); 254 | lcd.print((Encoder.getAccelerationEnabled()) ? "on " : "off"); 255 | } 256 | } 257 | break; 258 | 259 | case ClickEncoder::Held: 260 | if (systemState != State::Settings) { // enter settings menu 261 | 262 | // disable acceleration, reset in menuExit() 263 | lastEncoderAccelerationState = Encoder.getAccelerationEnabled(); 264 | Encoder.setAccelerationEnabled(false); 265 | 266 | engine->navigate(&miSettings); 267 | 268 | systemState = State::Settings; 269 | previousSystemState = systemState; 270 | updateMenu = true; 271 | } 272 | break; 273 | } 274 | 275 | if (updateMenu) { 276 | updateMenu = false; 277 | 278 | if (!encMovement) { // clear menu on child/parent navigation 279 | lcd.clear(); 280 | } 281 | 282 | // render the menu 283 | engine->render(renderMenuItem, menuItemsVisible); 284 | 285 | /* 286 | if (LCD_LINES > 2) { 287 | lcd.setCursor(0, LCD_LINES - 1); 288 | lcd.print("Doubleclick to "); 289 | if (engine->getParent() == &miExit) { 290 | lcd.print("exit. "); 291 | } 292 | else { 293 | lcd.print("go up."); 294 | } 295 | } 296 | */ 297 | } 298 | 299 | // dummy "application" 300 | if (systemState == State::Default) { 301 | if (systemState != previousSystemState) { 302 | previousSystemState = systemState; 303 | encLastAbsolute = -999; 304 | 305 | lcd.setCursor(0, 0); 306 | lcd.print("Main Screen"); 307 | 308 | if (LCD_LINES > 2) { 309 | lcd.setCursor(0, LCD_LINES - 1); 310 | lcd.print("Hold for setup"); 311 | } 312 | } 313 | 314 | if (encAbsolute != encLastAbsolute) { 315 | encLastAbsolute = encAbsolute; 316 | char tmp[10]; 317 | sprintf(tmp, "%4d", encAbsolute); 318 | lcd.setCursor(0, 1); 319 | lcd.print("Position: "); 320 | lcd.print(tmp); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /examples/TFTMenuTest/TFTMenuTest.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // ---------------------------------------------------------------------------- 11 | // helpers 12 | 13 | class ScopedTimer { 14 | public: 15 | ScopedTimer(const char * Label) 16 | : label(Label), ts(millis()) 17 | { 18 | } 19 | ~ScopedTimer() { 20 | Serial.print(label); Serial.print(": "); 21 | Serial.println(millis() - ts); 22 | } 23 | private: 24 | const char *label; 25 | const unsigned long ts; 26 | }; 27 | 28 | #define clampValue(val, lo, hi) if (val > hi) val = hi; if (val < lo) val = lo; 29 | #define maxValue(a, b) ((a > b) ? a : b) 30 | #define minValue(a, b) ((a < b) ? a : b) 31 | 32 | // ---------------------------------------------------------------------------- 33 | // display 34 | 35 | #define LCD_CS 9 36 | #define LCD_DC 7 37 | #define LCD_RST 8 38 | Adafruit_ST7735 tft = Adafruit_ST7735(LCD_CS, LCD_DC, LCD_RST); 39 | 40 | // ---------------------------------------------------------------------------- 41 | // encoder 42 | 43 | ClickEncoder Encoder(A0, A1, A2, 2); 44 | 45 | void timerIsr(void) { 46 | Encoder.service(); 47 | } 48 | 49 | // ---------------------------------------------------------------------------- 50 | 51 | Menu::Engine *engine; 52 | 53 | // ---------------------------------------------------------------------------- 54 | 55 | namespace State { 56 | typedef enum SystemMode_e { 57 | None = 0, 58 | Default = (1<<0), 59 | Settings = (1<<1), 60 | Edit = (1<<2) 61 | } SystemMode; 62 | }; 63 | 64 | uint8_t systemState = State::Default; 65 | bool lastEncoderAccelerationState = true; 66 | uint8_t previousSystemState = State::None; 67 | 68 | // ---------------------------------------------------------------------------- 69 | 70 | bool menuExit(const Menu::Action_t a) { 71 | Encoder.setAccelerationEnabled(lastEncoderAccelerationState); 72 | systemState = State::Default; 73 | return true; 74 | } 75 | 76 | bool menuDummy(const Menu::Action_t a) { 77 | return true; 78 | } 79 | 80 | bool menuBack(const Menu::Action_t a) { 81 | if (a == Menu::actionDisplay) { 82 | engine->navigate(engine->getParent(engine->getParent())); 83 | } 84 | return true; 85 | } 86 | 87 | // ---------------------------------------------------------------------------- 88 | 89 | uint8_t menuItemsVisible = 5; 90 | uint8_t menuItemHeight = 12; 91 | 92 | void renderMenuItem(const Menu::Item_t *mi, uint8_t pos) { 93 | //ScopedTimer tm(" render menuitem"); 94 | 95 | uint8_t y = pos * menuItemHeight + 2; 96 | 97 | tft.setCursor(10, y); 98 | 99 | // a cursor 100 | tft.drawRect(8, y - 2, 90, menuItemHeight, (engine->currentItem == mi) ? ST7735_RED : ST7735_BLACK); 101 | tft.print(engine->getLabel(mi)); 102 | 103 | // mark items that have children 104 | if (engine->getChild(mi) != &Menu::NullItem) { 105 | tft.print(" > "); 106 | } 107 | } 108 | 109 | // ---------------------------------------------------------------------------- 110 | 111 | // Name, Label, Next, Previous, Parent, Child, Callback 112 | MenuItem(miExit, "", Menu::NullItem, Menu::NullItem, Menu::NullItem, miSettings, menuExit); 113 | 114 | MenuItem(miSettings, "Settings", miTest1, Menu::NullItem, miExit, miCalibrateLo, menuDummy); 115 | 116 | MenuItem(miCalibrateLo, "Calibrate Lo", miCalibrateHi, Menu::NullItem, miSettings, Menu::NullItem, menuDummy); 117 | MenuItem(miCalibrateHi, "Calibrate Hi", miChannel0, miCalibrateLo, miSettings, Menu::NullItem, menuDummy); 118 | 119 | MenuItem(miChannel0, "Channel 0", miChannel1, miCalibrateHi, miSettings, miChView0, menuDummy); 120 | MenuItem(miChView0, "Ch0:View", miChScale0, Menu::NullItem, miChannel0, Menu::NullItem, menuDummy); 121 | MenuItem(miChScale0, "Ch0:Scale", Menu::NullItem, miChView0, miChannel0, Menu::NullItem, menuDummy); 122 | 123 | MenuItem(miChannel1, "Channel 1", Menu::NullItem, miChannel0, miSettings, miChView1, menuDummy); 124 | MenuItem(miChView1, "Ch1:View", miChScale1, Menu::NullItem, miChannel1, Menu::NullItem, menuDummy); 125 | MenuItem(miChScale1, "Ch1:Scale", miChBack1, miChView1, miChannel1, Menu::NullItem, menuDummy); 126 | MenuItem(miChBack1, "Back", Menu::NullItem, miChScale1, miChannel1, Menu::NullItem, menuBack); 127 | 128 | MenuItem(miTest1, "Test 1 Menu", miTest2, miSettings, miExit, Menu::NullItem, menuDummy); 129 | MenuItem(miTest2, "Test 2 Menu", miTest3, miTest1, miExit, Menu::NullItem, menuDummy); 130 | MenuItem(miTest3, "Test 3 Menu", miTest4, miTest2, miExit, Menu::NullItem, menuDummy); 131 | MenuItem(miTest4, "Test 4 Menu", miTest5, miTest3, miExit, Menu::NullItem, menuDummy); 132 | MenuItem(miTest5, "Test 5 Menu", miTest6, miTest4, miExit, Menu::NullItem, menuDummy); 133 | MenuItem(miTest6, "Test 6 Menu", miTest7, miTest5, miExit, Menu::NullItem, menuDummy); 134 | MenuItem(miTest7, "Test 7 Menu", miTest8, miTest6, miExit, Menu::NullItem, menuDummy); 135 | MenuItem(miTest8, "Test 8 Menu", Menu::NullItem, miTest7, miExit, Menu::NullItem, menuDummy); 136 | 137 | // ---------------------------------------------------------------------------- 138 | 139 | void setup() { 140 | Serial.begin(57600); 141 | Serial.print("Starting..."); 142 | 143 | tft.initR(INITR_REDTAB); 144 | tft.setTextWrap(false); 145 | tft.setTextSize(1); 146 | tft.setRotation(3); 147 | 148 | Timer1.initialize(1000); 149 | Timer1.attachInterrupt(timerIsr); 150 | 151 | pinMode(MISO, OUTPUT); 152 | pinMode(MOSI, OUTPUT); 153 | // enable pull up, otherwise display flickers 154 | PORTB |= (1 << MOSI) | (1 << MISO); 155 | 156 | engine = new Menu::Engine(&Menu::NullItem); 157 | menuExit(Menu::actionDisplay); // reset to initial state 158 | } 159 | 160 | // ---------------------------------------------------------------------------- 161 | 162 | int16_t encMovement; 163 | int16_t encAbsolute; 164 | int16_t encLastAbsolute = -1; 165 | bool updateMenu = false; 166 | 167 | // ---------------------------------------------------------------------------- 168 | 169 | void loop() { 170 | 171 | // handle encoder 172 | encMovement = Encoder.getValue(); 173 | if (encMovement) { 174 | encAbsolute += encMovement; 175 | 176 | if (systemState == State::Settings) { 177 | engine->navigate((encMovement > 0) ? engine->getNext() : engine->getPrev()); 178 | updateMenu = true; 179 | } 180 | } 181 | 182 | // handle button 183 | switch (Encoder.getButton()) { 184 | 185 | case ClickEncoder::Clicked: 186 | if (systemState == State::Settings) { 187 | engine->invoke(); 188 | updateMenu = true; 189 | } 190 | break; 191 | 192 | case ClickEncoder::DoubleClicked: 193 | if (systemState == State::Settings) { 194 | engine->navigate(engine->getParent()); 195 | updateMenu = true; 196 | } 197 | 198 | if (systemState == State::Default) { 199 | Encoder.setAccelerationEnabled(!Encoder.getAccelerationEnabled()); 200 | tft.setTextSize(1); 201 | tft.setCursor(10, 42); 202 | tft.print("Acceleration: "); 203 | tft.print((Encoder.getAccelerationEnabled()) ? "on " : "off"); 204 | } 205 | break; 206 | 207 | case ClickEncoder::Held: 208 | if (systemState != State::Settings) { // enter settings menu 209 | 210 | // disable acceleration, reset in menuExit() 211 | lastEncoderAccelerationState = Encoder.getAccelerationEnabled(); 212 | Encoder.setAccelerationEnabled(false); 213 | 214 | tft.fillScreen(ST7735_BLACK); 215 | tft.setTextColor(ST7735_WHITE, ST7735_BLACK); 216 | 217 | engine->navigate(&miSettings); 218 | 219 | systemState = State::Settings; 220 | previousSystemState = systemState; 221 | updateMenu = true; 222 | } 223 | break; 224 | } 225 | 226 | if (updateMenu) { 227 | updateMenu = false; 228 | 229 | if (!encMovement) { // clear menu on child/parent navigation 230 | tft.fillRect(8, 1, 120, 100, ST7735_BLACK); 231 | } 232 | 233 | // simple scrollbar 234 | Menu::Info_t mi = engine->getItemInfo(engine->currentItem); 235 | uint8_t sbTop = 0, sbWidth = 4, sbLeft = 100; 236 | uint8_t sbItems = minValue(menuItemsVisible, mi.siblings); 237 | uint8_t sbHeight = sbItems * menuItemHeight; 238 | uint8_t sbMarkHeight = sbHeight * sbItems / mi.siblings; 239 | uint8_t sbMarkTop = ((sbHeight - sbMarkHeight) / mi.siblings) * (mi.position -1); 240 | tft.fillRect(sbLeft, sbTop, sbWidth, sbHeight, ST7735_WHITE); 241 | tft.fillRect(sbLeft, sbMarkTop, sbWidth, sbMarkHeight, ST7735_RED); 242 | 243 | // debug scrollbar values 244 | #if 0 245 | char buf[30]; 246 | sprintf(buf, "itms: %d, h: %d, mh: %d, mt: %d", sbItems, sbHeight, sbMarkHeight, sbMarkTop); 247 | Serial.println(buf); 248 | #endif 249 | 250 | // render the menu 251 | { 252 | //ScopedTimer tm("render menu"); 253 | engine->render(renderMenuItem, menuItemsVisible); 254 | } 255 | 256 | { 257 | ScopedTimer tm("helptext"); 258 | tft.setTextSize(1); 259 | tft.setCursor(10, 110); 260 | tft.print("Doubleclick to "); 261 | if (engine->getParent() == &miExit) { 262 | tft.print("exit. "); 263 | } 264 | else { 265 | tft.print("go up."); 266 | } 267 | } 268 | } 269 | 270 | // dummy "application" 271 | if (systemState == State::Default) { 272 | if (systemState != previousSystemState) { 273 | previousSystemState = systemState; 274 | encLastAbsolute = -999; // force updateMenu 275 | tft.fillScreen(ST7735_WHITE); 276 | tft.setTextColor(ST7735_BLACK, ST7735_WHITE); 277 | tft.setCursor(10, 10); 278 | tft.setTextSize(2); 279 | tft.print("Main Screen"); 280 | 281 | tft.setTextSize(1); 282 | tft.setCursor(10, 110); 283 | tft.print("Hold button for setup"); 284 | } 285 | 286 | if (encAbsolute != encLastAbsolute) { 287 | encLastAbsolute = encAbsolute; 288 | tft.setCursor(10, 30); 289 | tft.setTextSize(1); 290 | tft.print("Position:"); 291 | tft.setCursor(70, 30); 292 | char tmp[10]; 293 | sprintf(tmp, "%4d", encAbsolute); 294 | tft.print(tmp); 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /examples/TFTMenuTest/fixedmenu.xxx: -------------------------------------------------------------------------------- 1 | 2 | void menuInvoke(void) 3 | { 4 | uint8_t preventTrigger = FALSE; 5 | 6 | if (menuLastInvokedItem != menuCurrentItem) { // prevent 'invoke' twice in a row 7 | menuLastInvokedItem = menuCurrentItem; 8 | preventTrigger = TRUE; // don't invoke 'trigger' at first Display event 9 | menuExecuteCallbackAction(menuActionDisplay); 10 | } 11 | 12 | if (menuCurrentChild != &menuNull) { // navigate to registered submenuitem 13 | menuNavigate(menuCurrentChild); 14 | } 15 | else { // call trigger in already selected item that has no child 16 | if (!preventTrigger) { 17 | menuExecuteCallbackAction(menuActionTrigger); 18 | } 19 | } 20 | } 21 | 22 | uint8_t menuExecuteCallbackAction(const menuAction_t action) 23 | { 24 | if (menuCurrentItem && menuCurrentItem != &menuNull) { 25 | MenuCallback_t callback = (MenuCallback_t)pgm_read_word(&menuCurrentItem->Callback); 26 | 27 | if (callback != NULL) { 28 | return (*callback)(action); 29 | } 30 | } 31 | return TRUE; 32 | } 33 | 34 | void menuNavigate(const MenuItem_t * newitem) 35 | { 36 | uint8_t commit = TRUE; 37 | if (newitem && newitem != &menuNull) { 38 | if (newitem == menuParent(menuCurrentItem)) { // navigating back to parent 39 | commit = menuExecuteCallbackAction(menuActionParent); // exit/save callback 40 | } 41 | if (commit != FALSE) { 42 | menuPreviousItem = menuCurrentItem; 43 | menuCurrentItem = newitem; 44 | menuExecuteCallbackAction(menuActionLabel); 45 | } 46 | } 47 | } 48 | 49 | menuInfo_t menuInfo(const MenuItem_t * mi) { 50 | menuInfo_t result = { 0, 0 }; 51 | 52 | const MenuItem_t * i = menuChild(menuParent(menuCurrentItem)); 53 | for (; i && &i->Next && i != &menuNull; i = menuNext(i)) { 54 | result.siblings++; 55 | if (i == mi) { 56 | result.position = result.siblings; 57 | } 58 | } 59 | 60 | //result.siblings--; 61 | 62 | return result; 63 | } 64 | 65 | // ---------------------------------------------------------------------------- 66 | 67 | void menuRender(const MenuRenderCallback_t render, uint8_t maxDisplayedMenuItems) 68 | { 69 | uint8_t start = 0; 70 | uint8_t itemCount = 0; 71 | const uint8_t center = maxDisplayedMenuItems >> 1; 72 | menuInfo_t mi = menuInfo(menuCurrentItem); 73 | 74 | if (mi.position >= (mi.siblings - center)) { // at end 75 | start = mi.siblings - maxDisplayedMenuItems; 76 | } 77 | else { 78 | start = mi.position - center; 79 | if (maxDisplayedMenuItems & 0x01) start--; // center, if odd 80 | } 81 | 82 | if (start & 0x80) start = 0; // prevent overflow 83 | 84 | // first item in current menu level 85 | const MenuItem_t * i = menuChild(menuParent(menuCurrentItem)); 86 | 87 | for (; i && i != &menuNull && &i->Next; i = menuNext(i)) { 88 | if (itemCount - start >= maxDisplayedMenuItems) break; 89 | if (itemCount >= start) render(i, itemCount - start); 90 | itemCount++; 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /examples/TFTMenuTest/workingtftmenu.xxx: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | namespace Menu 5 | { 6 | typedef enum Action_s { 7 | actionNone = 0, 8 | actionLabel = (1<<0), // render label 9 | actionDisplay = (1<<1), // display menu 10 | actionTrigger = (1<<2), // trigger was pressed 11 | actionParent = (1<<3), // before move to parent 12 | actionCustom = (1<<7) 13 | } Action_t; 14 | 15 | typedef bool (*Callback_t)(Action_t); 16 | 17 | typedef struct Info_s { 18 | uint8_t siblings; 19 | uint8_t position; 20 | } Info_t; 21 | 22 | typedef struct Item_s { 23 | struct Item_s const * Next; 24 | struct Item_s const * Previous; 25 | struct Item_s const * Parent; 26 | struct Item_s const * Child; 27 | const Callback_t Callback; 28 | const char * Label; 29 | } Item_t; 30 | 31 | typedef void (*RenderCallback_t)(const Item_t *, uint8_t); 32 | 33 | // a typesafe null item 34 | const Item_t NullItem PROGMEM = { (const Menu::Item_s *)NULL, (const Menu::Item_s *)NULL, (const Menu::Item_s *)NULL, (const Menu::Item_s *)NULL, (const Menu::Callback_t)NULL, (const char *)NULL }; 35 | 36 | class Engine { 37 | public: 38 | const Item_t * currentItem; 39 | const Item_t * previousItem; 40 | const Item_t * lastInvokedItem; 41 | 42 | public: 43 | Engine() 44 | : currentItem(&NullItem), previousItem(&NullItem), lastInvokedItem(&NullItem) 45 | { 46 | } 47 | 48 | Engine(const Item_t *initialItem) 49 | : currentItem(initialItem), previousItem(&NullItem), lastInvokedItem(&NullItem) 50 | { 51 | } 52 | 53 | public: 54 | void navigate(const Item_t * targetItem); 55 | void invoke(void); 56 | bool executeCallbackAction(const Action_t action) const; 57 | Info_t itemInfo(const Item_t * item) const; 58 | void render(const RenderCallback_t render, uint8_t maxDisplayedMenuItems) const; 59 | 60 | public: 61 | const char * getLabel(const Item_t *item = &NullItem) const { 62 | if (item == &NullItem) item = Engine::currentItem; 63 | return (const char *)pgm_read_word(&item->Label); 64 | } 65 | 66 | const Item_t * getPrev(const Item_t *item = &NullItem) const { 67 | if (item == &NullItem) item = Engine::currentItem; 68 | return reinterpret_cast(pgm_read_word(&item->Previous)); 69 | } 70 | 71 | const Item_t * getNext(const Item_t *item = &NullItem) const { 72 | if (item == &NullItem) item = Engine::currentItem; 73 | return reinterpret_cast(pgm_read_word(&item->Next)); 74 | } 75 | 76 | const Item_t * getParent(const Item_t *item = &NullItem) const { 77 | if (item == &NullItem) item = Engine::currentItem; 78 | return reinterpret_cast(pgm_read_word(&item->Parent)); 79 | } 80 | 81 | const Item_t * getChild(const Item_t *item = &NullItem) const { 82 | if (item == &NullItem) item = Engine::currentItem; 83 | return reinterpret_cast(pgm_read_word(&item->Child)); 84 | } 85 | }; 86 | 87 | void Engine::navigate(const Item_t * targetItem) { 88 | uint8_t commit = true; 89 | if (targetItem && targetItem != &NullItem) { 90 | if (targetItem == getParent(currentItem)) { // navigating back to parent 91 | commit = executeCallbackAction(actionParent); // exit/save callback 92 | } 93 | if (commit) { 94 | previousItem = currentItem; 95 | currentItem = targetItem; 96 | executeCallbackAction(actionLabel); 97 | } 98 | } 99 | } 100 | 101 | void Engine::invoke(void) { 102 | bool preventTrigger = false; 103 | 104 | if (lastInvokedItem != currentItem) { // prevent 'invoke' twice in a row 105 | lastInvokedItem = currentItem; 106 | preventTrigger = true; // don't invoke 'trigger' at first Display event 107 | executeCallbackAction(actionDisplay); 108 | } 109 | 110 | const Item_t *child = getChild(); 111 | if (child != &NullItem) { // navigate to registered submenuitem 112 | navigate(child); 113 | } 114 | else { // call trigger in already selected item that has no child 115 | if (!preventTrigger) { 116 | executeCallbackAction(actionTrigger); 117 | } 118 | } 119 | } 120 | 121 | 122 | bool Engine::executeCallbackAction(const Action_t action) const { 123 | if (currentItem && currentItem != &NullItem) { 124 | Callback_t callback = (Callback_t)pgm_read_word(¤tItem->Callback); 125 | 126 | if (callback != NULL) { 127 | return (*callback)(action); 128 | } 129 | } 130 | return true; 131 | } 132 | 133 | 134 | Info_t Engine::itemInfo(const Item_t * item) const { 135 | Info_t result = { 0, 0 }; 136 | 137 | const Item_t * i = getChild(getParent()); 138 | 139 | //Serial.print("iteminfo start at "); 140 | //Serial.print(getLabel(i)); 141 | 142 | for (; i && &i->Next && i != &NullItem; i = getNext(i)) { 143 | //Serial.print("test: "); 144 | //Serial.println(getLabel(i)); 145 | 146 | result.siblings++; 147 | if (i == item) { 148 | result.position = result.siblings; 149 | } 150 | } 151 | 152 | return result; 153 | } 154 | 155 | void Engine::render(const RenderCallback_t render, uint8_t maxDisplayedMenuItems) const { 156 | if (currentItem == &NullItem) { 157 | Serial.println("not rendering null item"); 158 | return; 159 | } 160 | 161 | Serial.print("start render for "); 162 | Serial.println(getLabel(currentItem)); 163 | 164 | uint8_t start = 0; 165 | uint8_t itemCount = 0; 166 | const uint8_t center = maxDisplayedMenuItems >> 1; 167 | Info_t mi = itemInfo(currentItem); 168 | 169 | //Serial.print(" s/p/c "); 170 | //Serial.print(mi.siblings); 171 | //Serial.println(mi.position); 172 | //Serial.println(center); 173 | 174 | if (mi.position >= (mi.siblings - center)) { // at end 175 | start = mi.siblings - maxDisplayedMenuItems; 176 | } 177 | else { 178 | start = mi.position - center; 179 | if (maxDisplayedMenuItems & 0x01) start--; // center, if odd 180 | } 181 | 182 | //Serial.print(" st: "); Serial.println(start); 183 | 184 | if (start & 0x80) start = 0; // prevent overflow 185 | 186 | // first item in current menu level 187 | const Item_t * i = getChild(getParent(currentItem)); 188 | //Serial.print("render starts from: "); Serial.println(getLabel(i)); 189 | for (; i && i != &NullItem && &i->Next; i = getNext(i)) { 190 | if (itemCount - start >= maxDisplayedMenuItems) break; 191 | if (itemCount >= start) render(i, itemCount - start); 192 | itemCount++; 193 | } 194 | } 195 | 196 | }; 197 | 198 | 199 | #define MenuItem(Name, Label, Next, Previous, Parent, Child, Callback) \ 200 | extern const Menu::Item_t Next, Previous, Parent, Child; \ 201 | const Menu::Item_t PROGMEM Name = { \ 202 | &Next, &Previous, &Parent, &Child, \ 203 | &Callback, \ 204 | Label \ 205 | } 206 | 207 | --------------------------------------------------------------------------------