20x4 LCD Menu with Rotary Encoder

Hi guys!
Recently, I've been trying to develop a menu for a 20x4 LCD Display using a Rotary Encoder.
Until now I've managed to have menu displayed, but I wish to extend it an have sub-menus as well.
By now I can only scroll through the main menu code, but not also through the sub-menu.
I think I know where the problem is.
When getting into a sub-menu, if the rotary is turned than the interrupt is trigered and the move_Up or move_Down function is called and therefore the main_menu is printed.

Does anyone have any idea, how could I use the rotary rotation and selection options within the sub-menus as well.

Thanks!

This is the code that i've done by now:

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

#define PINA 2
#define PINB 3
#define PUSHB 4


volatile boolean turned;   // rotary was turned
volatile boolean fired;    // knob was pushed
volatile boolean up;  // true when turned cw

int CursorLine = 0;
int DisplayFirstLine = 0;

char* MenueLine[] = {" Option 1"," Option 2"," Option 3"," Option 4"," Option 5"};
char* One[] = {" One-1"," One-2"," One-3"," One-4"," One-5"};
char* Two[] = {" Two-1"," Two-2"," Two-3"," Two-4"," Two-5"};
char* Three[] = {" Three-1"," Three-2"," Three-3"," Three-4"," Three-5"};

int MenueItems;


// Variable for the button's current state.
// button_mode = 1 when the button is up, and 0 when the button is pressed.
// This variable is 'static' and persists because it is declared outside of a function.
int button_mode = 1;


// Interrupt Service Routine for a change to encoder pin A
void isr ()
{
  if (digitalRead (PINA))
    up = digitalRead (PINB);
  else
    up = !digitalRead (PINB);
  turned = true;
}  // end of isr


void setup ()
{
  digitalWrite (PINA, HIGH);     // enable pull-ups
  digitalWrite (PINB, HIGH);
  digitalWrite (PUSHP, HIGH);  

  attachInterrupt(3, isr, CHANGE);
  
  lcd.begin (20,4);

}  // end of setup

void loop ()
{
  
  if (turned)
    {
   if (up)
      move_up();
    else
      move_down();
    turned = false;
    }

   clickPin();
    
}  // end of loop

void clickPin()
{
  
if ((digitalRead(PUSHB) == LOW) &&   (button_mode == 1))
{
 // Button was up before, but is pressed now. Set the button to pressed
button_mode = 0; // Button is pressed.
  selection();
  fired = false;
}
else if ((digitalRead(PUSHB) == HIGH) && (button_mode == 0))
  {
 // Button was down before, but is released now. Set the button to released.
 button_mode = 1; 
  }
}
 
void print_menu() 
{
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("   Main Menu  ");
  for (int i=1;i<4;i++) 
  {
    lcd.setCursor(0,i);
    lcd.print(MenueLine[DisplayFirstLine + i]);
  }
  lcd.setCursor(0,(CursorLine-DisplayFirstLine)+1);
}

void print_menu1() 
{
  lcd.clear();
  for (int i=1;i<4;i++) 
  {
    lcd.setCursor(0,i);
    lcd.print(One[DisplayFirstLine + i]);
  }
  lcd.setCursor(0,(CursorLine-DisplayFirstLine));
}

void print_menu2() 
{
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Menu option 2");
  for (int i=1;i<4;i++) 
  {
    lcd.setCursor(0,i);
    lcd.print(Two[DisplayFirstLine + i]);
  }
  lcd.setCursor(0,(CursorLine-DisplayFirstLine));
}

void print_menu3() 
{
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Menu option 3");
  for (int i=1;i<4;i++) 
  {
    lcd.setCursor(0,i);
    lcd.print(Three[DisplayFirstLine + i]);
  }
  lcd.setCursor(0,(CursorLine-DisplayFirstLine));
}

void move_down() 
{
  if (CursorLine == (DisplayFirstLine+4-1)) 
  {
    DisplayFirstLine++;
  }
  if (CursorLine == (MenueItems-1)) 
  {
    CursorLine = 0;
    DisplayFirstLine = 0;
  }
  else 
  {
    CursorLine=CursorLine+1;
  }
  print_menu();
}

