0

I want to create menus with options that select other menus, without causing a stack overflow error due to recursion.

But the menus have to be initialized, and a menu a can point to a menu b and b to a. It's also important that some menus can have varying amounts of options.

public class Menus {
    public static Menu mainMenu = new Menu("Main Menu");
    public static Menu homeMenu = new Menu("Home Menu");
    public static Menu BMenu = new Menu("B Menu");

    public static void initMenus() {
        mainMenu.addOptions(
                new MenuOption(
                        "bmenu",
                        BMenu
                ),
                new MenuOption(
                        "exit",
                        mainMenu,
                        CLI::stop
                )
        );
        BMenu.addOptions(
                new MenuOption(
                        "main",
                        mainMenu
                ),
                new MenuOption(
                        "exit",
                        BMenu,
                        CLI::stop
                )
        );
    }
}
public class MenuOption {
    private final String name;
    private final Runnable[] actions;
    private final Menu nextMenu;
    public MenuOption(String name, Menu nextMenu, Runnable... actions) {
        this.name = name;
        this.actions = actions;
        this.nextMenu = nextMenu;
    }
    public MenuOption(String name, Menu nextMenu) {
        this.name = name;
        this.actions = new Runnable[0];
        this.nextMenu = nextMenu;
    }
    public void run() {
        for (Runnable r : actions) {
            r.run();
        }
    }
    public void run(int delay) {
        for (Runnable r : actions) {
            r.run();
            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }

    public String getName() {
        return name;
    }

    public Menu getNextMenu() {
        return nextMenu;
    }
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Menu {
    private final String title;
    private final List<MenuOption> options;
    public Menu(String title, List<MenuOption> options) {
        this.title = title;
        this.options = options;
    }
    public Menu(String title, MenuOption... options) {
        this.title = title;
        this.options = new ArrayList<>(Arrays.asList(options));
    }

    public String getTitle() {
        return title;
    }

    public List<MenuOption> getOptions() {
        return options;
    }
    public Menu select(int index) {
        if (index > 0 && index <= options.size()) {
            options.get(index-1).run();
            return options.get(index-1).getNextMenu();
        }
        return this;
    }
    public void addOption(MenuOption option) {
        options.add(option);
    }
    public void addOptions(MenuOption... options) {
        for (MenuOption option : options) {
            addOption(option);
        }
    }
}
import java.util.Arrays;
import java.util.Scanner;

public class CLI {
    private static Menu currentState = Menus.mainMenu;
    private static volatile boolean running = true;
    public static void run() {
        Scanner scanner = new Scanner(System.in);
        Menus.initMenus();
        while (running) {
            clearAndDisplay();
            currentState = currentState.select(scanner.nextInt());
        }
    }
    public static String generateMenu(String Title, String... options ) {
        StringBuilder menu = new StringBuilder();
        menu.append(Title).append("\n");
        for (int option = 1; option <= options.length; option++) {
            menu.append(option).append(". ").append(options[option-1]).append("\n");
        }
        return menu.toString();
    }

    public static String generateMenu(Menu state) {
        return generateMenu(state.getTitle(),
                state.getOptions().stream()
                        .map(MenuOption::getName)
                        .toArray(String[]::new)
        );
    }

    public static void clear() {
        System.out.print("\033[2J");
        System.out.print("\033[1;1H");
        System.out.flush();
    }
    public static void clearAndDisplay() {
        clear();
        System.out.print(generateMenu(currentState));
    }
    public static void stop() {
        running = false;
    }
}

Currently, they are assigned to values, if they don't exist, and if they do, return the value. But first menu can't be created, because the other one is null, and the second one can't be created, because the first one is null. I was thinking, I could maybe use a placeholder, leaving the problematic option empty, creating the menus and then assigning the option, but I'm not sure how I would go about it.

Is there a much easier way to create this concept that I'm not seeing?

I tried to figure out a way to get both menus initialized, but I can't figure this out.

EDIT: I managed to fix the problem, but I'm not confident that this implementation is done well, please let me know if you see an issue.

14
  • 3
    Please post a minimally reproducible sample. Please include the source code of the MenuOption class. Commented May 24 at 16:10
  • 1
    Menu is a Java AWT class. How is this also a command line interface? Commented May 24 at 16:14
  • What issues are you having trying to leave the MenuOption "empty" or with a null value, then setting the correct value after the objects have been instantiated? Are setters not available? Commented May 24 at 16:16
  • Sorry, I added the relevant classes. The menus will be printed on the terminal, and read scanner system.in int input to select option, then the current menu will be changed and any action associated with the option will be taken. It's not AWT, it's for a uni project and we're not allowed to use gui. I managed to fix the issue so far, but if you're seeing any other issues please let me know. Commented May 24 at 16:24
  • 1
    I dont see why you need a volatile variable. Everything you are doing seems to be done on the same thread, and you arent doing any multithreading Commented May 24 at 16:31

1 Answer 1

1

I've implemented a different method because, due to my inability, I can't find a way to modify your code and make it work.

First observation: when using a menu, selecting an item calls a method, making it very difficult to add items to it, which is why I'm disregarding this option.

First we create an enum with the names of the actions:

public enum Item {
   OPEN,
   CLOSE,
   EXIT,
   CLEAR,
   ETC, 
   SELECT,
   DELETE,
   CHANGE, 
   UP
}

This interface is used to group objects of the Menu class and the Action, since a menu can have both a Menu and an Action as an item.

public interface SubList {
   public void action();
   public String getName();
   public int getLen();   
}

The Action class does little more than implement the interface methods, within the contructor, if it has not already been created, it creates the Select object and instantiates action.


public class Action implements SubList {

   static Select select;
   Item action;
   
   public Action( Item act ) {
      action = act;
      if( select == null ) select = new Select();
   }
 
   @Override
   public void action() {
      select.selection( action );
   }

   @Override
   public String getName() {
      return action.toString().toLowerCase();
   }

   @Override
   public int getLen() {
      return 1;
   }
}

The Menu class has three attributes: an array of SubList items, an array of String with their names, and the name itself. All of these are instantiated in the constructor. It implements the interface methods and adds getSubItem() to retrieve a specific item from the list.

public class Menu implements SubList {

   SubList[] list;
   String names[];
   String name;
   
   public Menu( SubList sub[], String nams[], String nam ) {
      names = nams;
      list = sub;
      name = nam; 
   } 

   public SubList getSubItem( int position ) {
      return list[ position ];
   }

   @Override
   public void action() {
      System.out.println( "Chose your option: " );
      for( int i = 0; i < list.length; i++ ) {
         System.out.println((i + 1) + " -> " + list[ i ].getName() );
      }
   }

   @Override
   public String getName() {
      return name;
   }

   @Override
   public int getLen() {
      return names.length;
   }
}

The Select class, whose existence is due to my "mania" of having many small classes instead of few large ones, has the selection method, which receives an Item and consequently calls the corresponding method.

public class Select {

   public String selection( Item name ) {
      switch( name ) {
         case OPEN:
            return open();
         case CLEAR:
            return clear();
         case EXIT:
            return "exit";

         ... // others
         default:
            throw new AssertionError();
      }
   }

   boolean up() {
      System.out.println( "-> up" );
      return false;
   }

   boolean open() {
      System.out.println( "-> open" );
      return false;
   }

     // innecesary
     // boolean exit() {}

   ... // others
}

Finally, the Main has only two attributes, the Scanner object and a boolean, which are shared by all the show methods (I say "all the methods" because we are doing recursion), which allows us to finish the execution of all of them.

public class Main {

   Scanner sca = new Scanner( System.in );
   boolean toBeContinued = true;

   void init() {
      String namesMenu[] = { "Options", "Archive", "exit" };
      String namesSubMenuA[] = { "close", "open", "exit" };
      String namesSubMenuB[] = { "options", "clear", "etc", "exit" };
      String namesSubMenuC[] = { "select", "delete", "change", "exit" };
      Action[] acts = new Action[]{ new Action( CLOSE ), 
         new Action( OPEN ), new Action( CLEAR ), new Action( ETC ), 
         new Action( EXIT ), new Action( SELECT ), new Action( CHANGE ), 
         new Action( DELETE ), new Action( UP ) };

      SubList[] subOptions = { acts[ 5 ], acts[ 7 ], acts[ 6 ], acts[ 8 ], acts[ 4 ] };
      Menu opt = new Menu(
              subOptions,
              namesSubMenuB,
              "subOptions" );
      SubList[] actions = { acts[ 0 ], acts[ 1 ], acts[ 8 ], acts[ 4 ] };
      SubList[] options = { opt, acts[ 2 ], acts[ 3 ], acts[ 8 ], acts[ 4 ] };

      Menu menu = new Menu(
                      new SubList[]{
                         new Menu(
                                 actions,
                                 namesSubMenuA,
                                 namesMenu[ 0 ] ),
                         new Menu(
                                 options,
                                 namesSubMenuB,
                                 namesMenu[ 1 ] ),
                         new Menu(
                                 new SubList[]{
                                    acts[ 4 ] },
                                 new String[]{ "exit" },
                                 namesMenu[ 2 ] )
                      },
                      namesMenu,
                      "Menu Principal"
      );
      show( menu );
   }

   void show( Menu menu ) {
      boolean localContinue = true;
      int option;
      while( toBeContinued && localContinue ) {
         menu.action();
         option = sca.nextInt() - 1;
         SubList aux = menu.getSubItem( option );

         if( aux.getLen() == 1 ) {
            Action aux2;
            if( aux instanceof Menu ) {
               aux2 = (Action) ( (Menu) aux ).getSubItem( 0 );
            }
            else {
               aux2 = ( (Action) aux );
            }
            if( aux2.getName().equals( "exit" ) ) {
               toBeContinued = false;
            }
            if( aux2.getName().equals( "up" ) ) {
               localContinue = false;
            }
            aux2.action();
         }
         else {
            if( aux instanceof Action ) {
               ( (Action) aux ).action();
            }
            else {
               show( (Menu) aux );
            }
         }
      }
   }

   public static void main( String[] args ) {
      new Main().start();
   }
}

The init method first creates the arrays with the names of the items, then creates another array, which contains all the Item objects that exist, that way, we reuse them, instead of creating the menu object directly, I preferred to create the Menu opt that will end up being a submenu, and the different SubList that will be part of the different Menu (I think it's much clearer this way, at the cost of writing more code).

The show method instantiates localContinue as "true", to control the execution of each instance of it, within the while, we call the action of the received object, which will show us the list of options, we instantiate option with the user's response, and we instantiate aux with the corresponding item.

If the size of aux is "1", we instantiate aux2, determining if it is an object of the Menu or Action class, then we check if the Action name is "exit" we pass toBeContinued to "false" so that the execution ends, and if it is "up" we pass localContinued to false so that the current method ends.

Finally, if we do not enter the first if, we check if aux is an instance of Action, in which case we call its action() method, and if not, we invoke show passing it aux as a parameter.

PS: I'm sure this implementation can be improved, I just hope it helps you get ideas and build your own solution.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.