Creating Context-Sensitive Capabilities for File-Based Nodes (Part 2)

In part 1, we created an Action for files of a certain type in the NetBeans Platform. What was special was that the Action is contextually sensitive to a custom defined "OpenCapability" interface. If an implementation of that interface is present, something can happen. If it is absent, something different can happen.

But what is it that can happen? I.e., how can the above infrastructure be used to do something useful? In this second part, we continue by looking at a scenario where an Action is enabled/disabled based on the presence/absence of a key found in a file. If the key is present, the value is used to find a specific TopComponent to be opened when the Action is invoked. As with the earlier part, this uses an approach introduced by Toni Epple in the recent NetBeans Platform Certified Training to the Institute of Marine Research in Bergen, Norway.

  1. In my module's layer file, I have a folder named "Reference", containing two FileObjects. The definition of the FileObjects points to actually existing files within my module:
    <folder name="Reference">
        <file name="Cruises.imrc" url="cruises.imrc">
            <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/reference.png"/>
        </file>
        <file name="Logbooks.imrc" url="logbooks.imrc">
            <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/reference.png"/>
        </file>
    </folder>

  2. So, there are two files within my module, named "cruises.imrc" and "logbooks.imrc". The former contains this content:
    window=CruisesTopComponent

    ... while the latter, i.e., "logbooks.imrc" is empty.

  3. Next, I enable the NetBeans Platform to recognize the above two files. I do this by using the New File Type wizard to create support for files that have "imrc" as their file extension. The wizard registers the new file type and also creates a skeleton DataObject for it.

  4. Within that generated DataObject, I load files for which the DataObject is created. I load them as Properties files. Then I check for the existence of a "window" key in that file. If that key exists, I add the "OpenCapability" to the cookieset. (I agree with Jean-Marc Borer in my recent blog entry relating to this topic, i.e., that Lookup should be used rather than Cookies. However, here I'm simply going along with the code generated by the New File Type wizard, i.e., code relating to cookies is generated, hence my additional code uses that too.)
    public class IMRCategoryDataObject extends MultiDataObject {
    
        Properties p;
    
        public IMRCategoryDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
            super(pf, loader);
            p = new Properties();
            //"pf" is the FileObject that is bound to this DataObject in the layer file,
            //thanks to the New File Type wizard. It has an InputStream, so that we
            //can load it into a Properties object:
            p.load(pf.getInputStream());
            
            CookieSet cookies = getCookieSet();
            cookies.add((Node.Cookie) DataEditorSupport.create(this, getPrimaryEntry(), cookies));
    
            OpenCapability oc = new OpenCapability() {
                @Override
                public void open() {
                    TopComponent tc = WindowManager.getDefault().findTopComponent(p.getProperty("window"));
                    tc.open();
                    tc.requestActive();
                }
            };
    
            if (p.getProperty("window") != null) {
                getCookieSet().assign(OpenCapability.class, oc);
            }
    
        }
    
        @Override
        protected Node createNodeDelegate() {
            final DataNode node = new DataNode(this, Children.LEAF, getLookup());
            node.setDisplayName(node.getName());
            return node;
        }
    
        @Override
        public Lookup getLookup() {
            return getCookieSet().getLookup();
        }
    
    }

    Note: The rest of this blog entry discusses the highlighted code above in detail. (The remainder of the code above is not interesting, i.e., it is standard DataObject code.)

  5. Then I create Nodes on top of the two FileObjects and display those Nodes in an explorer view. Only the Node for the "cruises.imrc" has an enabled "Open Window" action, since the Node for the other FileObject has no "window" key. In other words, the "OpenCapability" is only available in one of the two cases, causing the Action registration to enable the menu item for "cruises.imrc", but not for "logbooks.imrc".

  6. As in the previous blog entry, the "OpenCapability" is defined as follows:
    package no.imr.viewer;
    
    public interface OpenCapability {
        public void open();
    }

    ...while the NetBeans Platform is sensitive to the above capability via this registration in the layer file, which is linked into the contextual menu of the IMRCNode displayed in the explorer view via a layer entry in Loader/text/x-imrcategory, which is the MIME type of files using the ".imrc" file extension:

    <folder name="Actions">
        <folder name="Edit">
            <file name="no-imr-viewer-OpenCategoryAction.instance">
                <attr name="delegate" methodvalue="org.openide.awt.Actions.inject"/>
                <attr name="displayName" bundlevalue="no.imr.viewer.Bundle#CTL_OpenCategoryAction"/>
                <attr name="injectable" stringvalue="no.imr.viewer.OpenCategoryAction"/>
                <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
                <attr name="noIconInMenu" boolvalue="false"/>
                <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
                <attr name="type" stringvalue="no.imr.viewer.OpenCapability"/>
            </file>
        </folder>
    </folder>

    Since the "OpenCapability" is only added to the cookieset if the "window" key is present, which is absent in the case of "logbooks.imrc", the Action is disabled when you right-click the Node for "logbooks.imrc". When the Action is enabled, invocation of the Action results in "open" being called on the "OpenCapability":

    public final class OpenCategoryAction implements ActionListener {
    
        private final OpenCapability context;
    
        public OpenCategoryAction(OpenCapability context) {
            this.context = context;
        }
    
        public void actionPerformed(ActionEvent ev) {
            context.open();
        }
        
    }

    And what does "open" mean in this context? In this context, defined in the DataObject with which this blog entry started, "open" results in the opening of the TopComponent for which the "window" key has been defined. In the case of the "cruises.imrc" file, that means the "CruisesTopComponent".

