MenuBackend, new menu managment library

I just created a new library for menu managment.
It has no limits as to what kind of logical layout you want.

MenuBackend 1.0

Every item can be thought of as a cross with directional connections in these direcitons: above, right, belov, left.

You can add an item to another item at these locations. Say you have two items, called A and B.
If you want these to be ordered as a horizontal menu with A being the first item, you simply add B to a at the right position.

A.addRight(B);

Now your menu support these actions (if you have added the A item to the menu):

menu.moveRight(); //now menu.getCurrent() == B
menu.moveLeft(); //now menu.getCurrent() == A

Both menu setup and menu navigation is demonstrated in the example below.

This menu also support backstepping. It is demonstrated in the example below.

#include <MenuBackend.h>

/*
This program demonstrates a menu modeled after the menubar in the Arduino IDE

+root
+file +edit +sketch +tools +help
+new +cut +verify (V) +autoformat +environment
+open
+examples
+ArduinoISP
*/

//this controls the menu backend and the event generation
MenuBackend menu = MenuBackend(menuUseEvent,menuChangeEvent);
//beneath is list of menu items needed to build the menu
MenuItem miFile = MenuItem("File");
MenuItem miNew = MenuItem("New");
MenuItem miOpen = MenuItem("Open");
MenuItem miExamples = MenuItem("Examples");
MenuItem miArduinoISP = MenuItem("ArduinoISP");

MenuItem miEdit = MenuItem("Edit");
MenuItem miCut = MenuItem("Cut");

MenuItem miSkecth = MenuItem("Sketch");
MenuItem miVerify = MenuItem("Verify",'V');

MenuItem miTools = MenuItem("Tools");
MenuItem miAutoformat = MenuItem("Autoformat");

MenuItem miHelp = MenuItem("Help");
MenuItem miEnvironment = MenuItem("Environment");

//this function builds the menu and connects the correct items together
void menuSetup()
{
Serial.println("Setting up menu...");
//add the file menu to the menu root
//when add is used, as opposed to addX as you see below, the item is added below the item it's added to
menu.getRoot().add(miFile);
//add all items below File to the file menu item,
//notice the arduino isp item is added to the right of the examples item
miFile.add(miNew).add(miOpen).add(miExamples).addRight(miArduinoISP);
//because edit item is to the right of the file item, we use the addRight method when inserting this item
//then we add the cut item, because it is below the edit
miFile.addRight(miEdit).add(miCut);
miEdit.addRight(miSkecth).add(miVerify);
miSkecth.addRight(miTools).add(miAutoformat);
miTools.addRight(miHelp).add(miEnvironment);
}

/*
This is an important function
Here all use events are handled

This is where you define a behaviour for a menu item
*/
void menuUseEvent(MenuUseEvent used)
{
Serial.print("Menu use ");
Serial.println(used.item.getName());
if (used.item == "ArduinoISP") //comparison using a string literal
{
Serial.println("menuUseEvent found ArduinoISP");
}
if (used.item == miVerify) //comparison agains a known item
{
Serial.println("menuUseEvent found Verify (V)");
}
}

/*
This is an important function
Here we get a notification whenever the user changes the menu
That is, when the menu is navigated
*/
void menuChangeEvent(MenuChangeEvent changed)
{
Serial.print("Menu change ");
Serial.print(changed.from.getName());
Serial.print(" ");
Serial.println(changed.to.getName());
}

void setup()
{
Serial.begin(9600);

menuSetup();
Serial.println("Starting navigation (see source for description):");

menu.moveDown(); //move to file
menu.moveDown(); //move to new
menu.moveDown(); //move to open
menu.moveDown(); //move to examples
menu.moveRight(); //move to arduinoisp
menu.use(); //use arduniisp
menu.moveLeft(); //move to examples
menu.moveUp(); //move to open
menu.moveUp(); //move to new
menu.moveUp(); //move to file
menu.moveRight(); //move to edit
menu.moveRight(); //move to sketch
menu.moveDown(); //move to verify
menu.use(); //use verify
menu.moveBack(); //move back to sketch
menu.moveBack(); //move back to edit
menu.moveBack(); //move back to file
menu.moveBack(); //move back to new
menu.use(); //use new

menu.use('V'); //use verify based on its shortkey 'V'
}

void loop()
{
//
}

I'd be glad if someone could test this and give me some feedback. I unfortunantly have no arduino available at the moment.