void move_up() 
{
  if ((DisplayFirstLine == 0) & (CursorLine == 0)) {
    DisplayFirstLine = MenueItems-4;  
  }
  else if (DisplayFirstLine == CursorLine) 
  {
    DisplayFirstLine--;
  }
  if (CursorLine == 0) 
  {
    CursorLine = MenueItems-1;
  }
  else 
  {
    CursorLine=CursorLine-1;
  }
  print_menu();
}

void selection() 
{

  lcd.clear();
  lcd.print("MY MENU:");
  lcd.setCursor(0,1);
  lcd.print("You selected:");
  lcd.setCursor(0,2);
  lcd.print(MenueLine[CursorLine]);
  delay(2000);
  lcd.clear();
  
  switch (CursorLine) 
  {
    case 0:
      First();
      break;
    case 1:
      Second();
      break;
    case 2:
      Third();
      break;
    case 3;
      break;  
    default:
      break;
  }
}


void First()
{
  lcd.setCursor(0,0);
  lcd.print("You are in menu:");
  lcd.setCursor(0,1);
  lcd.print("ONE");
  print_menu1();
}

void Second()
{
  lcd.setCursor(0,0);
  lcd.print("You are in menu:");
  lcd.setCursor(0,1);
  lcd.print("TWO");
  print_menu2();

}

void Third()
{
  lcd.setCursor(0,0);
  lcd.print("You are in menu:");
  lcd.setCursor(0,1);
  lcd.print("THREE");
  print_menu3();
}

This site uses a rotary control to change the screen values - might help - i have built his project and the code / hardware does work ok.
http://www.ad7c.com/projects/ad9850-dds-vfo/

Here is one idea using existing data setup.
The idea is to keep the main menu on first line and submenus bellow it.
Needs work to clean things up ( scrolling) and replace fixed submenu with variable / function.

Are you going to use the menu "one shot style" or change it when your program is running?
You probably need to add the main menu selection to the end of the submenu so you can get back to main menu.
Of course the indexes i and j need to be accessible from the rest of the code to be useful.
Or change the whole approach using pointers.

Have fun scrolling the display.

void Menu(void)
{
lcd_i2c.clear();
for (int i = 0; i < 5; i++)
{
lcd_i2c.setCursor(0, i); main menu
lcd_i2c.print(MenueLine*);*

  • delay(1000);*
  • for (int j = 0; j < 5; j++)*
  • {*
  • lcd_i2c.setCursor(1, j + 1); submenu offset*
  • lcd_i2c.print(One[j]);*
  • delay(1000);*
  • }*
  • delay(3000);*
  • }*
    }

Not sure why the first code is so messed up. So here is the second copy.

