I have a non-singleton TopComponent. Can I write actions which show all available instances in the main menu?

Apache NetBeans Wiki Index

Note: These pages are being reviewed.

Some people applications where there is one primary kind of window component - an editor of some kind, or something that visualizes some data. There may be several such components open at any time, and they all are just instances of the same TopComponent subclass which are showing different data.

In this case, it can be useful to list all such components in the main menu, to facilitate switching between components. This involves three steps:

  1. Track all opened instances of the TopComponent subclass

  2. Write an Action which provides an array of menu items, one for each TopComponent instance

  3. Register that action in one of the main window menus

Tracking TopComponents And Creating Actions For Them

Keeping track of all of the instances of our TopComponent subclass is simple. Whenever a new one is constructed, we will just add it to a list. There are only two caveats:

  • We do not want memory leaks, so we should use a WeakReference. That way a component which has been closed can be garbage collected

  • It is not guaranteed that, during restart, previously opened components will be deserialized on the event thread (though they should be). So the list should be synchronized

We will add a static method which creates a list of `Action`s suitable for use with standard Swing `JMenuItem`s.

//Helper annotation to allow our component to be remembered across restarts.
//The DTD does not have to be defined, it just needs to be a unique name
@ConvertAsProperties(dtd = "-//org.netbeans.demo.multitopcomponent//MultiTopComponent//EN", autostore = false)
public class MultiTopComponent extends TopComponent {
  //A index for our display name, so we can tell the components apart

  static int ix;
  //Keep a list of all components we create.  Synchronize it because
  //they could be deserialized on some random thread;  use WeakReferences
  //so we don't hold a closed TopComponent in memory if it will never
  //be used again
  private static List<Reference<TopComponent>> all =
          Collections.synchronizedList(
          new ArrayList<Reference<TopComponent>>());

  public MultiTopComponent() {
    setDisplayName("Component " + ix++);
    all.add(new WeakReference<TopComponent>(this));
    setLayout(new BorderLayout());
    add(new JLabel(getDisplayName()), BorderLayout.CENTER);
  }

  public static List<Action> allActions() {
    List<Action> result = new ArrayList<Action>();
    for (Iterator<Reference<TopComponent>> it = all.iterator(); it.hasNext();) {
      Reference<TopComponent> tc = it.next();
      TopComponent comp = tc.get();
      if (comp == null) {
        it.remove();
      } else {
        result.add(new ShowAction(comp.getDisplayName(), tc));
      }
    }
    return result;
  }

  private static final class ShowAction extends AbstractAction {
    //Our action should not hold a strong reference to the TopComponent -
    //if it is closed, it should get garbage collected.  If a menu
    //item holds a reference to the component, then it won't be

    private final Reference<TopComponent> tc;

    public ShowAction(String name, Reference<TopComponent> tc) {
      this.tc = tc;
      putValue(NAME, name);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      TopComponent comp = tc.get();
      if (comp != null) { //Could have been garbage collected
        comp.requestActive();
      } else {
        //will almost never happen
        Toolkit.getDefaultToolkit().beep();
      }
    }

    @Override
    public boolean isEnabled() {
      TopComponent comp = tc.get();
      return comp != null &amp;&amp; comp.isOpened();
    }
  }

  @Override
  public int getPersistenceType() {
    return PERSISTENCE_ONLY_OPENED;
  }

  void readProperties(java.util.Properties p) {
    setDisplayName(p.getProperty("name"));
  }

  void writeProperties(java.util.Properties p) {
    p.setProperty("name", getDisplayName());
  }
}

This class contains persistence code - particularly the @ConvertAsProperties annotation and the methods readProperties()``writeProperties() and getPersistenceType(). These methods save some information about our TopComponent to disk on shutdown, in the form of a Properties object. If we do not want our components to be reopened after an application restart, we can just return PERSISTENCE_NEVER from getPersistenceType(), and delete the other persistence-related methods and the annotation. Note that you can omit the *Properties() methods and the annotation, and the components will be reopened on startup — but without persistence code, this is done by serializing the whole component to disk, which is both slower and stores more data than necessary. Typically, for an editor component, just storing the path to the file being edited is enough.