1 Like

Hi,
It works for me. I use RBBB clones.

Curious why you develop with just a .h file? You will throw the newbies for a loop.

I want to extend this to use a lcd and buttons, but I see no reason why it would not work.

Great job, thanks.

I see many advantages with useing just a .h file.

It is easier to share, it never becomes a problem that the .h version does not match the .cpp version.

When coding with templates or libraries with static methods/members you get far less linkage problems.

It is much faster to develop this way, and since I license it LGPL anyways I do not need to hide the implementation from the interface.

Legacy reason: Back in the days Arduino stored a .o for compiled classes. I then used only .h to avoid having to delete the .o in order to force a reompile.

The only problem I've encountered is that it can be slightly troublesome to separate classes that need each other. But such classes (if they're not an artifact of poor system design) are often better placed in the same .h anyway.

[edit]BTW: Thank you for taking the time to test this!
I'll be glad to know how your experience is like when you've made the menu and lcd link. If you have any suggestions or find anything hard to understand, do not hesitate to let me know.

I'll put this up on the playground sometime in the future. But if you see this post, and need assistance, please send me a mail og PM and I'll help you, and get to the playground site authoring. :)[/edit]

Once I have everything figured out and working, I will provide some pictures and info on using the menu with a LCD and Keypad.

One thing that I can think of is the ability to getUp(), getRight(), getDown(), and getLeft() without adjusting the current point so that you could theoretically traverse the menu tree in the background to find the surrounding items. For example on my LCD I have four lines and I would like to display the surrounding items if there are any.

i.e.

//User opens menu
----------------------------------------
|>Readings                             |
| Controls                             |
| Settings                             |
|                                      |
----------------------------------------

//User presses right twice
----------------------------------------
| Readings                             |
| Controls                             |
|>Settings                             |
|                                      |
----------------------------------------

//User presses down
----------------------------------------
|>Actions                              |
| Date & Time                          |
| Water Level                          |
|                                      |
----------------------------------------

//User presses down again
//User presses down
----------------------------------------
|>Add Action                           |
| Edit Action                          |
| Delete Action                        |
|                                      |
----------------------------------------

http://www.arduino.cc/playground/uploads/Profiles/MenuBackend_1-1.zip
You can now call getBefore, getAfter, getRight and getDown on any MenuItem. :slight_smile:

I think its poor form, but you wrote it, so ok. I would prefer .h/.cpp form myself.

I think dan9186 is on the right track, but I use 16x2 lcds myself...

I think its poor form, but you wrote it, so ok. I would prefer .h/.cpp form myself

Why? What's the benefit of splitting the declaration from the definition?

Ok now I guess my next question would be, how do I make use of that to get two levels to the right or something like that?

If I try something like

lcd.at(1,2,menu.getCurrent().getName());
lcd.at(2,2,menu.getCurrent().getRight()->getName());

I get exactly what I expected, the current menu item and the one to the right of it. Problem though is that if I do one more

lcd.at(3,2,menu.getCurrent().getRight().getRight()->getName());

I get the following error.

In function 'void loop()':
error: request for member 'getRight' in '((MenuItem*)menu.MenuBackend::getCurrent())->MenuItem::getRight()', which is of non-class type 'MenuItem*

There is one other issue that I've come up with as I plan this a little more. If I wanted to use the same scheme to display 4 items at a time, then using the .getRight() like that wont work. It will work just fine for when I am at the level just after the menu root, but if you wanted to list the items under one of the base menus then it would need to be .getAfter(). Which would mean you would either need some sorta way to tell if you are at the head of one of the menus or you would have to change the structure of the implementation. It would be my preference to show more of the menu than just the current item that you are on so that the user can have a better understanding of where they are in the menu structure. Do you have any suggestions for this or ideas? If it is just all way to complicated I can simply go with using one line to display the menu possibilities and some sort of character to designate when the menu item has children/siblings or not.

Thanks for your help and ideas.

Does this work...
lcd.at(3,2,menu.getCurrent().getRight()->getRight()->getName());

@Dan9186: What you want to print to the user is entirely up to you. You can easily check to see if there are any items above/right/below/left and print them accordingly. You could use getCurrent if you want your print to be relative to where the user is, or you could use the root, to have a static reference for printing.

I might write a MenuRendrer or something one day, but people want their menus printed differently so I'm guessing a good framework for that (that satisfies most users) is a lot of work, and takes a lot of time.