void Menu(void)
{
  lcd_i2c.clear();
  for (int i = 0; i < 5; i++)
  {
    lcd_i2c.setCursor(0, i);
    lcd_i2c.print(MenueLine[i]);
    delay(1000);

    for (int j = 0; j < 5; j++)
    {
      lcd_i2c.setCursor(1, j + 1);
      lcd_i2c.print(One[j]);
      delay(1000);
    }
    delay(3000);
  }

Hi, thanks for the reply.
I get what you are saying, but it won't work in the context that I'm trying to make the Menu work.
Based on the code you added, it will just print the sub-menu after it has printed out the main menu.

What I want to do, is get to the main menu. Select the click-button and enter in one of the option.
While on the option selected, scroll through the sub-menu as well and select one of the members of that sub-menu.

The issue I am having lies on the move_up and move_down functions I guess.
I am able to scroll through and select from the Main Menu, but I can't do so when I am in the sub-menu.
Whenever in the sub-menu, when scrolling the rotary encoder, the interrupt trigers me to go to the main menu again.

I can't seem to figure out, how to scroll through the sub-menu options and select one of them and then when selecting display something.
It seems, I need to change something in the interrupt routine or in when the interrupt routine is trigered since I need it to be triggered on different hierarchies of the Menu.

Thanks!

Memedhe:
I get what you are saying, but it won't work in the context that I'm trying to make the Menu work.

I see topics like yours about "rotary encoder menu" popping up in this forum about once per month. The last one was posted in Very basic menu. - Project Guidance - Arduino Forum

Unfortunately, everyone who asks seems to have slightly different usage intentions as well as different feature requests.

And unfortunately, I cannot remember that a suitable solution has been posted to any of the requests, that is easy to use for people without programming knowledge.

I'm currently thinking about it and am experimenting with some code snippets. The problem is, that you cannot solve your problem with such an easy code as you are trying. Handling a menu is not a task for 'sequential control' but for a 'finite state machine'.

I'm trying to seperate 'input' (from rotary encoder) and 'output' (to LCD), so that different types of LCDs could be used. And I'm trying to create an user interface that is easily usable and consists of two parts:

  • an array as a data structure that describes the menu
  • an user written function that handles the menu actions, mainly output on LCD

If if find a solution, I'll post it here.

@jurs Thank you for your response and your insight :slight_smile:

You are definitely right regarding the issue of different requirements for the LCD Menus and I hope you'll be able to come up with a general solution that could be used by all users.

Personally, I've tried using different approaches, different libraries as well.
One of the common libraries that I have encountered is the Backend Menu Library but it does not seem to work in the ''general sense''.
It is more appropriate I think for 16x2 LCD Displays that are controlled by push-buttons.

Thanks!

By now, I have been able to build up two programmes seperately.
One that records the rotation of the Rotary Encoder and increments/decrements a value based on the rotation of the Encoder.
Also,a very helpful code that makes use of the Rotary Encoder Switch to perform four actions:
Single-click; Double-click; Hold and Long-Hold.

So, by using these it could be possible to manipulate through the menus. Like, select a menu option with a single click or exit a sub-menu with a double-click or long-hold.

I'm hoping to make the LCD look something like this:

============ ================
Main Menu Option 1

Option 1 Sub-Option 1
Option 2 => Select with a single-click => > Sub-Option 2
Option 3 Sub-Option 3
============ ================

Have another layer of the sub-sub-option:
============= ====================
Sub-Option 2 "You selected: "

Sub-Sub-Option1 =>Select the Sub-sub-option1=> " Sub-sub-Option1"
Sub-Sub-Option2
Sub-sub-Option3
=============== ====================

Memedhe:
Personally, I've tried using different approaches, different libraries as well.
One of the common libraries that I have encountered is the Backend Menu Library but it does not seem to work in the ''general sense''.
It is more appropriate I think for 16x2 LCD Displays that are controlled by push-buttons.

I think I have seen that "Backend Menu Library" before, but it didn't convince me.

The user interface is too complicated: User has to define a data structure AND as special setup function to prepare the menu and the menu seems to have no support for user edited input. My approach will be much different and I will have an eye on user input, so that the rotary encoder not only will enable menu navigation, but also enable user input to variables. Such like changing configuration values. Or setting date and time in an RTC sketch.

But I'm currently just in some preliminary tests of code snippets and have nothing ready working yet except of snippets like reading a rotary encoder, reading a button, data structure for menu, executing callback functions and several things more. Needs some additional work before I can post something.

Thanks for the effort :slight_smile:
I have come to understand that building such a menu is not an easy thing, at least not for me. I've been trying.

As regarding the Backend Menu, the thing is I don't really understand how it really works. Seems not so user friendly. For someone with not a lot of experience in programming, it's not very appropriate.

One interesting example I have come accross, is this one:

You can find the code here:
https://b1954a81d64a6708574fc19ceb638c128f3998f4.googledrive.com/host/0B8Ql_9-ygoisczg4STFicFZPWEE/RotaryEncoderOptions.html

I hope it might be of any help. Although, even for this one I think there's too many things in one place.

Regards :slight_smile:

Memedhe:
Hi, thanks for the reply.
I get what you are saying, but it won't work in the context that I'm trying to make the Menu work.
Based on the code you added, it will just print the sub-menu after it has printed out the main menu.

What I want to do, is get to the main menu. Select the click-button and enter in one of the option.
While on the option selected, scroll through the sub-menu as well and select one of the members of that sub-menu.

The issue I am having lies on the move_up and move_down functions I guess.
I am able to scroll through and select from the Main Menu, but I can't do so when I am in the sub-menu.
Whenever in the sub-menu, when scrolling the rotary encoder, the interrupt trigers me to go to the main menu again.

This is where you just need to add another layer to switch to using the state machine process.

I can't seem to figure out, how to scroll through the sub-menu options and select one of them and then when selecting display something.
It seems, I need to change something in the interrupt routine or in when the interrupt routine is trigered since I need it to be triggered on different hierarchies of the Menu.

Thanks!

OK, the post was just to get an idea how to select the menu / submenu.

Let me explain in some details.

The way I did this in my project was to select the main menu by turning the encoder (left) to select from the main menu and turning the encoder again (right) to select the submenu.
That is why I prefer indexes instead of pointers - just count the interrupt / turns to select the correct item.
In theory - use encoder button to clear the counter (initialize in setup) , turn the encoder to select the main menu and switch state , either use button again to clear the counter or just clear it after a timeout, or by turning the encoder in opposite direction and than select the submenu using (same) counter.

( I am assuming your encoder outputs Gray code which has "direction" build-in and there are sample codes available )

Pretty much state machine process:

switch (state )
case 0:
select menu index i
set state to 1
case 1:
select submenu index j
set state to 2
case 2:
selection OK do something

....

Addendum
Just noticed few thing in your code
You do not "Set" pullup", just initialize mode

pinMode(PINA, INPUT_PULLUP);

Interrupts (on Uno) are numbered 0 and 1 not pin designated

so attachInterrupt(3, isr, CHANGE); should be

attachInterrupt(0, isr, CHANGE);

but on Uno for sure !

PS I am looking for my old Gray code project.

Today I was working on the menu and started from beginning.
The idea of using a state machine came up to mind.
I tried implementing it. It worked to just select an option from a menu, when it came to a sub-menu and scrolling through it I tried to use the same variable as a state machine.
If I may be more clear, I used the rotary encoder count in a switch case. And this way, scroll through the menu.
If the rottary switch was pressed while rotary count has a value of 1,2 or 3 then select that Option in the Menu.

When selecting a menu option, use the rottary count in a switch case again for the sub-menu and apply the same logic for a sub-menu. Although, I am not sure this might be a very smart solution.
I did try the code and it wasn't working as expected, so I should definitely spend more time on it.

I don't know how clear I was in my description. If not clear enough, I apologize.

One more thing, you mentioned that by rotating the rottary encoder to the right you select a sub-menu.
Why not, use rotating to scroll up and down and use the rottary button select a sub-menu with a single-click and exit a sub-menu with a double-click or a long-hold?

example for the state machine:

// just to get a clearer idea to what I said above

switch(rotary_count)
{
   case 1:
  if(button_pressed)
  {
   //enter the menu option
   // print out the sub-menu of the menu option
     switch(rotary_count)
     {  case 1:
         if(button_pressed)
      { // enter in the sub-menu option
        // print the sub-sub-menu
      }
        else
      { //print the first option's sub-menu
        //scroll down the sub-menu
       }
       case 2:
         if(button_pressed)
      { // enter in the sub-menu 2nd option
        // print the sub-sub-menu
      }
        else
      { //print the first option's sub-menu
         //scroll down the sub-menu
       } 
     } 
 else
{
 //print the main menu
// and scroll down for one step
}
break;
case 2:
  if(button_pressed)
  {
   //enter the 2nd menu option
   // print out the 2nd sub-menu of the menu option
   }
 else
{
 //print the main menu
// and scroll down for one step further
}
break;
.
.
.
}

I am sure you can come up with different ways to do this.
Combining the interrupt count as a selector is a good idea,
I was just suggesting to update counts first than select the menu.
It may get convoluted if you do more than 2 levels of menus.

You may want to revise your isr too
On first glance it seems that all you need is to update "state" / counter and evaluate it against previous state to come up with direction either in isr or elsewhere else.

I still do not know if you are using / getting Gray code from the encoder,
but since you trigger the interrupt on one input only I assume not.

There was another non Gray code discussion here and we never did came to any conclusion on how to detect encoder direction if you trigger ( dynamic ) on one input and than read immediately (static) the other input. Especially if you trigger on CHANGE. But you may not need to know the direction if you use the button.

My encoder is on Uno ( not currently hooked up) and I may try to write some kind of emulation on Due to see how it MAY detect the direction.

Whatever you decide keep at it in "single steps" action code - specific menu process and verify each code block.

It is easy to consolidate / put into common /function code later.
Good luck.

Here is another shot at it.

You can get the iCount from the encoder as you suggested and use iState to make menu / submenu selection.
You can output to the LCD as you wish, I just cleared it and print only the selection for test purposes.
I found the easiest is to use "flash" to indicate selection - flashes all points in character after the text.
Kinda of cheesy.
I would suggest to make the submenu as two dimensional array - the first index would correspond to main menu. MenuMulti [][] ....

If you go to another sub menu you need to watch the RAM usage, there are ways to save constants elsewhere. But let's make the main / submenu work first.

void Menu(void)
{
  int iState = 0;
  int iCount = 0;
  int i, j, k;
  //initialize
  lcd_i2c.clear();
  // main menu
  for ( i = 0; i < 5; i++)
  {
    lcd_i2c.setCursor(0, i);
    lcd_i2c.print(MenueLine[i]);
    delay(1000);
  }

  do
  {
    iCount = random(0, 5); // select menu at random
    switch (iState)
    {
      case 0: // main menu
        {
          // test clear all
          lcd_i2c.clear();
          // show selection only
          lcd_i2c.setCursor(0, iCount);
          lcd_i2c.print(MenueLine[iCount]);
          delay(2000);

          // test clear all
          lcd_i2c.clear();
          // move selection to first line
          lcd_i2c.setCursor(0, 0);
          lcd_i2c.print(MenueLine[iCount]);
          delay(2000);
          //switch to submenu
          iState++;
          break;
        }
      case 1: // sub menu menu
        {
          // test clear all
          lcd_i2c.clear();
          delay(2000);

          // show menu again
          lcd_i2c.setCursor(0, 0);
          lcd_i2c.print(MenueLine[iCount]);
          delay(2000);

          // show all submenus
          for ( j = 0; j < 5; j++)
          {
            lcd_i2c.setCursor(2, j + 1);        // submenu offset
            lcd_i2c.print(One[j]);                // replace with MenuMulti [i][j] 
            delay(1000);
          }

          // test clear all
          lcd_i2c.clear();
          delay(2000);

          // print selected submenu only
          lcd_i2c.setCursor(2, iCount + 1);       // submenu offset
          lcd_i2c.print(One[iCount]);
          delay(2000);


          // switch state
          iState++;


          break;

        }
      case 2: // sub sub  menu
        {
          break;

        }


      default:
        // invalid state
        break;
    }
  } while (1);

  return;
}

Hi again!

Regarding the issue of the interrupt service routine there's this nice piece of code written by @NickGammon:
http://forum.arduino.cc/index.php?topic=62026.0

I think I forgot to also mention that I am working with an Arduino Mega 2560 which in comparison to Arduino Uno has a lot more memory space. So, even that the program keeps getting bigger I hope I won't have to worry about memory saving.

Thanks for the piece of code, I shall look through it and try to adapt it as best I can :slight_smile:
I really hope I'll be able to come up with something useful and when I do, I will make sure to share it here :slight_smile:

Here is another run at it.
So far it emulates the encoder turns on main menu only.
It runs until DONE prints, but from there it is "under construction"!
I did not check the delays , they are basically there so the prints are traceable.
I really think it would be cool to have "safe cracker" method to select the menu.
Turn (left) to select the main and than turn (right) to select the submenu.
But you do what you want.

PS I think I am having way too much fun with this and I need to get back to my USB project.

#define EMULATE

void Menu(void)
{
  int iState = 0;
  int iCount = 0;
  int i, j, k;
  int iEmulateTurns;
  bool bEncoderButton = true;          // active ground

  long lStartTimer, lTimeout = 5000;   // wait 5 seconds after last turn
  //initialize
  lcd_i2c.clear();
  // main menu
  for ( i = 0; i < 5; i++)
  {
    lcd_i2c.setCursor(0, i);
    lcd_i2c.print(MenueLine[i]);
    //delay(1000);
  }
  delay(1000);
  do
  {

#ifdef EMULATE
    iEmulateTurns = random(0, 4); // temp max is 4 so its shows without scrolling
    iCount = 0;              // start timer at 1 turn but the array is zero numbered
#endif


    // emulate encoder timer
    lStartTimer = millis();

    switch (iState)
    {
      case 0: // main menu
        {
          // test clear all
          lcd_i2c.clear();

          // temporary iCount - 1 because it always start at 4 - last line
          //iCount--;

          // emulate turns
          iCount = 0;
          do
          {
            lcd_i2c.clear();
            for (int i = 0; i < 5; i++)
            {
              if (i != iCount)
              {
                lcd_i2c.setCursor(0, i);
                lcd_i2c.print(MenueLine[i]);
                //delay(2000);
              }
              else
              {

indicate selection 

                lcd_i2c.setCursor(0, iCount);
                //lcd_i2c.blink();          // blink the  last line printed
                lcd_i2c.print("      *");  //shift with spaces
                // lcd_i2c.scrollDisplayRight();
                lcd_i2c.print(MenueLine[iCount]);
                //delay(2000);
              }
            }

            delay(2000);

          } while ((iCount++ != iEmulateTurns & ((millis() - lStartTimer) < lTimeout)) & bEncoderButton    ); // read the button !
          lcd_i2c.clear();
          lcd_i2c.print("DONE");

          for (;;);




          lcd_i2c.clear();
          for (int i = 0; i < 5; i++)
          {
            if (i == iCount)
            {
              // clears wholer display - bummer
              lcd_i2c.setCursor(0, iCount);
              //lcd_i2c.flash();
              lcd_i2c.scrollDisplayRight();
              lcd_i2c.print(MenueLine[iCount]);
              delay(2000);
            }
            else
            {
              lcd_i2c.setCursor(0, iCount);
              lcd_i2c.print(MenueLine[iCount]);
              delay(2000);


            }
          }




          for (;;);

          // test clear all
          lcd_i2c.clear();
          // move selection to first line
          lcd_i2c.setCursor(0, 0);
          lcd_i2c.print(MenueLine[iCount]);
          delay(2000);
          //switch to submenu
          iState++;
          break;
        }
      case 1: // sub menu menu
        {
          // test clear all
          lcd_i2c.clear();
          delay(2000);

          // show menu again
          lcd_i2c.setCursor(0, 0);
          lcd_i2c.print(MenueLine[iCount]);
          delay(2000);

          // show all submenus
          for ( j = 0; j < 5; j++)
          {
            lcd_i2c.setCursor(2, j + 1);        // submenu offset
            lcd_i2c.print(One[j]);
            delay(1000);
          }

          // test clear all
          lcd_i2c.clear();
          delay(2000);

          // print selected submenu only
          lcd_i2c.setCursor(2, iCount + 1);       // submenu offset
          lcd_i2c.print(One[iCount]);
          delay(2000);


          // switch state
          iState++;


          break;

        }
      case 2: // sub sub  menu
        {
          break;

        }


      default:
        // invalid state
        break;
    }
  } while (1);





  return;
}

I got the basic select menu and submenu working.
I am using two dimensional array for submenu.
It is getting too big to post here ( I comment my code as much as possible and some folks here do not like that ) so if you are still interested send me a PM.

Hi, in the first piece of code that I have posted I was able to scroll through and select the options of the Main Menu. Also, when selecting an option of the Main Menu display the options of the sub-menus.
As said before, triggering interrupts in this level seems an issue.
So, now I am trying to change the approach and take into account the advices you have given :slight_smile:

I'm looking forward to seeing how you've resolved the issue :slight_smile:
I'll be glad to try it and give feedback if it working for me as well.

Well I got the scrolling menus working, but just realized I may be taking all the fun of this project away from you.
So let me know when to stop, OK?
I did take a brief look on the "single interrupt pin" method, but I really think you do not need directional counter. But I subscribe to KISS method, so if it works for you, use it.
If you going to use states , and if you planning to to have more then two levels of menus it would be simpler that way , you still need a way to correlate number of turns with correct state.
I think that if you start with 0 count, than turn (left) to select main menu and than if you turn (right) and start subtracting the count it won't do much without some convoluted calculations.
After all you want to do just another selection (count) irregardless of which direction is the encoder turning.
Just saying...

Hmm, not sure if I completely got the "KISS" method part?!
Regarding the menus, would it maybe be simple to enter in the main menu when the program is executed and then the cursor to follow the count, so if count 1 then setCursor(0,1), if count 2 then setCursor(0,2) and so on.

Then, when selectin a menu option...to have the count reset and do the same operation for the sub-menu.
And work this way for another menu level.

What do you think?
Not sure how easy or hard that will be to do, but it's worth a try :slight_smile: