Read this this post; there are numerous posts like that: a newbie struggling with some sort of menu implementation. They want nested menus of course and an option to quit the entire thing. Of course they tie their 'business rules' tight together with their (feeble) attempts of their individual menu items and get heavily entangled in complicated control structures.
The post (see the link above) made me write this little article. Of course it is spoonfeeding because the code in this article can be used at will and saves all the new programmers from re-inventing the wheel again because I just did that in this article. The weather is sunny over here, I'm sitting in my back garden and it's Sunday so I have a few hours off; here goes:
The Design
I want to implement a simple text base menu system; menus can be part of another menu (or more of them). Selecting an option (a 'menu item') either opens up another menu or it activates some code. When that code has finished the same menu will be displayed again. A user must be able to navigate 'upwards' again, i.e. the menu that contains the current menu as an item will be displayed again. A user must also be able to quit the entire thing.
When you think of it: it doesn't make sense to navigate upwards when the current menu is the top menu; we'll make that navigation optional. Also we don't want the user to be able to quit everything in every single menu; we'll make that an optional feature also.
Because this is a simple text based menu system we'll display the options as simple Strings of text on the console prepended by a number like this:
Menu title:
0: first option
1: second option
2: third option
...
And we prompt the user to type 0, 1, 2, ... upto n-1 where there are n options in the menu. Of course an incorrect input deserves a warning and a retry. The 'quit' and 'back' options will be the last options (if present) in any menu.
A TextMenuItem
What exaclty is a menu item? It is represented by a single line of text and a piece of code that has to be executed when the item is selected. That's easy enough; here is the TextMenuItem class:
Expand|Select|Wrap|Line Numbers
- package textmenu;
- public class TextMenuItem implements Runnable {
- private String title;
- private Runnable exec;
- protected TextMenuItem(String title) { this(title, null); }
- public TextMenuItem(String title, Runnable exec) {
- this.title= title;
- this.exec= exec;
- }
- public String getTitle() { return title; }
- public boolean isExec() { return exec != null; }
- protected void setExec(Runnable exec) { this.exec= exec; }
- public void run() {
- try {
- exec.run();
- }
- catch (Throwable t) {
- t.printStackTrace(System.err);
- }
- }
- }
I decided to model that piece of code as a Runnable. See the API documentation for all the details of that interface. You can find a link to that documentation in the first post of the 'Answers' section of the Java forum. It is strongly advised to download the documentation and keep it stored on your local harddisk somewhere, you'll need it more often than not.
Back to that class: it has a single 'getter' for the title and a 'setter' for the executable piece of code (the Runnable). We can also check whether or not such a piece of code was set previously by the 'isExec()' method.
The TextMenuItem class itself also implements the Runnable interface and (thus) it has implemented a 'run()' method. All it does is blindly call the 'run()' method from the executable piece of code and catch any Exceptions (Throwables) it may throw.
But what if the exec member variable is null? Who or what takes care of that? Think a bit: a TextMenuItem will only be selected from a menu; there are no orphan TextMenuItems. A TextMenu will take care of that. By convention only the 'back' TextMenuItem will have a null exec member variable, all other TextMenuItems must have a non-null exec member variable; that is one of the reasons why the first constructor isn't public because I don't want anyone to call it and ruin the game.
If you take a close look at that first constructor you'll see that it is a protected constructor. This implies that it can only be called from the class itself and from its sub-classes. The others (the users) have to call the second constructor.
A menu also shows itself as a single line of text in another menu (unless it is a top-level menu of course). So in a way a TextMenu is a TextMenuItem and can be a sub-class of this first class.
What's the task of a TextMenu when it is selected? Basically it has to display all of its TextMenuItems, prompt the user to select one of them and execute that TextMenuItem; all the TextMenu has to do is call its 'run()' method; with one single exception: if the TextMenuItem returns false when its 'isExec()' method is called the 'back' option was selected and the currently running TextMenu has to return from its own 'run()' method. Think a bit about this and it'll make sense.
A TextMenu
First I'll show the class (it's quite a bit bigger than the first one but it does a lot more) and then we'll dissect all the code; here it is:
Expand|Select|Wrap|Line Numbers
- package textmenu;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.List;
- public class TextMenu extends TextMenuItem {
- private static final TextMenuItem quit= new TextMenuItem("quit", new Runnable() {
- public void run() {
- System.exit(0);
- }
- });
- private static final TextMenuItem back= new TextMenuItem("back");
- List<TextMenuItem> items;
- public TextMenu(String title, TextMenuItem ... items) { this(title, false, true, items); }
- public TextMenu(String title, boolean addBack, boolean addQuit, TextMenuItem ... items) {
- super(title);
- setExec(this);
- initialize(addBack, addQuit, items);
- }
- private void initialize(boolean addBack, boolean addQuit, TextMenuItem ... items) {
- this.items= new ArrayList<TextMenuItem>(Arrays.asList(items));
- if (addBack) this.items.add(back);
- if (addQuit) this.items.add(quit);
- }
- private void display() {
- int option= 0;
- System.out.println(getTitle()+":");
- for (TextMenuItem item : items) {
- System.out.println((option++)+": "+item.getTitle());
- }
- System.out.print("select option: ");
- System.out.flush();
- }
- private TextMenuItem prompt() throws IOException {
- BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
- while (true) {
- display();
- String line = br.readLine();
- try {
- int option= Integer.parseInt(line);
- if (option >= 0 && option < items.size())
- return items.get(option);
- }
- catch (NumberFormatException e) { }
- System.out.println("not a valid menu option: "+line);
- }
- }
- public void run() {
- try {
- for (TextMenuItem item= prompt(); item.isExec(); item= prompt())
- item.run();
- }
- catch (Throwable t) {
- t.printStackTrace(System.out);
- }
- }
- }
The first few lines of this class define two private static members: a 'back' menu item and a 'quit' menu item. We don't want the user to pass those in explicitly so we define them here and let the TextMenu do the work.
Both constructors take a String title and a list of TextMenuItems to populate the current menu. The second constructor also takes two boolean parameters telling it whether or not to add a 'back' menu item and/or a 'quit' menu item.
If you look at the second constructor it calls the protected constructor of its super-class, sets itself as the piece of executable code and initializes itself. Why didn't it call the other super-class constructor as in:
Expand|Select|Wrap|Line Numbers
- super(title, this);
So the constructor sets just the title by calling its protected super-class constructor and sets itself as the runnable piece of code in the second step by calling the protected 'setExec()' method in its super-class. Nothing outside of the class hierarchy can do the same and we want it to be that way. That way turned Javas idiosyncracy into a good thing ...
The private 'initialize()' methods builds the entire menu: it creates a List of a copy of all the TextMenuItems and optionally adds a 'back' and a 'quit' menu item. Why does it create a copy of a list that already is a copy of all the TextMenutems? The first list is a 'write through' list (read the API documentation); this means that if someone outside these classes changes something in the array the changes will shine through in the list. We don't want that so we make a copy of that first list. The fist list is also a fixed-sized list and if we want to add other TextMenuItems to a menu later we won't be able to do that.
What happens when this menu is selected from another menu? It's 'run()' method will be called; it basically does this:
Expand|Select|Wrap|Line Numbers
- for (TextMenuItem item= prompt(); item.isExec(); item= prompt())
- item.run();
In the original method body you can see that if something goes wrong (an Exception is thrown), the method catches it, displays it and terminates the execution of the current menu. What else should it do except displaying what went wrong? In a more industrial strength menu system more clever exception handling might be implemented but we'd like to keep it simple for now.
We're getting into the details of the menu handling; the body of the 'prompt()' method looks like this:
Expand|Select|Wrap|Line Numbers
- BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
- while (true) {
- display();
- String line = br.readLine();
- try {
- int option= Integer.parseInt(line);
- if (option >= 0 && option < items.size())
- return items.get(option);
- }
- catch (NumberFormatException e) { }
- System.out.println("not a valid menu option: "+line);
- }
Note that we only catch Exceptions thrown because of incorrect input, not because some pet parrot chew the wire of your keyboard or something even more desastrous. Such events will most likely throw another type of Exception. Such Exceptions are handled by the 'run()' method that called this method and it will print the Exception stack trace and terminate as well.
The last thing that needs to be done is displaying an entire menu: the title of the TexTMenu is displayed followed by a numbered list of TextMenuItems; this is not especially rocket science and you can figure out for yourself what the following code does:
Expand|Select|Wrap|Line Numbers
- int option= 0;
- System.out.println(getTitle()+":");
- for (TextMenuItem item : items) {
- System.out.println((option++)+": "+item.getTitle());
- }
- System.out.print("select option: ");
- System.out.flush();
top menu:
0: item 1
1: nested menu
2: quit
and a nested menu like this:
nested menu:
0: item 2
1: item 3
2: back
Note that the top-level menu has a 'quit' item but no 'back' item while the nested menu has no 'quit' item but it does have a 'back' item. Here is the test code; it's a bit boring but it does what it has to do and it exercises the menu system a bit:
Expand|Select|Wrap|Line Numbers
- public class TestTextMenu {
- private static TextMenuItem item1= new TextMenuItem("item 1",new Runnable() {
- public void run() {
- System.out.println("running item 1");
- }
- });
- private static TextMenuItem item2= new TextMenuItem("item 2",new Runnable() {
- public void run() {
- System.out.println("running item 2");
- }
- });
- private static TextMenuItem item3= new TextMenuItem("item 3",new Runnable() {
- public void run() {
- System.out.println("running item 3");
- }
- });
- private static TextMenu nestedMenu= new TextMenu("nested menu", true, false, item2, item3);
- private static TextMenu topMenu= new TextMenu("top menu", false, true, item1, nestedMenu);
- public static void main(String[] args) {
- topMenu.run();
- }
- }
Display the menus and prompts by using JOptionPanes;
Have more advanced navigation options (try a 'top' item for starters);
Have help text available;
Make it behave like JMenus and JMenuItems (read the API documentation);
Make the 'quit' item a bit more polite;
Make your parrots behave more sensible.
All except the last option are quite doable; have fun.
kind regards,
Jos
ps. any discussions, please post them in the Topics>Java>Answers section; this article is closed.