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.

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 guest on February 21, 2007 at 11:29 PM 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 21, 2007 at 11:34 PM 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 22, 2007 at 08:00 PM PST #

Nice

Posted by guest on February 23, 2007 at 01: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 23, 2007 at 08:21 PM 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 10, 2007 at 08:21 PM 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 11:51 AM 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 12: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 12:42 AM PDT #

Hello, I'm from Brazil and this article almost solves my problem.
I have a component that receives some object,
then I read the methods that contain an annotation that I created.
Then for each of these methods I have to create a button (action) on the toolbar, and each button should trigger the method of the object received by the component.

Example:

public class someClass{
@ ToolbarAction (name = "someAction" icon = "org/prj/resorces/icon1.pnj)
public void someMethod () {... }
@ ToolbarAction (name = "otherAction" icon = "org/prj/resorces/icon2.pnj)
public void otherMethod () {... }
}

If my component receives an instance of SomeClass, should appear two buttons on the toolbar.

With this article I managed to add and remove actions on the toolbar, but actions always runs the same thing.

Posted by Glauco on December 02, 2010 at 11:24 AM PST #

Hi Geertjan,
When i am double click on node it display the children nodes..but i want disable this,how it is possible.
thank you

Posted by sivaprasad on February 06, 2011 at 12:59 PM PST #

Hi, Geertjan. This works! I have two questions:

1) The added menu item is disabled. How to enable it?

2) When I try to undo the menu add by renaming xxx.instance to xxx_hidden.instance,
there is an IllegalStateException: no selectionType parameter in {}
at org.openide.awt.Actions.inject(Actions.java:698).

Following your guidance, this is what the Action wizard added to Actions/Window:

<file name="com-company-MyAction.instance">
* <attr bundlevalue="com.company.Bundle#CTL_MyAction" name="displayName"/>
<attr methodvalue="org.openide.awt.Actions.context" name="instanceCreate"/>
* <attr name="type" stringvalue="com.company.MyTopComponent"/>
* <attr methodvalue="org.openide.awt.Actions.inject" name="delegate"/>
* <attr name="injectable" stringvalue="com.company.MyAction"/>
<attr name="selectionType" stringvalue="EXACTLY_ONE"/>
* <attr boolvalue="false" name="noIconInMenu"/>
</file>

This is the code that tries to remove the new entry in Actions/Window by renaming
the entry, taken from some other working code:

String actPath = "Actions/Window/";
String actFilename = "com-company-MyAction";
String actExt = "instance";
FileObject actObj = FileUtil.getConfigFile(actPath+actFilename+"."+actExt);
if (actObj != null) { // If module found and enabled
FileLock flock = null;
try {
flock = actObj.lock();
// The next line gives IllegalStateException:
actObj.rename(flock, actFilename + "_hidden", actExt);
}
catch (IOException ex) {logger.error(ex);}
finally {if (flock != null) {flock.releaseLock();} } }

None of my other Action/Window entries have any of the attributes marked with * above,
but that is what the Action wizard generated. How to overcome this?

Thanks!!

Posted by guest on March 02, 2012 at 01:17 PM PST #

Hi Geertjan,

i tried to implement this to my application (a storage management tool with different users login in) and it did not work out.

Hers's the scenario:
I'm having a login modul that authenticates the user. Now all actions appearing on the menu are created in annotations in the action class itself, e.g.

@ActionID(category = "File",
id = "org.myapp.ui.actions.neu.NewUserAction")
@ActionRegistration(iconBase = "org/myapp/utils/icons/newUser.png",
displayName = "#CTL_NewUserAction")
@ActionReferences({
@ActionReference(path = "Menu/Tools/Admin", position = 1)
})
@Messages("CTL_BenutzerNeuAction=Add New User")
public final class BenutzerNeuAction implements ActionListener {...}

The result is a new menuitem: Tools -> Admin -> Add New User, but of course it is visible to all authenticated users.

Now i want to hide that menuitem from not-admin users, or - even better - ged rid of:
@ActionReferences({
@ActionReference(path = "Menu/Tools/Admin", position = 1)
})
and build this menuitem in my login-modul, eg.:
(in the login-modul installer)
if (user.getAdmin) {
buildMenuItem(newUser)
}

I'm not using the layer.xml right now, because - with 10 or more usergroups - this seems to much handwork for me.

Do you think that is possible? And, if yes, any suggestions how to do this?

Thank you,
Patrick

Posted by Patrick on March 26, 2013 at 03:33 AM PDT #

I'd recommend using the layer. I.e., a different layer for each usergroup, each defining the UI that relates to the usergroup. Also, let's not have a long discussion about this here in a blog entry, rather join and write to the dev@platform.netbeans.org alias and discuss this there.

Posted by Geertjan on March 26, 2013 at 07:04 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