Friday, July 18, 2014

Add JMenuItem to JMenu after the menu has already been displayed to the user.

How I would do it ...


Temporarily disable new menus to prevent accidental clicks ...


MyMenuListener - How to use it:
        fileMenu.addMenuListener(new MyMenuListener(fileMenu) {

            @Override
            protected void quickMenu(MyMenuListener.MyMenuWorker menuWorker) {

                menuWorker.add(new JMenuItem("Fast New Menu A"));
                menuWorker.add(new JMenuItem("Fast New Menu B"));

            }

            @Override
            protected void slowMenu(MyMenuListener.MyMenuWorker menuWorker) {

                /** do something here that takes a long time. */
                
                menuWorker.add(new JMenuItem("Delayed Menu 1"));
                menuWorker.add(new JMenuItem("Delayed Menu 2"));
                
            }
        });



MyMenuListener - The code:  (as a single executable java file)
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;

/**
 * Demonstrates how to add menu items to a menu, dynamically on a background
 * thread using a SwingWorker, after the menu has already been displayed on
 * screen.
 *
 * @author javajon.blogspot.com
 */
public class SwingWorkerMenuBar extends javax.swing.JFrame {

    private javax.swing.JMenu fileMenu;

    /**
     * Creates new form SwingWorkerMenuBar. (yay?)
     */
    public SwingWorkerMenuBar() {
        initComponents();

        /**
         * this part is required for the menu to be dynamic.
         */
        fileMenu.addMenuListener(new MyMenuListener(fileMenu) {

            /**
             * This is the part that catches the menu just before it's displayed
             * so we can make immediate modifications. Any modifications you can
             * make from memory (i.e. without hitting a database or a file
             * system) can be made here and will display with the menu the first
             * time it becomes visible. Caution: if you put your
             * long-running code here then the menu won't show up at all until
             * your long-running code is done.
             */
            @Override
            protected void quickMenu(MyMenuListener.MyMenuWorker menuWorker) {

                menuWorker.add(new JMenuItem("Fast New Menu A"));
                menuWorker.add(new JMenuItem("Fast New Menu B"));

            }

            /**
             * this is where we add menus that might take some time to build ie
             * data has to be looked up in the database or read from a file
             * store or network share.
             */
            @Override
            protected void slowMenu(MyMenuListener.MyMenuWorker menuWorker) {
                for (int i = 500; i >= 50; i = (i / 33) * 25) {
                    try {

                        // simulate processing & send a new menu to UI
                        Thread.sleep(i);
                        menuWorker.add(new JMenuItem("New Menu " + i + " Delay"));

                    } catch (InterruptedException ex) {
                        Logger.getLogger(SwingWorkerMenuBar.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        });
        
    }

    public static abstract class MyMenuListener implements MenuListener, ActionListener {

        /**
         * This is the part that catches the menu just before it's displayed so
         * we can make immediate modifications. Any modifications you can make
         * from memory (i.e. without hitting a database or a file system) can be
         * made here and will display with the menu the first time it becomes
         * visible. Caution: if you put your long-running code here then
         * the menu won't show up at all until your long-running code is done.
         *
         * @param menuWorker use this object to add new menus
         */
        protected abstract void quickMenu(MyMenuWorker menuWorker);

        /**
         * this is where we add menus that might take some time to build ie data
         * has to be looked up in the database or read from a file store or
         * network share.
         *
         * @param menuWorker use this object to add new menus
         */
        protected abstract void slowMenu(MyMenuWorker menuWorker);

        /**
         * the menu with which this listener is associated.
         */
        private final JMenu menu;

        /**
         * the menus that have been created using menuWorker.add(menuItem).
         */
        private final List myMenus = new ArrayList();

        /**
         * the menus that have been disabled to prevent the user from
         * accidentally clicking on a menu that wasn't there a moment ago.
         */
        private final List menusToEnable = new ArrayList();

        /**
         * basic lock object to make sure threads are playing nice with shared
         * memory.
         */
        private final Object LOCK = new Object();

        /**
         * this is the part that re-enables menu options.
         */
        private final javax.swing.Timer timer = new Timer(300, this);

        /**
         * constructor.
         * @param menu 
         */
        public MyMenuListener(final JMenu menu) {
            this.menu = menu;
        }

        /**
         * this happens when the user clicks the menu, but before the pop up
         * menu appears.
         * @param e 
         */
        @Override
        public void menuSelected(MenuEvent e) {
            new MyMenuWorker(menu).execute();
        }

        @Override
        public void menuDeselected(MenuEvent e) {

        }

        @Override
        public void menuCanceled(MenuEvent e) {

        }

        /**
         * background worker that can do long-running tasks such as database
         * queries and then add more menu options to the list after the list has
         * already been displayed.
         */
        public class MyMenuWorker extends SwingWorker {

            private boolean quick;
            private int insertAt = 0;
            private final JMenu menu;

            public MyMenuWorker(JMenu menu) {
                synchronized (LOCK) {
                    this.menu = menu;                    

                    // clear out menus that were added last time
                    for (int i = 0; i < menu.getItemCount(); i++) {
                        final JMenuItem menuItem = menu.getItem(i);
                        if (menuItem != null && myMenus.contains(menuItem)) {
                            if (myMenus.remove(menuItem)) {
                                menu.remove(menuItem);
                                i--;
                            }
                        }
                    }

                    // add instant menus
                    this.quick = true;
                }
                quickMenu(this);
                synchronized (LOCK) {
                    this.insertAt = menu.getItemCount(); // end of list
                }
            }

            @Override
            protected Boolean doInBackground() throws Exception {
                synchronized (LOCK) {
                    this.quick = false;
                }
                slowMenu(this);
                return null;
            }

            /**
             * add menu items after the menu has already been shown. these are
             * the menu items that are published above using publish().
             *
             * @param menuItems menu items to add
             */
            @Override
            protected void process(List menuItems) {
                final int originalInsert = insertAt;
                for (final JMenuItem item : menuItems) {
                    myMenus.add(item);
                    menu.add(item, insertAt++);
                }
                tempDisable(menu, originalInsert);
                resizeMenu();
            }

            /**
             * feel free to reuse, modify, or distribute.
             * http://javajon.blogspot.com/
             */
            private void resizeMenu() {
                final JPopupMenu popupMenu = menu.getPopupMenu();
                if (popupMenu.isVisible()) {

                    // redraw the menu
                    menu.revalidate();

                    // resize the menu
                    popupMenu.setVisible(false);
                    popupMenu.setVisible(true);

                }
            }

            /**
             * report errors.
             */
            @Override
            protected void done() {
                try {
                    get();
                } catch (InterruptedException ex) {
                    Logger.getLogger(SwingWorkerMenuBar.class.getName()).log(Level.INFO, null, ex);
                } catch (ExecutionException ex) {
                    Logger.getLogger(SwingWorkerMenuBar.class.getName()).log(Level.INFO, null, ex);
                }
            }

            /**
             * when called from quickMenu() this method
             *
             * @param item
             */
            void add(JMenuItem item) {
                if (quick) {
                    myMenus.add(menu.add(item));
                } else {
                    publish(item);
                }
            }

        }

        /**
         * temporarily disables menu options because they are changing and you
         * don't want the user to click and have the menu option change just
         * before the click is made.
         *
         * @param menu
         * @param firstIndex
         */
        private void tempDisable(
                final JMenu menu,
                final int firstIndex) {

            synchronized (LOCK) {
                int insertAt = 0;
                for (int i = firstIndex; i < menu.getItemCount(); i++) {
                    final JMenuItem menuItem = menu.getItem(i);
                    if (menuItem.isEnabled()) {
                        menuItem.setEnabled(false);
                        if (!menusToEnable.contains(menuItem)) {
                            menusToEnable.add(insertAt++, menuItem);
                        }
                    }
                }

                timer.start();
            }

        }

        /**
         * this is the code that re-enables menus when they have been disabled
         * to prevent accidental clicking. this is the ActionListener
         * implementation for the timer task.
         *
         * @param e
         */
        @Override
        public void actionPerformed(ActionEvent e) {

            synchronized (LOCK) {
                if (menusToEnable.isEmpty()) {

                    /**
                     * if there's nothing to do, kill the timer.
                     */
                    timer.stop();

                } else {

                    /**
                     * re-enable the menus.
                     */
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            synchronized (LOCK) {
                                if (!menusToEnable.isEmpty()) {
                                    // enable the first menu that's disabled
                                    for (int i = 0; i < menu.getItemCount(); i++) {
                                        final JMenuItem enableMe = menu.getItem(i);
                                        if (menusToEnable.contains(enableMe)) {
                                            // enable one custom menu and quit
                                            enableMe.setEnabled(true);
                                            enableMe.revalidate();
                                            menusToEnable.remove(enableMe);
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    });

                } // empty / kill timer
            } // synchronized
        }

    }

    /**
     * create a simple form with a file menu.
     */
    private void initComponents() {
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        /**
         * simulate a file menu with a single menu option on it.
         */
        final javax.swing.JMenuBar menuBar;
        setJMenuBar(menuBar = new javax.swing.JMenuBar());
        menuBar.add(this.fileMenu = new javax.swing.JMenu("File"));
        fileMenu.add("Existing Menu I");
        fileMenu.add("Existing Menu II");

        /**
         * form content, form size (i think), and center on screen.
         */
        getContentPane().setPreferredSize(new Dimension(400, 260));
        pack();
        setLocationRelativeTo(null);

    }

    /**
     * default program entry point -- generated by NetBeans.
     *
     * @param args the command line arguments
     */
    public static void main(String args[]) {

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new SwingWorkerMenuBar().setVisible(true);
            }
        });
    }

}






No comments:

Post a Comment