And, Coding Badly makes a good guess about that ->, all getX() returns pointers, this is so you can test if there are an item at that location, as the example demonstrates. :slight_smile:

Just thinking on it, what would be the disadvantages to having getCurrent() return a pointer as well? This would be soley for the purpose of having any item that you return from getX() will respond the same way. This way you could use the same code to display a name if you got a menu item from getCurrent() just like you would from getRight().

Done like this it currently gives an error since getCurrent() does not return the same as the other getX() does.

      MenuItem *miLine1 = menu.getCurrent();
      MenuItem *miLine2 = menu.getCurrent().getRight();
      MenuItem *miLine3 = menu.getCurrent().getRight()->getRight();
      MenuItem *miLine4 = menu.getCurrent().getRight()->getRight()->getRight();
      
      if(miLine1) lcd.at(1,2,miLine1->getName());
      if(miLine2) lcd.at(2,2,miLine2->getName());
      if(miLine3) lcd.at(3,2,miLine3->getName());
      if(miLine4) lcd.at(4,2,miLine4->getName());

I am trying to come up with some way that I can have a ">" as a current selection indicator and have it move instead of the rest of the menu. Any thoughts?

My other question is since I quite often get pointers confused, what exactly does the -> do there? I'm not sure that I really understand the syntax.

Also just thought about this. Two things that might be useful. One something like hasChildren() that returns true/false on any menu item, and two the option to setCurrent( MenuItem item ) so that you could setup an escape from the menu by calling setCurrent( menu.getRoot() ).

Oh yes Dan9186, those 2 functions would definitely be handy.
Sometimes you do want to jump back to the top menu or exit to somewhere else.

Looks like you guys are on it, so I should get to work on my 16x2 menus...

Cant use my Playground profile tonight, so here is:
Header: Pastebin.com - #1 paste tool since 2002!
Keywords: Pastebin.com - #1 paste tool since 2002!
Version 1.2 with hasChildren and setCurrent

The hasChildren can be tested as normal:

if (item.hasChildren())

Or you can do specific tests

if ( mi->hasChildren() & MenuItem::RIGHT) {/*yay! we have a child to the right!*/}

When using setCurrent, it will trigger an event if the item you set it to does not match the current.

menu.setCurrent( menu.getRoot() ); //will trigger a change if current!=root

I will keep the getRoot and getCurrent to returning references, because this is the methods most user will need. And, I do not want to introduce pointers (and the need to test for 0) to the beginners. Also, I like to guarantee that both the root, and the current returns a reference not a null, because of the reasons just stated. :slight_smile:

[edit]Thank you for useful feedback and suggestions![/edit]

Just tested it out and at first glance, moveLeft() and moveDown() don't seem to be working properly. I can't get them to move the current anywhere. On top of that moveRight() seems to be doing what moveDown() did previously. Did you change the structure of the directions or is this just a error from where you added in the changes to do hasChildren()?

The bug in 1.2 got introduced by my being stupid.
When elements are added I use a set method, and that takes a position parameter (above,right etc), but for version 1.2 I also added the constants MenuItem::ABOVE/RIGHT etc for checking against the hasChildren. I had to change the names of my internal representation of the directions in order to avoid naming conflicts, but forgot to change the identifying names in my switch/case in the set method.
Stupid, but easily fixable.

Could you test if this works:

if (menu.getCurrent().hasChildren() & (MenuItem::ABOVE | MenuItem::BELOW))
{
  Serial.println("The current item has either an element below or above");
}

Thank you for your contribution with this library!

I made a test last nite, and added some serial navigation (using 1.0).
So far I like it a lot. I will try to re-test with your latest tonite.

I did confirm to myself that jumping to the root, or top menus would be helpful. You definitely get a feel for this with the serial nav. Going down 4 steps, and seeing that to return you have to go up 4 steps.

Thanks.

I figured that was the case with the bug. I had compared the new and old code and saw that there was a difference is some of the internal variables.

Anyways, when I test what you asked I get this.

ERROR:
In function 'void loop()':
error: 'ABOVE' is not a member of 'MenuItem'

Thanks

Nice library, I will definitely test more and give you some feedback.

Feedback is awesome! I am insanely busy these days, but I regulary check this forum, and will try to make suggested changes when I have time.
Last two weeks of my bachelor degree study... one 50 page dissertation and some exams. :slight_smile: