├── .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 |
--------------------------------------------------------------------------------