Writing an Action which provides an array of menu items

The DynamicMenuContent interface allows an Action to act as a factory for menu items - to control what components are shown in a menu to represent it. It also allows a single action to produce multiple menu items.

Here we will create an action which produces an array of menu items. All of them will be shown inline in the main menu:

public class MultiComponentAction extends AbstractAction implements DynamicMenuContent {

    @Override
    public void actionPerformed(ActionEvent e) {
        throw new AssertionError("Should never be called");
    }

    @Override
    public JComponent[] getMenuPresenters() {
        List<Action> actions = MultiTopComponent.allActions();
        List<JComponent> result = new ArrayList<JComponent>(actions.size());
        for (Action a : actions) {
            result.add (new JMenuItem(a));
        }
        return result.toArray(new JComponent[result.size()]);
    }

    @Override
    public JComponent[] synchMenuPresenters(JComponent[] jcs) {
        //We could iterate all of our JMenuItems from the previous call to
        //getMenuPresenters() here, weed out those for dead TopComponents and
        //add entries for newly created TopComponents here
        return getMenuPresenters();
    }
}

This will create an inline array of menu items, not a submenu. If you want a submenu instead, then implement getMenuPresenters() as follows:

        List<Action> actions = MultiTopComponent.allActions();
        JMenu menu = new JMenu("Multi TopComponents");
        for (Action a : actions) {
            menu.add (a);
        }
        return new JComponent[] { menu };

Registering The Action

Now we just need to actually add our multi-item action to the main menu, by registering it in our module’s XML layer.

In this example, we register it in the Actions/Window folder and then create a link in the Window menu folder using a .shadow file. Note that we could simply put the .instance file directly in the Menu/Window folder, but this approach is the preferred practice:

&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE filesystem PUBLIC
     &amp;quot;-//NetBeans//DTD Filesystem 1.1//EN&amp;quot;
     &amp;quot;http://www.netbeans.org/dtds/filesystem-1_1.dtd&amp;quot;&amp;gt;
&amp;lt;filesystem&amp;gt;
    &amp;lt;folder name=&amp;quot;Actions&amp;quot;&amp;gt;
        &amp;lt;folder name=&amp;quot;Window&amp;quot;&amp;gt;
            &amp;lt;file name=&amp;quot;org-netbeans-demo-multitopcomponent-MultiComponentAction.instance&amp;quot;&amp;gt;
                &amp;lt;attr name=&amp;quot;position&amp;quot; intvalue=&amp;quot;230&amp;quot;/&amp;gt;
            &amp;lt;/file&amp;gt;
        &amp;lt;/folder&amp;gt;
    &amp;lt;/folder&amp;gt;
    &amp;lt;folder name=&amp;quot;Menu&amp;quot;&amp;gt;
        &amp;lt;folder name=&amp;quot;Window&amp;quot;&amp;gt;
            &amp;lt;!-- This is the action that actually shows all available components --&amp;gt;
            &amp;lt;file name=&amp;quot;MultiComponent.shadow&amp;quot;&amp;gt;
                &amp;lt;attr name=&amp;quot;position&amp;quot; intvalue=&amp;quot;230&amp;quot;/&amp;gt;
                &amp;lt;attr name=&amp;quot;originalFile&amp;quot;
                stringvalue=&amp;quot;Actions/Window/org-netbeans-demo-multitopcomponent-MultiComponentAction.instance&amp;quot;/&amp;gt;
            &amp;lt;/file&amp;gt;
        &amp;lt;/folder&amp;gt;
    &amp;lt;/folder&amp;gt;
&amp;lt;/filesystem&amp;gt;