How to Close Objects in the NetBeans Platform

Just like in this blog entry yesterday, you've included the "User Utilities" module (by right-clicking your NetBeans Platform application, choosing Properties, clicking Libraries, and checking the box next to "User Utilities" in the "ide" cluster) and so now you magically have "File | Open File" in the menu bar:

However, you don't have a "Close File" menu item in the list above. Let's change that! If we look in the layer file of a module (i.e., create a new XML Layer file from the New File dialog, expand the "Important Files" node, and then expand the layer file), you'll see that a Close Action already exists at Actions/System/Close:

<file name="org-openide-actions-CloseAction.instance">
    <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
    <attr name="delegate" methodvalue="org.openide.awt.Actions.performer"/>
    <attr name="selectionType" stringvalue="ANY"/>
    <attr name="surviveFocusChange" boolvalue="false"/>
    <attr name="displayName" bundlevalue="org/openide/awt/Bundle#Close"/>
    <attr name="noIconInMenu" boolvalue="true"/>
    <attr name="type" stringvalue="org.netbeans.api.actions.Closable"/>
</file>

The above defines a CloseAction which (1) needs to be displayed somewhere and (2) is activated when any number of Closables are selected.

What are Closables? They are implementations of "org.netbeans.api.actions.Closable". That interface has a single method, close, which returns a boolean. What you do with that implementation is completely up to you. But multiple objects in your application could be publishing Closables, so it makes sense to actually do something like closing something when invoking the CloseAction, which is enabled when a Closable is in the currently selected item's Lookup. Then test for some condition to be true and do something like closing the item, e.g., removing the Node, closing the TopComponent, or something similar to that.

Right now, without doing anything, the above CloseAction is available on editor tabs. I.e., right-click the tab of an editor document and you'll see "Close". Now we'd also like to see that in the File menu. Therefore, add the following to one of your layer files:

<folder name="Menu">
    <folder name="File">
        <file name="org-openide-actions-CloseAction.shadow">
            <attr name="originalFile" stringvalue="Actions/System/org-openide-actions-CloseAction.instance"/>
            <attr intvalue="850" name="position"/>
        </file>
    </folder>
</folder>

Run the application again and now you see this:

In other words, you now have a "Close" menu item, defined by the layer entry shown above, which means that it will become enabled automatically when something that is currently selected has a Closable in its Lookup. But before we get to that stage, you might be reading this and asking "How can I change the display text 'Close' to be 'Close File' instead?" That's the wrong question because you have no idea which objects may have Closable in their Lookup, i.e., it could be a Node, it could be a TopComponent, it could be something else, hence "Close" is the most appropriate display text or maybe it could be "Close Object" or "Close Item" instead. If you don't like this, you can create your own CloseAction, which is not hard at all:

@ActionID(
        category = "File",
        id = "org.zip.file.CloseZIPFolderAction")
@ActionRegistration(
        displayName = "#CTL_CloseZIPFolderAction")
@ActionReferences({
    @ActionReference(path = "Menu/File", position = 850),
    @ActionReference(path = "Loaders/folder/any/Actions", position = 250)
})
@Messages("CTL_CloseZIPFolderAction=Close ZIP Folder")
public final class MyCloseAction implements ActionListener {

    private final List<ZIPFolderClosable> context;

    public MyCloseAction(List<ZIPFolderClosable> context) {
        this.context = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        for (ZIPFolderClosable closable : context) {
            closable.close();
        }
    }
    
}

The above Action will be available from the File menu and also when right-clicking any folder. What happens when the Action is invoked? Well, it will only be enabled if one or more of your own special ZIPFolderClosables are in the Lookup of the currently selected object. When invoked, "close" will be called on all of those ZIPFolderClosables. What will that ZIPFolderClosable look like? It will be an interface with a method "close" that returns a boolean. The difference is that now you have a special Action for closing your own special object, i.e., ZIP folders.

Tip: If you want the text in the Close menu item to be updated dynamically, investigate the "menuText" attribute, as described here. And here's the result of that investigation:


@ActionID(
        category = "File",
        id = "org.zip.file.CloseZIPFolderAction")
@ActionRegistration(
        lazy = false,
        displayName = "NOT-USED")
@ActionReferences({
    @ActionReference(path = "Menu/File", position = 850),
    @ActionReference(path = "Loaders/folder/any/Actions", position = 250)
})
public final class MyCloseAction extends AbstractAction implements LookupListener {

    private Lookup.Result<Node> nodes;

    public MyCloseAction() {
        nodes = Utilities.actionsGlobalContext().lookupResult(Node.class);
        nodes.addLookupListener(
                WeakListeners.create(LookupListener.class, this, nodes));
        resultChanged(new LookupEvent(nodes));
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        for (Node node : nodes.allInstances()) {
            Closable closable = node.getLookup().lookup(Closable.class);
            if (closable != null) {
                closable.close();
            }
        }
    }

    @Override
    public void resultChanged(LookupEvent le) {
        Collection<? extends Node> p = nodes.allInstances();
        if (p.size() == 1) {
            Node currentNode = p.iterator().next();
            putValue("menuText", "Close " + currentNode.getDisplayName());
        } else {
            putValue("menuText", "Close");
        }
    }
    
}

Next, we need to publish a Closable into an object that needs to be closable when selected. Since all editor documents automatically have a Closable in their Lookup, you'll see the Close action above (i.e., the one defined at Actions/System/Close in the layer) will be enabled whenever you have an editor document selected.

But what about your own objects? How do you integrate your own objects into the above infrastructure? Well, for the ZIPDataObject discussed in recent blog entries, something like this is needed, i.e., add Closable to the Lookup of the ZIPNode, while (optionally) making sure all files currently opened from within the ZIP folder are also closed when the ZIP folder closes:

instanceContent.add(new Closable() {
    @Override
    public boolean close() {
        //Remove the FileObject from Central Lookup:
        CentralLookup.getDefault().remove(key);
        //Refresh the Node hierarchy, which will remove the Node:
        refresh(true);
        //But now we also want to close the related files
        //that are currently open, i.e., those from the 
        //currently closed ZIP folder.
        //Start by identifying the path to the ZIP folder:
        String fullPathToZIPFolder = key.getPath();
        //Iterate through open TopComponents, find matching FileObjects
        //and close those TopComponents
        Set opened = WindowManager.getDefault().getRegistry().getOpened();
        for (TopComponent topComponent : opened) {
            FileObject tcFileObject = topComponent.getLookup().lookup(FileObject.class);
            if (tcFileObject != null) {
                String fullPathToFile = tcFileObject.getPath();
                if(fullPathToFile.contains(fullPathToZIPFolder)){
                    if (topComponent.canClose()){
                       topComponent.close();
                    }
                }
            }
        }
        return true;
    }
});

And so now you have a centralized mechanism for closing all objects in a standardized way. Each object doesn't have its own Close action. Instead, all objects share the same Close action, they simply provide different implementations of what should happen when the Close action is invoked.

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
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today