Dynamically Creating Menu Items (Part 1)

Let's set up an infrastructure for dynamically creating menu items in a NetBeans Platform application. We start with this, i.e., a simple skeleton. We have an Action presented as a JMenu over which we have control via Presenter.Menu, together with a LookupListener that we're going to use for listening to the Lookup for objects of interest:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.actions.Presenter;
@ActionID(category = "Shortcuts", id = "org.bla.core.DynamicShortcutMenu")
@ActionReference(path = "Menu/Shortcuts")
public class DynamicShortcutMenu 
    implements ActionListener, Presenter.Menu, LookupListener {
    JMenu menu = new JMenu();
    @Override
    public JMenuItem getMenuPresenter() {
        return menu;
    }
    @Override
    public void resultChanged(LookupEvent le) {
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        // not used
    }
}

Next, let's develop our LookupListener further so that we can listen for new objects of interest. What the "object of interest" is, is up to you. Let's create an interface named "Clickable" as the currency, i.e., the communication mechanism that will be used to communicate with the Action that we have created above, i.e., to tell it that new menu items are needed.

import javax.swing.AbstractAction;
public interface Clickable {
    AbstractAction getAbstractAction();
}

OK. We have an interface that is a wrapper around an AbstractAction. Later on, we'll publish implementations of this interface.

For the moment, let's focus on changing the earlier code so that we can consume the implementations, once those implementations are published.

@ActionID(category = "Shortcuts", id = "org.bla.core.DynamicShortcutMenu")
@ActionRegistration(displayName = "not-used", lazy = false)
@ActionReference(path = "Menu/Shortcuts")
public class DynamicShortcutMenu extends AbstractAction
        implements ContextAwareAction, Presenter.Menu, LookupListener {
    private final Lookup.Result<Clickable> clickables;
    private JMenu menu = new JMenu("Dynamic");
    public DynamicShortcutMenu() {
        this(Utilities.actionsGlobalContext());
    }
    public DynamicShortcutMenu(Lookup lkp) {
        clickables = lkp.lookupResult(Clickable.class);
        clickables.addLookupListener(
                WeakListeners.create(LookupListener.class, this, clickables));
    }
    @Override
    public JMenuItem getMenuPresenter() {
        return menu;
    }
    @Override
    public void resultChanged(LookupEvent le) {
        for (Clickable c : clickables.allInstances()){
            menu.add(c.getAbstractAction());
        }
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        // not used
    }
    @Override
    public Action createContextAwareInstance(Lookup lkp) {
        return new DynamicShortcutMenu(lkp);
    }
}

Great. We're now listening to the global lookup for Clickables and then adding their AbstractActions to the menu we created above.

Now that we know how to consume Clickables, let's learn how to publish a Clickable. Below you see a TopComponent that contains a JButton. When clicked, the JButton publishes a Clickable into the Lookup of the TopComponent. At that stage, the Action code above causes a new JMenuItem to be added. Note that at no point is a JMenuItem removed, i.e., when selection changes to a different TopComponent, the JMenuItem added via the code below will still be in the dynamic menu, which is probably how you want things to be anyway.

@TopComponent.Description(
        preferredID = "SomeTopComponent",
        persistenceType = TopComponent.PERSISTENCE_ALWAYS
)
@TopComponent.Registration(
        mode = "explorer",
        openAtStartup = true)
public class SomeTopComponent extends TopComponent {
    private DateFormat formatter = new SimpleDateFormat("HH:mm:ss");
    private InstanceContent ic = new InstanceContent();
    public SomeTopComponent() {
        setDisplayName("Some");
        setLayout(new BorderLayout());
        add(new JButton(new AbstractAction("Add new menu item to shortcuts") {
            @Override
            public void actionPerformed(ActionEvent e) {
                ic.set(Collections.singletonList(new SomeClickable()), null);
            }
        }), BorderLayout.NORTH);
        associateLookup(new AbstractLookup(ic));
    }
    private class SomeClickable implements Clickable {
        @Override
        public AbstractAction getAbstractAction() {
            final String formatted = formatter.format(System.currentTimeMillis());
            return new AbstractAction("Added at: " + formatted) {
                @Override
                public void actionPerformed(ActionEvent e) {
                    JOptionPane.showMessageDialog(null, "hello from " + formatted);
                }
            };
        }
    }
}

Here's the result.

Of course, this throws up some new questions. Especially, how to achieve this result for a menu within the menubar, rather than a submenu within a menu within the menubar. I.e., as you can see above, this only solves the situation where a third-level menu needs to be dynamic. But what about if the menu "Dynamic" above needs to itself be dynamic? And, even more complexly, what if the "Shortcuts" menu should, itself, be dynamic? See part 2 for the solutions to these questions.

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« July 2015
SunMonTueWedThuFriSat
   
4
7
11
12
20
27
28
29
30
31
 
       
Today