Close

Java Swing - A generic builder to create JMenuBar or JPopupMenu invoking on JTable/JTree

[Updated: Jul 4, 2018, Created: Apr 6, 2018]

Following example creates a generic builder (MenuBuilder) for building menus which can be used for JMenuBar or for JPopupMenu invoking on JTable or JTree.

Example

An interface for menu actions

For using our MenuBuilder, we need to implement this interface to define what actions should be taken when a menu is clicked, and also to enable/disable menu items dynamically before they show up.

package com.logicbig.uicommon.menu;

import java.util.List;

public interface MenuAction {

  void perform(String command, List<?> selection);

  boolean shouldEnable(String command, List<?> selection);
}

The main class

Let's see our main class to see how MenuBuilder is used.

public class MenuBuilderExampleMain {
  public static void main(String[] args) {
      JTree tree = createJTree();
      JTable table = createJTable();
      MenuBuilder menuBuilder = buildMenu();
      menuBuilder.buildPopupMenu(tree);
      //in real scenario we will create different menu for each component
      menuBuilder.buildPopupMenu(table);
      JMenu menu = menuBuilder.buildMenu("Main menu");
      JMenuBar jMenuBar = new JMenuBar();
      jMenuBar.add(menu);

      JFrame frame = createFrame();
      frame.setJMenuBar(jMenuBar);
      frame.add(new JScrollPane(tree), BorderLayout.WEST);
      frame.add(new JScrollPane(table), BorderLayout.CENTER);
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
  }

  private static MenuBuilder buildMenu() {
      //in real scenario separate actions should be created for each menu
      MenuAction nodeAction = createNodeAction();
      return MenuBuilder.init()
                        .menu("1", nodeAction)
                        .menu("2", nodeAction)
                        .parent("3", nodeAction)
                        .menu("4", nodeAction)
                        .menu("5", nodeAction)
                        //nested parent
                        .parent("5a", nodeAction)
                        .menu("5a1", nodeAction)
                        .menu("5a2", nodeAction)
                        .parentEnd()
                        .menu("5b", nodeAction)
                        .parentEnd()
                        .menu("6", nodeAction);
  }

  private static MenuAction createNodeAction() {
      return new MenuAction() {
          @Override
          public void perform(String command, List<?> selection) {
              System.out.println("menu action invoked: " + command);
              System.out.println("selections: ");
              for (Object o : selection) {
                  if (o instanceof Object[]) {//table row
                      System.out.println(Arrays.toString((Object[]) o));
                  } else {
                      System.out.println(o);
                  }
              }
          }

          @Override
          public boolean shouldEnable(String command, List<?> selection) {
              System.out.println("shouldEnable invoked: " + command);
              //just disable some randomly,
              // in real scenario we will disable based on selection, command and some other external dynamic params
              return Math.random() < 0.6;
          }
      };
  }

  private static JTable createJTable() {
      return new JTable(new Object[][]{
              new Object[]{1, 2, 3},
              new Object[]{4, 5, 6},
              new Object[]{7, 8, 9}},
              new String[]{"one", "two", "three"});
  }

  private static JTree createJTree() {
      JTree tree = new JTree();//using default tree
      JTreeUtil.setTreeExpandedState(tree, true);
      return tree;
  }

  private static JFrame createFrame() {
      JFrame frame = new JFrame("Menu Builder Example");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setSize(new Dimension(600, 400));
      return frame;
  }
}

The Menu Builder

package com.logicbig.uicommon.menu;

import com.logicbig.uicommon.table.JTableUtil;
import com.logicbig.uicommon.tree.JTreeUtil;
import javax.swing.*;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;

public class MenuBuilder {
    private static final String ClientMenuActionProp = "ClientMenuActionProp";
    private List<MenuNode> menuItemList = new ArrayList<>();
    private List<MenuNode> currentParents = new ArrayList<>();

    public static MenuBuilder init() {
        return new MenuBuilder();
    }

    public MenuBuilder menu(String displayName, MenuAction action) {
        MenuNode menuElement = new MenuNode(displayName, action);
        if (currentParents.size() == 0) {
            menuItemList.add(menuElement);
        } else {
            currentParents.get(currentParents.size() - 1).addChild(menuElement);
        }
        return this;
    }

    public  MenuBuilder parent(String displayName, MenuAction menuAction) {
        MenuNode menuElement = new MenuNode(displayName, menuAction);
        menuElement.children = new ArrayList<>();
        if (currentParents.size() == 0) {
            menuItemList.add(menuElement);
        } else {
            currentParents.get(currentParents.size() - 1).addChild(menuElement);
        }
        currentParents.add(menuElement);

        return this;
    }

    public MenuBuilder parentEnd() {
        if (currentParents.size() == 0) {
            throw new IllegalArgumentException("MenuBuilder#endParent() call should "
                    + "only be after MenuBuilder()#parent()");
        }
        currentParents.remove(currentParents.size() - 1);
        return this;
    }

    //todo add more build methods for JRadioButtonMenuItem (radioMenu(.....)),
    // JCheckBoxMenuItem (checkMenu(....)) and for custom menu component (customMenu(...))

    //creates JMenus for JMenuBar
    public JMenu buildMenu(String name) {
        JMenu menu = new JMenu(name);
        initMenuListener(menu);
        addElementsToRootMenu(menu, this.menuItemList, null);
        return menu;
    }