And that's how setting a property in a file can result in an Action being enabled/disabled on a Node. Why is this useful? Well, maybe a new module will register a new ".imrc" file within the "Reference" folder. Let's say, for example, that a "Fish" node should be displayed under the "Reference" node. But, possibly that "Fish" node will not have a TopComponent to be opened. In that case, the ".imrc" file will not have a "window" key, so that the "Open" menu item will be disabled. If, on the other hand, the "Fish" node has a related TopComponent, i.e., one displaying the currently selected fish, the "fish.imrc" file would have a key/value pair like this, for enabling the "Open" menu item and causing the "FishTopComponent" to be found:

window=FishTopComponent

And that's how new additions to the "Reference" node can come from the outside, while still having distinct behavior. In the case of the Institute of Marine Research this could be pretty useful since each Node represents a different database which could have some very specific functionality. In such scenarios, contextually available (enabled/disabled present/absent) Actions on Nodes is essential and the above approach very elegantly solves this requirement. It is also a generic approach that can be used in any number of similar scenarios.

Best thing to do, however, is to make sure that the TopComponent, ".imrc" file, and layer registration are all found within the same module. That's what would probably be done anyway, since the usecase for this scenario assumes that each file would come from a different module, but it's important to remember anyway. For example, imagine that the TopComponent comes from one module, while the ".imrc" file comes from another one. Who is to say that both modules will be present and that the TopComponent will actually be found? Only way to really guarantee this is to put everything related to specific instance of this infrastructure into the same module.

Of course, the context-aware opening of TopComponents is just one example. The sky is the limit when you're using the NetBeans Platform, but if you've read this far, you probably already know that.

Comments:

>> If that key exists, I add the "OpenCapability" to the cookieset. (I agree with Jean-Marc Borer in my recent blog entry relating to this topic, i.e., that Lookup should be used rather than Cookies.

I still disagree with that :-) : No cookies are used here. Cookie is an Interface, none of the classes we use implements this interface. getCookieSet is just a name for a method that manages the Lookup. It's absolutely OK to use it, adding another lookup just adds complexity.

Posted by Toni Epple on April 18, 2010 at 04:26 AM PDT #

But it's a confusing name.

Posted by Geertjan Wielenga on April 18, 2010 at 04:32 AM PDT #

OK, with that I agree :-)

Posted by Toni Epple on April 18, 2010 at 04:36 AM PDT #

Rather than the properties-based approach described in this blog entry, you could simply register a new attribute for the FileObject in the layer. The attribute would be named "window", with the name of the TopComponent to be opened as the value. Then you wouldn't need a physical properties file at all. Instead, you'd read the layer file, using FileUtil.getConfigFile to find the folder, file, and attribute. Based on presence/absence of the attribute, you'd enable/disable the action, via adding/removing the capability from the cookieset. (However, the layer file is a pretty crowded place, so this properties file approach might still make sense, i.e., to create a distinct place for your context-sensitive action-enablement functionality.)

These comments above come from a discussion about the approach described in this blog entry, by Toni and myself here in Oslo.

Posted by Geertjan Wielenga on April 18, 2010 at 07:13 AM PDT #

SystemFileSystem.icon is deprecated. Rather than

<attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/reference.png"/>

use

<attr name="iconBase" stringvalue="no/imr/viewer/reference.png"/>

Posted by Jesse Glick on April 19, 2010 at 01:22 AM PDT #

Question 1: How do you remove the function 'add new template' of the file based node created by the new file type wizard. The structure should not allways behave as an extension point for the user, but the module should register nodes to only mirror the database relations.

Qestion 2: How do you extend the attribute set for the file based nodes, and how do you let the property editor track the current selected file base node given by the fileobject nodedelegate? For example the file based node "Reference" maybe want a <attr> tag in the layer for i.e. schema name. How do you expose it for the user through node-selection, and is it possible for the user to change it in the property editor and serialize it back in the system file system?

b.r. Åsmund Skålevik IMR Institute

Posted by Åsmund Skålevik on May 04, 2010 at 08:46 AM PDT #

Question 1: remove this from aour layer:

<file name="org-openide-actions-SaveAsTemplateAction.shadow">
<attr name="originalFile" stringvalue="Actions/System/org-openide-actions-SaveAsTemplateAction.instance"/>
<attr name="position" intvalue="900"/>
</file>

Posted by Toni Epple on May 05, 2010 at 12:42 AM PDT #

Question 2: just add attributes to your files as you like. Later read them via:

http://bits.netbeans.org/dev/javadoc/org-openide-filesystems/org/openide/filesystems/FileObject.html#getAttribute%28java.lang.String%29

Posted by Toni Epple on May 05, 2010 at 12:44 AM PDT #

>> getCookieSet is just a name for a method that manages the Lookup

I don't fully agree :P

You can only add objects to a CookieSet that implement Node.Cookie. No chance to add your own object type then.

If you declare your own Lookup with an InstanceContent (otherwise you cannot add/remove anything to it), you have to populate your lookup with the content of the provided CookieSet otherwise your will miss important things like the icon associated to your DataObject. To make the thing complete, you have to override some accessors to the CookieSet to make every thing work smoothly.

Any counter argument?

Posted by Jean-Marc Borer on May 27, 2010 at 11:01 PM PDT #

"You can only add objects to a CookieSet that implement Node.Cookie. No chance to add your own object type then." - you can use http://bits.netbeans.org/dev/javadoc/org-openide-nodes/org/openide/nodes/CookieSet.html#assign%28java.lang.Class,%20T...%29 to add any kind of object.

Posted by Jesse Glick on May 28, 2010 at 12:02 AM PDT #

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