Download NetBeans!

20070222 Thursday February 22, 2007

Programmatically Hiding Actions in NetBeans Platform Applications

I got a very interesting question yesterday from two engineers from a local company, Samyak Infotech Pvt. Ltd., which is close to Hyderabad. They said they read my blog, so hopefully this will find them and not confuse them too much. :-) (No guarantees that the approach taken in this blog is correct, by the way.) They're obviously very advanced NetBeans module writers. Their question boiled down to the scenario where you have users in a database and, depending on whether their login succeeds, some additional menus are shown in the application's menu bar. How to implement this? Jesse Glick and Tonny Kohar gave some advice. It ended up being a bit of a puzzle and it still isn't complete, but here's the basic outline (and I'm hoping that it will make more sense to me as I write about it).

First, some pictures. In this scenario, the user types a name in the Options window. Ultimately, this would be connected somehow to a database and maybe you wouldn't use the Options window at all for this. However, to set the scene, I had to be able to type in a name somewhere. So here it is:

Now I click OK. Next (i.e., immediately), only if I typed "Tom", do I see the following menus:

If I do not type "Tom", then the above screenshot would not have (1) "Some", (2) "Menu for Tom", and (3) "Super secret submenu". All three of these would not be there. When I go back to the Options window and change the name to anything else, and when I then click OK in the Options window, all three of the above disappear. They reappear when I go back and enter "Tom" again.

If I understood it correctly, this is exactly what the engineers that I met wanted to implement.

So here are the pieces of this solution (I am sure there are other, and probably better, solutions, by the way):

  1. Use the Options window wizard to generate stubs for implementing a new category in the Options window's Miscellaneous panel. Use the Preferences API, as described in detail elsewhere in this blog, to persist the name typed in the text field.

  2. Use the Action wizard to create a stub for implementing a new menu item. However, delete the Menu entries generated in the layer.xml file. If you keep that entry there, under the Menu folder, the menu item will automatically be displayed. You do not want this to happen. You want it to be hidden. You will create the required entry in the NetBeans user directory programmatically in the next step, when the correct name is entered in the text field.

  3. Use the Module Installer wizard to create a stub for implementing a new module installer. Using a module installer in general is a bad idea, because it slows down start up time. However, how else is the application going to know at start up whether to show or hide the menu item? The module installer discovers that the login works (it is persisted via the Persistence API, so here the module installer retrieves the preference, which is also described in this blog) and then writes entries in the NetBeans user directory.

    Here's some code that I have in the installer. If name equals Tom, the code below creates the new folder Menu/Some. It then writes a shadow file with a reference to the action's actual instance class:

    //Create the folders:
    FileObject menuSomeFolder = Repository.getDefault().getDefaultFileSystem().getRoot().getFileObject("Menu/Some");
    if (menuSomeFolder == null) {
        try {
            menuSomeFolder = Repository.getDefault().getDefaultFileSystem().getRoot().createFolder("Menu/Some");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    //Create the shadow file in the above folder, 
    //with content connecting the shadow file to the real instance:
    FileObject newMenuItem = Repository.getDefault().getDefaultFileSystem().getRoot().getFileObject("Menu/Some/org-netbeans-modules-test-TestAction.shadow");
    if (newMenuItem == null) {
        try {
            newMenuItem = menuSomeFolder.createData("org-netbeans-modules-test-TestAction","shadow");
            JOptionPane.showMessageDialog(null,newMenuItem.toString());
            FileLock lock = newMenuItem.lock();
            OutputStream out = newMenuItem.getOutputStream(lock);
    
            //In the shadow file, write the reference to the instance file,
            //which must match the registration in the module's layer.xml file:
            OutputStreamWriter out1 = new OutputStreamWriter(out, "UTF-8");
            out1.write("nbfs://nbhost/SystemFileSystem/Actions/Build/org-netbeans-modules-test-TestAction.instance");
            out1.flush();
            out1.close();
    
            out.close();
            lock.releaseLock();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    Note: The line Actions/Build/org-netbeans-modules-test-TestAction.instance matches the Actions registration entry generated by the Action wizard in my layer.xml file. This line is needed to connect the shadow file to the real Java class. (I discovered this after copying a menu item, in the Advanced section of the Options window, from one menu to another. When you do that, the IDE generates a shadow file in your NetBeans user directory. When you open that you will find just one line, which is in the format shown above.)

  4. At this point, I simply did a null check on the folders and shadow file. If they were not null, while the username was not Tom, the file objects are deleted. As a result, they are no longer in the NetBeans user directory. As a result of that, they're not in the menu bar either. Whenever the preference changes, the file objects for the folder and shadow file are either created or deleted. Simple and effective.

  5. Next, I used Jesse's suggestion of taking the NetBeans API DynamicMenuContent class. Here's the relevant code added to the action class created in step 2, in order to implement DynamicMenuContent:

    public JComponent[] getMenuPresenters() {
        JMenu m = new JMenu("Menu for " + Installer.getUserName());
        JMenuItem item = new JMenuItem("Super secret submenu");
        m.add(item);
        return new JComponent[] {m};
    
    }
    public JComponent[] synchMenuPresenters(JComponent[] items) {
        return getMenuPresenters();
    }

    So the name "Tom" is retrieved from the module installer, added as text to the menu, to which a submenu is added. However, since I created the folder Menu/Some, any shadow file in that folder, pointing to a corresponding instance file, is displayed in the new menu. So, I could create a whole new module (i.e., the third party vendor could do this too) and add my own action to the same folder, as shown here, where I've added "Some Other" menu item:

    In fact, the DynamicMenuContent class is not needed at all in this scenario, because all submenus are automatically retrieved from the layer.xml file's Menu/Some folder. However, the Action wizard doesn't show my original "Menu for Tom" action and neither do the nodes representing the layer.xml file. This must be a bug. Here's proof:

    So, before installing the "Some Other" action in the "Some" folder, the "Some" folder wouldn't have been visible in the screenshot above, even though I was working in an IDE which had the module including the "Menu for Tom" action installed in the "Some" folder.

Again, I don't know if this is the best approach, or even a good approach. But it works and it shows yet again the versatility of the NetBeans layer.xml file concept. I am able to show and hide menus and submenus, even entire actions, depending on a name set and retrieved by the Persistence API. In fact, note that I am not hiding the action, but showing it, because by default there is no action in this scenario. It is programmatically created and deleted as needed. And any third party vendor can add menu items as submenus below my action, since they're only going to be shown when the action is shown, which is exactly how one would want it. Hurray.

Postscript: Some caveats on this approach, received from Jesse: I'm unclear why you are modifying the SFS; you shouldn't need to, AFAIK. Generally modifying the SFS is a poor idea as changes are persisted to the userdir. It is also likely to be bad for performance. Anyway, if you do, use FileUtil.createData on Repo.default.defaultFS.root; and don't write that ugly URL to the body of the file, leave it empty but set the attr originalFile to the full path.

Feb 22 2007, 06:09:23 AM PST Permalink

Trackback URL: http://blogs.sun.com/geertjan/entry/hiding_menus_in_netbeans_platform
Comments:

Good morning, I’m a Cuban developer of software, and I’m very interesting in to learn and work with Net Bean, but I do not have access to the net bean site web, my country has not access to that side web, I’m needing documentation about net bean and examples, If there is somebody that may help me, send e-mail noel@mtz.desoft.cu. Noel

Posted by 200.55.143.203 on February 22, 2007 at 07:29 AM PST #

Hi Noel, I'm sorry, but for the same reason that your country doesn't have access to the NetBeans web site, I am not allowed to write to you.

Posted by Geertjan on February 22, 2007 at 07:34 AM PST #

Hi Geertjan, I am in the midst of evaluating NetBeans RCP to port our custom Swing App & have a somewhat similar requirement of enabling/disabling Menu Items depending upon which user has logged in the application. I am wary of the NB file system as its tied to the user's home directory & our target deployment scenario is to use WebStart with very little access to the user's disk. Can you advise on alternatives? Thanks

Posted by Krish on February 23, 2007 at 04:00 AM PST #

Nice

Posted by 210.211.251.49 on February 23, 2007 at 09:43 PM PST #

Hi Krish. There is a different approach, much simpler, has no impact on the user directory, but doesn't remove the top most menu. So, you wouldn't be able to hide the "File" and "Edit" level menus. But, if you had your menus within one of these, as submenus (with their own submenus), then this other approach would work. I will blog about this soon. Would you be interested in this scenario. Please keep in touch and feel free to ask any questions. If we can help with your evaluations, please drop me an e-mail at geertjan.wielenga@sun.com.

Posted by Geertjan on February 24, 2007 at 04:21 AM PST #

You can simply override getMenuPresenter() and getToolbarPresenter() in your action and invoke there super.getToolbarPresenter().setVisible(false). Because this method is invoked only once, you can not return just null because you would not be able to recreate the action in case of relogin.

Example:

public final class MyAction extends CallableSystemAction {

//... all the generated stuff

private JMenuItem menu; //for future reference

public JMenuItem getMenuPresenter() {
JMenuItem retValue;
retValue = super.getMenuPresenter();
menu = retValue;
retValue.setVisible(false); //we disable it by default
return retValue;
}

Posted by Wiechu on April 11, 2007 at 03:21 AM PDT #

The following content is copied from http://www.netbeans.org/download/dev/javadoc/org-openide-filesystems/overview-summary.html How to change menus, etc. after login? Since version 7.1 there is a way to change the content of system file system in a dynamic way. As system file systems contains various definitions (in NetBeans Platform menus, toolbars, layout of windows, etc.) it de-facto allows global change to these settings for example when user logs into some system. First thing to do is to create an implementation of filesystem. It can be created either from scratch, or by subclassing AbstractFileSystem, or MultiFileSystem. In this example we will subclass the MultiFileSystem: public class LoginFileSystem extends MultiFileSystem { private static LoginFileSystem INSTANCE; public LoginFileSystem() { // let's create the filesystem empty, because the user // is not yet logged in INSTANCE = this; } public static void assignURL(URL u) throws SAXException { INSTANCE.setDelegates(new XMLFileSystem(u)); } } It is necessary to register this instance in lookup by creating the file: META-INF/services/org.openide.filesystems.FileSystem with a single line containing the full name of your filesystem - e.g. your.module.LoginFileSystem. When done, the system will find out your registration of the filesystem on startup and will merge the content of the filesystem into the default system file system. You can show a dialog letting the user to log in to some system anytime later, and when the user is successfully in, just call LoginFileSystem.assignURL(url) where the URL is an XML file in the same format as used for regular layer files inside of many NetBeans modules. The system will notice the change in the content and notify all the config file listeners accordingly. Of course, instead of XMLFileSystem you can use for example memory file system, or any other you write yourself.

Posted by RatKing on May 24, 2007 at 06:51 PM PDT #

Hi Geertjan, I found it interesting that you declined to send email to the fellow Cuban who asked for some assistance. I don't know the circumstances and politics surrounding him not being able o access Netbeans site, but what politics stops you from responding to him really escapes me. I didn't realise learning stuffs was so heavily bound by politics that someone who posts all sorts of articles in the internet that is supposedly in the public domain for everyone to see, yet refuses to give assistance to people who look for assitence just because he hails from a country from a different political spectrum. I don't know how deeply you are involved with Netbeans. I can make a guess from following your posts and tutes for about a year now. If netbeans has issues with Cuba, does your Blog site and you yourself will have issues with Cuba automatically? From your, short and casual reply, its difficult for me to comprehend the mentality of people who apply the code of conduct, rules and regulation of their employer when it comes to sending an email for a bit of assitence. By the way, when you go to supermarket to do your groceries, do you boycott Cuban goods (if you actually find some )because Netbeans Org has issues with the Cuban Government? And did Netbeans have issues with Cuba because your Government has issues with it?... gee....I can write a whole friggin book on this issue.... By the way, I use Netbeans at work as well as home and I follow your articles as I need to conduct research into Netbeans RCP to determine how deeply we should couple our application with NB Platform. I hope our country doesn't have issues with your country as I may be forced to boycott your articles from then on for political reasons. :) Meanwhile I'll try to gather as much as I can ..... wish me luck..... and happy horizon.....

Posted by Anzaan on June 13, 2007 at 07:33 AM PDT #

Sorry, please contact the Sun legal department about your legal questions. I can give you an e-mail address if you want. Thanks.

Posted by Geertjan on June 13, 2007 at 07:42 AM PDT #

Post a Comment:

Name:
E-Mail:
URL:

Your Comment:

HTML Syntax: NOT allowed