    //supports JTree and JTable, it can be extended for others
    public JPopupMenu buildPopupMenu(JComponent source) {
        if (menuItemList.size() == 0) {
            throw new IllegalArgumentException("No menu child added");
        }
        JPopupMenu popupMenu = new JPopupMenu();
        addElementsToRootMenu(popupMenu, this.menuItemList, source);
        initPopupListener(popupMenu, source);
        return popupMenu;
    }

    @SuppressWarnings("unchecked")
    private void addElementsToRootMenu(JComponent rootMenu,
                                           List<MenuNode> menuItemList, JComponent source) {
        for (MenuNode menuNode : menuItemList) {
            if (menuNode.hasChildren()) {
                JMenu parentMenu = new JMenu(menuNode.displayName);
                rootMenu.add(parentMenu);
                parentMenu.putClientProperty(ClientMenuActionProp, menuNode.action);
                addElementsToRootMenu(parentMenu, menuNode.children, source);//recursive

            } else {
                JMenuItem mi = new JMenuItem(menuNode.displayName);
                rootMenu.add(mi);
                mi.putClientProperty(ClientMenuActionProp, menuNode.action);
                mi.addActionListener((e) -> {
                    MenuAction action = menuNode.action;
                    if (source == null) {
                        List list = new ArrayList<>();
                        list.add(mi.getText());
                        action.perform(mi.getActionCommand(), list);
                    } else {
                        List<?> t = getSelection(source);
                        action.perform(mi.getActionCommand(), t);
                    }
                });
            }
        }
    }

    private List<?> getSelection(JComponent source) {
        if (source instanceof JTree) {
            JTree tree = ((JTree) source);
            return JTreeUtil.getSelectedUserObjects(tree);
        } else if (source instanceof JTable) {
            JTable table = (JTable) source;
            return JTableUtil.getSelectedRows(table);
        }
        return new ArrayList<>();
    }

    private void initMenuListener(JMenu menu) {
        menu.addMenuListener(new MenuListener() {
            @Override
            public void menuSelected(MenuEvent e) {
                //no user selected objects in case of JMenu Bar
                enableDisableItems(menu, new ArrayList<>());
            }

            @Override
            public void menuDeselected(MenuEvent e) {
            }

            @Override
            public void menuCanceled(MenuEvent e) {
            }
        });
    }

    private void initPopupListener(JPopupMenu popup, JComponent component) {
        component.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    if (!selectionValid(component)) {
                        return;
                    }
                    popup.show(component, e.getX(), e.getY());
                }
            }

            private boolean selectionValid(JComponent component) {
                if (component instanceof JTree) {
                    return ((JTree) component).getSelectionCount() != 0;
                } else if (component instanceof JTable) {
                    return ((JTable) component).getSelectedRowCount() != 0;
                }
                return true;
            }
        });

        popup.addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                JPopupMenu popupMenu = (JPopupMenu) e.getSource();
                List<?> selectedUserObjects = getSelection(component);
                enableDisableItems(popupMenu, selectedUserObjects);
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
            }
        });
    }

    private void enableDisableItems(MenuElement parentMenuElement,
                                    List<?> selectedUserObjects) {
        for (MenuElement menuElement : parentMenuElement.getSubElements()) {
            JComponent c = (JComponent) menuElement;
            MenuAction menuAction = (MenuAction) c.getClientProperty(ClientMenuActionProp);
            if (menuAction != null) {
                String command = c instanceof AbstractButton ?
                        ((AbstractButton) c).getActionCommand() : null;
                c.setEnabled(menuAction.shouldEnable(command, selectedUserObjects));
            }
            if (c.isEnabled()) {
                enableDisableItems(menuElement, selectedUserObjects);//recursive
            }
        }
    }

    private static class MenuNode {
        private final String displayName;
        private MenuAction action;
        private ArrayList<MenuNode> children;

        public MenuNode(String displayName, MenuAction action) {
            this.displayName = displayName;
            this.action = action;
        }

        public boolean hasChildren() {
            return children != null && children.size() > 0;
        }

        public void addChild(MenuNode child) {
            if (children == null) {
                children = new ArrayList<>();
            }
            children.add(child);
        }
    }
}

Output

Console output:

shouldEnabled invoked: 1
shouldEnabled invoked: 2
shouldEnabled invoked: 4
shouldEnabled invoked: 5
shouldEnabled invoked: 5a1
shouldEnabled invoked: 5a2
shouldEnabled invoked: 5b
shouldEnabled invoked: 6
menu action invoked: 5a1
selections:
red

Console output:

shouldEnabled invoked: 1
shouldEnabled invoked: 2
shouldEnabled invoked: 4
shouldEnabled invoked: 5
shouldEnabled invoked: 5a1
shouldEnabled invoked: 5a2
shouldEnabled invoked: 5b
shouldEnabled invoked: 6
menu action invoked: 5a1
selections:
[7, 8, 9]

Console output:

shouldEnabled invoked: 1
shouldEnabled invoked: 2
shouldEnabled invoked: 4
shouldEnabled invoked: 5
shouldEnabled invoked: 5a1
shouldEnabled invoked: 5a2
shouldEnabled invoked: 5b
shouldEnabled invoked: 6
menu action invoked: 5a2
selections:
5a2

Example Project

Dependencies and Technologies Used:

  • JDK 1.8
  • Maven 3.3.9

A generic Menu Builder Select All Download
  • menu-builder-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
              • uicommon
                • menu
                  • MenuBuilder.java
                  • table
                  • tree

    See Also