MenuManager
contains the functions to manage menu items, there is a global instance of this class called menuMgr
. Here we present the most commonly used features, check out the reference docs for more details. There are a lot of iteration helper functions that can be used to navigate through menu structures. These are described further down the page. The following two reference documentation pages extend on the information here:
To enable next, back or both, you provide the pin onto which you’ve connected the button for this function. It will essentially register additional buttons with switches
on your behalf. Read through the guide for your input plugin if in doubt how it configured switches.
menuMgr.setNextButton(pinid_t pin);
menuMgr.setBackButton(pinid_t pin);
If you need custom control over menu manager input, then you can directly call into the menu manager as if the encoder has changed value, the button has been pressed, or even acting as if back or next were pressed. Note that this is an advanced feature and although this is possible, it’s often better to work with the RotaryEncoder or switches when possible.
// usually called by the encoder to indicate the current encoder position
void valueChanged(int value);
// called to indicate that the select button was pressed or help
void onMenuSelect(bool held);
// called when the back or next button is pressed
void performDirectionMove(bool dirIsBack);
Menu Manager makes it very easy to save values to EEPROM between runs. Each menu item can optionally have a storage point in the EEPROM area (-1 / 0xffff means not stored). Any items that have a valid EEPROM address will be persisted upon calling the save
function on menuMgr, and similarly, will be read back by calling load
.
In order to use any EEPROM functions, or to use EEPROM based choice menu items, you need to set up an EepromAbstraction and provide a pointer to menuMgr
using setEepromRef
. For details of how to create an EepromAbstraction see AVR and Arduino EEPROM example and I2C AT24 EEPROM example.
// before making any call to load or save, or before using ChoiceMenuItems
menuMgr.setEepromRef(eepromPtr);
void menuMgr.load(magicKey = 0xfade, firstRunCallback = NULL);
void menuMgr.save(magicKey = 0xfade);
Where:
Should you need to use EEPROM functions before initialisation, you can set the root menu item before initialise for this special case.
menuMgr.setRootMenu(rootItem);
A frequent question that is asked is ‘how should I call save to ensure that all menu item state is stored’. The best way to do this is to use a power loss detection circuit and call save in that call back. The second best is to have a timed function (maybe once every 10 minutes) along with a commit handler that detects changes. Never call save in the menu item callback, if using a rotary encoder that would break your EEPROM in days.
To retrieve the top most (first) menu item, or to get the currently active top menu item.
MenuItem* menuMgr.getRoot();
MenuItem* menuMgr.getCurrentMenu();
You can create additional items at runtime, you can even create new submenus at runtime. Any item that extends from RuntimeMenuItem
can easily be created on the fly.
To append a menu item immediately after an existing item you use the below function, if you are adding several at once, use the silent option to prevent displays and remotes refreshing more than once. If you use silent, then you must follow this with a call to notifyStructureChanged
.
menuMgr.addMenuAfter(existingItemPtr, newItemPtr, silent);
menuMgr.notifyStructureChanged();
You can even change the local display to a menu structure completely defined at runtime by calling the following, but bear in mind these would not be remotely through the API. However, it is very handy for local security dialogs and other similar local only situations:
menuMgr.setCurrentMenu(MenuItem* firstItemPtr);
You can listen for menu editing and structural changes using by providing an object that implements MenuManagerObserver
. You create an object that extends from that class and provide it addChangeNotification
:
class MyObserver : public MenuManagerObserver {
public:
virtual void structureHasChanged() {
Serial.println("menu tree structure changed");
}
virtual bool menuEditStarting(MenuItem* item) {
Serial.println("Editing started");
return true; // allow editing to start
}
virtual void menuEditEnded(MenuItem* item) {
Serial.println("Edit committed");
}
} myObserver;
void setup() {
// ...
menuMgr.addChangeNotification(&myObserver);
}
Alternatively, use the commit callback function which takes the same signature as a menu item callback but is only called upon an item being committed:
void setItemCommittedHook(MenuCallbackFn commitCallback)
Note that either of the above methods use one of the callback spaces. There is a limited number (default 4) of callbacks that can be registered, see the reference docs for more information. Be aware most renderers and remote capabilities tend to use a slot each.
When using the designer, this code gets added automatically, this is to fully document the API.
We initialise the menu manager as shown below, where renderer is a pointer to a renderer (or an instance of NoRenderer
), root is the very first menu item and then this is followed by the pin used for either switches or the rotary encoder. Remember that pinA of the encoder must be an interrupt capable pin. If you’re not using the designer, then the plugins for display and remote are available within the tcMenu code repo within the two plugin packages, or may be packaged with the embedded folder. Copy the required files into your sketch and make sure they are initialised.
menuMgr.initForEncoder(MenuRenderer* renderer, MenuItem* root, uint8_t encoderPinA, uint8_t encoderPinB, uint8_t encoderButton);
menuMgr.initForUpDownOk(MenuRenderer* renderer, MenuItem* root, uint8_t upPin, uint8_t downPin, uint8_t okPin);
menuMgr.initWithoutInput(MenuRenderer* renderer, MenuItem* root);
So for example to initialise for no local user interface. Define a global variable:
NoRenderer noRenderer;
Now initialise for no input:
menuMgr.initWithoutInput(&noRenderer, &rootMenuItem);
Most of the useful functions to iterate over menu items are within the following reference documentation page, we only document the most popular ones here, so it is worth checking this page too:
This class allows you to go through a menu structure using a common iteration approach. The iterator is provided with a predicate filter, then each item is tested against the predicate, and any that match are returned as the next match. Let’s take an example that gets all items of type AnalogMenuItem:
MenuItemTypePredicate intPredicate(MENUTYPE_INT_VALUE, TM_EXTRA_INCLUDE_SUBMENUS);
MenuItemIterator iterator;
iterator.setPredicate(&intPredicate);
auto item = iterator.nextItem();
while(item != nullptr) {
Serial.println(item->getId());
item = iterator.nextItem();
};
The first parameter to the type predicate is one of the MenuType
enumeration. The second parameter is a flag that tells the predicate which mode it’s working in, you can or together more than one choice:
// bit 0 represnts either regular or inverted mode (eg not a Analog item)
#define TM_REGULAR 0
#define TM_INVERTED 1
// bit 3 being on prevents entry into menus marked local only
#define TM_REGULAR_LOCAL_ONLY 8
#define TM_INVERTED_LOCAL_ONLY 9
// bit 4 being on tells the predicate to always recurse into submenus
#define TM_EXTRA_INCLUDE_SUBMENUS 16
There is another ready-made predicate that filters by remote number and changes. This is useful if you are implementing a remote facility.
RemoteNoMenuItemPredicate(int remoteNumber);
You can make your own predicate by extending from MenuItemPredicate
Get a menu item by ID
MenuItem* getMenuItemById(int id);
Get the parent for a given item
MenuItem* getParentRoot(MenuItem* current);
Menu iteration uses an internal queue. To save memory on the device this is limited to 4 items in depth. EG Root -> SubMenu -> SubMenu -> SubMenu
. If you need deeper trees than this, you can redefine MAX_MENU_DEPTH
to any value at the cost of a little memory.