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):
- 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.
- 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.
- 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.)
- 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.
- 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
Posted by 200.55.143.203 on February 22, 2007 at 07:29 AM PST #
Posted by Geertjan on February 22, 2007 at 07:34 AM PST #
Posted by Krish on February 23, 2007 at 04:00 AM PST #
Posted by 210.211.251.49 on February 23, 2007 at 09:43 PM PST #
Posted by Geertjan on February 24, 2007 at 04:21 AM PST #
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 #
Posted by Anzaan on June 13, 2007 at 07:33 AM PDT #
Posted by Geertjan on June 13, 2007 at 07:42 AM PDT #


