Software Engineer and NASA contractor in the Washington D.C. area. Specializes in Java, the NetBeans Platform, D3.JS and ANSI C. Sean is a DZone MVB and is not an employee of DZone and has posted 9 posts at DZone. You can read more from them at their website. View Full User Profile

JavaFX Accordion Slide Out Menu for the NetBeans Platform

05.17.2013
| 6026 views |
  • submit to reddit

Let's say you have a NetBeans Platform application that puts a premium on vertical space.  Maybe a Heads Up Display on a Touch Screen?  Wouldn't it be great to have the menu slide out from the edge of the screen only when you need it?  Well the NetBeans Platform provides slide-in TopComponents, of course, but a JMenu just isn't going to work out so well inside one.

We can use JavaFX as part of the solution as it provides some capabilities that the base Swing components available in the NetBeans Platform do not.  Let's say we take all of our root MenuBar items and place them within an Accordion type pane.  Each collapsible TitledPane of the Accordion control could then contain the sub-menu items, maybe represented by a JavaFX MenuButton.  This would allow for a recursive Menu like effect but the overall container could be placed anywhere. 

Something like the screenshot below:

What we see here is the described effect sliding out and overlayed on top of the Favorites tab.  I sprinkled in some transparency for good measure.  Notice how we are able to completely eliminate the Menu Bar and Tool Bar gaining potentially valuable real estate?  The rest of this tutorial will explain the steps necessary to achieve something like this. 

To fully understand and follow this tutorial you will need to start with the tutorial that this tutorial is clearly evolved from of which the link is below:

http://netbeans.dzone.com/articles/how-integrate-javafx-netbeans

That article was written by Geertjan Wielenga and it will become clear that much of the base code to accomplish this article was extended from Geertjan's example.  Thanks again Geertjan!

Similar articles to this that may be helpful are below:

http://netbeans.dzone.com/articles/javafx-fxml-meets-netbeans 

http://netbeans.dzone.com/articles/how-embed-javafx-chart-visual

All these articles are loosely coupled in a tutorial arc towards explaining and demonstrating the advantages of integrating JavaFX into the NetBeans Platform.  The following two steps are borrowed exactly as found from Geertjan's tutorial:

Step 1. Remove the default menubar and replace with your own: 

import org.openide.modules.ModuleInstall;

public class Installer extends ModuleInstall {

    @Override
    public void restored() {
            System.setProperty("netbeans.winsys.menu_bar.path", "LookAndFeel/MenuBar.instance");
    }
}
Step 2:  In the layer.xml file define your Swing replacement menubar. 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
<filesystem>
    <file name="Toolbars_hidden"/>
    <folder name="LookAndFeel">
        <file name="MenuBar.instance">
            <attr name="instanceOf" stringvalue="org.openide.awt.MenuBar"/>
            <attr name="instanceCreate" newvalue="polaris.javafxwizard.jfxmenu.HiddenMenuBar"/>
        </file>
    </folder>
</filesystem>
I have also taken the liberty to hide the Toolbars as well.  Now why are we replacing the old MenuBar with a new MenuBar if we intend to hide it?  Well if you hide the MenuBar via the layer.xml as I did the Toolbars the filesystem folder tree will not be instantiated.  That means we won't be able to dynamically determine the Menu Folder tree to rebuild our custom AccordionMenu.  The solution?  Make an empty Menubar.

package polaris.javafxwizard.jfxmenu;

import javax.swing.JMenuBar;

/**
 *
 * @author SPhillips (King of Australia)
 */
public class HiddenMenuBar extends JMenuBar {
    public HiddenMenuBar() {
        super();
    }
}

Step 3:  Build an "AccordionMenu" using JavaFX

This is where the tutorials diverge and this process gets a bit more complicated.  Our task is to use the JavaFX/Swing Interop pattern to create a component that extends JFXPanel yet can give the user access to all the items that were once in the Menu Bar.  The basic algorithm is as such:

Create a component that extends JFXPanel
Implement the standard Platform.runLater() pattern for creating a JavaFX scene
Loop through each top level file object in the Menu folder of the application file system:
Create a JavaFX Flow Pane for each file object
Recursively create JavaFX ButtonMenu items for submenus
Add ButtonMenu items to FlowPanes
Add FlowPane to JavaFX TitledPane
Add TitledPane to JavaFX Accordion component
Add Accordion to scene
So instead of Menus and SubMenus, we are using MenuButtons which can be recursively added to other MenuButtons and MenuItems.  The Accordion control gives us a space saving collapsible view with some nice animation.  The FlowPane makes it easy to layout the MenuButtons horizontally in a way that maximizes space.  Below is the code for my AccordionMenu class.  You will see where I borrowed heavily from Geertjan's example:

polaris.javafxwizard.jfxmenu;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Accordion;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TitledPane;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.FlowPane;
import javafx.scene.paint.Color;
import javax.swing.Action;
import javax.swing.SwingUtilities;
import org.openide.awt.Actions;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.util.Exceptions;

/**
 *
 * @author SPhillips (King of Australia)
 */
public class AccordionMenu extends JFXPanel{
    
    public Accordion accordionPane;
    public String transparentCSS = "-fx-background-color: rgba(0,100,100,0.1);";
    
    public AccordionMenu() {
        super();
        // create JavaFX scene
        Platform.setImplicitExit(false);
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                createScene(); //Standard Swing Interop Pattern
            }
        });        
    }
    private void createScene() {
        FileObject menuFolder = FileUtil.getConfigFile("Menu");
        FileObject[] menuKids = menuFolder.getChildren();
        //for each Menu folder need to create a TilePane and add it to an Accordion
        List<TitledPane> titledPaneList = new ArrayList<>();
        for (FileObject menuKid : FileUtil.getOrder(Arrays.asList(menuKids), true)) {
            //Build a Flow pane based on menu children
            //TOP level menu items should all be flow panes
            FlowPane flowPane = buildFlowPane(menuKid);
            flowPane.setStyle(transparentCSS);
            TitledPane newTitledPaneFromFileObject = new TitledPane(menuKid.getName(), flowPane);
            newTitledPaneFromFileObject.setAnimated(true);
            newTitledPaneFromFileObject.autosize();
            newTitledPaneFromFileObject.setStyle(transparentCSS);
            titledPaneList.add(newTitledPaneFromFileObject);
        }
        Group g = new Group();        
        Scene scene = new Scene(g, 400, 400,new Color(0.0,0.0,0.0,0.0));
        scene.setFill(null);
        g.setStyle(transparentCSS);
        accordionPane = new Accordion();
        accordionPane.setStyle(transparentCSS);
        accordionPane.getPanes().addAll(titledPaneList); 
        g.getChildren().add(accordionPane);
        setScene(scene);
        validate();
        this.setOpaque(true);
        this.setBackground(new java.awt.Color(0.0f, 0.0f, 0.0f, 0.0f));
    }
    private FlowPane buildFlowPane(FileObject fo) {
        //FlowPanes are made up of Buttons and MenuButtons built from actions and sub menus 
        FlowPane flowPane = new FlowPane(Orientation.HORIZONTAL,5,5);
        flowPane.setStyle(transparentCSS);
        //If anything at the Flow Pane level is an action we need to add it as a button
        //otherwise we can recursively build it as a MenuButton
        DataFolder df = DataFolder.findFolder(fo);
        DataObject[] childs = df.getChildren();
        for (DataObject oneChild : childs) {
            //If child is folder we need to build recursively
            if (oneChild.getPrimaryFile().isFolder()) {
                FileObject childFo = oneChild.getPrimaryFile();
                MenuButton newMenuButton = new MenuButton(childFo.getName());
                buildMenuButton(childFo, newMenuButton);
                flowPane.getChildren().add(newMenuButton);
            } else {
                Object instanceObj = FileUtil.getConfigObject(oneChild.getPrimaryFile().getPath(), Object.class);
                if (instanceObj instanceof Action) {
                    //If it is an Action we have reached an endpoint
                    final Action a = (Action) instanceObj;
                    String name = (String) a.getValue(Action.NAME);
                    String cutAmpersand = Actions.cutAmpersand(name);
                    Button buttonItem = new Button(cutAmpersand);
                    MenuEventHandler meh = new MenuEventHandler(a);
                    buttonItem.setOnAction(meh);
                    buttonItem.setEffect(new DropShadow());
                    flowPane.getChildren().add(buttonItem);
                }
            }
        }        
   
        return flowPane;
    }
    private void buildMenuButton(FileObject fo, MenuButton menuButton) {
        DataFolder df = DataFolder.findFolder(fo);
        DataObject[] childs = df.getChildren();
        for (DataObject oneChild : childs) {
            //If child is folder we need to build recursively
            if (oneChild.getPrimaryFile().isFolder()) {
                FileObject childFo = oneChild.getPrimaryFile();
                //Menu newMenu = new Menu(childFo.getName());
                MenuButton newMenuButton = new MenuButton(childFo.getName());
                //menu.getItems().add(newMenu);
                buildMenuButton(childFo, newMenuButton);
            } else {
                Object instanceObj = FileUtil.getConfigObject(oneChild.getPrimaryFile().getPath(), Object.class);
                if (instanceObj instanceof Action) {
                    //If it is an Action we have reached an endpoint
                    final Action a = (Action) instanceObj;
                    String name = (String) a.getValue(Action.NAME);
                    String cutAmpersand = Actions.cutAmpersand(name);
                    MenuItem menuItem = new MenuItem(cutAmpersand);
                    MenuEventHandler meh = new MenuEventHandler(a);
                    menuItem.setOnAction(meh);
                    menuButton.getItems().add(menuItem);
                }
            }
        }        
    }
    
    private class MenuEventHandler implements EventHandler<ActionEvent> {

        public Action theAction;
        
        public MenuEventHandler(Action action) {
            super();
            theAction = action;
        }
        
        @Override
        public void handle(final ActionEvent t) {
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    @Override
                    public void run() {
                        java.awt.event.ActionEvent event =
                                new java.awt.event.ActionEvent(
                                t.getSource(),
                                t.hashCode(),
                                t.toString());
                        theAction.actionPerformed(event);
                    }
                });
            } catch (    InterruptedException | InvocationTargetException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        
    }
}
I took the liberty of placing a few CSS stylings here and there, trying to play with the transparency.  Also I found that it looked better if a JavaFX Button was used for any Actions found at the very top level, instead of a MenuButton with a single item.

Step 4:  Build a Slide in TopComponent for the new AccordionMenu

Now that you have a JFXPanel Swing Interop component, your NetBeans Platform TopComponent doesn't need to know about JavaFX.  However in this scenario the Platform also is contributing via its wonderful docking framework.  Use the Window wizard and select Left Sliding In as a mode.  I would also advise making this component not closable, otherwise the user could lose the ability to use the menu.  Here are the annotations and constructor code in my TopComponent:

@ConvertAsProperties(
        dtd = "-//polaris.javafxwizard.jfxmenu//SlidingAccordion//EN",
        autostore = false)
@TopComponent.Description(
        preferredID = "SlidingAccordionTopComponent",
        iconBase="polaris/javafxwizard/jfxmenu/categories.png", 
        persistenceType = TopComponent.PERSISTENCE_ALWAYS)
@TopComponent.Registration(mode = "leftSlidingSide", openAtStartup = true)
@ActionID(category = "Window", id = "polaris.javafxwizard.jfxmenu.SlidingAccordionTopComponent")
@ActionReference(path = "Menu/JavaFX" /*, position = 333 */)
@TopComponent.OpenActionRegistration(
        displayName = "#CTL_SlidingAccordionAction",
        preferredID = "SlidingAccordionTopComponent")
@Messages({
    "CTL_SlidingAccordionAction=SlidingAccordion",
    "CTL_SlidingAccordionTopComponent=SlidingAccordion Window",
    "HINT_SlidingAccordionTopComponent=This is a SlidingAccordion window"
})
public final class SlidingAccordionTopComponent extends TopComponent {
    public AccordionMenu accordionMenu;
    public SlidingAccordionTopComponent() {
        initComponents();
        setName(Bundle.CTL_SlidingAccordionTopComponent());
        setToolTipText(Bundle.HINT_SlidingAccordionTopComponent());
        putClientProperty(TopComponent.PROP_CLOSING_DISABLED, Boolean.TRUE);
        putClientProperty(TopComponent.PROP_DRAGGING_DISABLED, Boolean.TRUE);
        putClientProperty(TopComponent.PROP_MAXIMIZATION_DISABLED, Boolean.TRUE);
        putClientProperty(TopComponent.PROP_UNDOCKING_DISABLED, Boolean.TRUE);
        putClientProperty(TopComponent.PROP_KEEP_PREFERRED_SIZE_WHEN_SLIDED_IN, Boolean.TRUE);
        setLayout(new BorderLayout());
        //Standard JFXPanel Swing Interop Pattern
        accordionMenu = new AccordionMenu();
        //transparency
        Color transparent = new Color(0.0f, 0.0f, 0.0f, 0.0f);
        accordionMenu.setOpaque(true);
        accordionMenu.setBackground(transparent);
        this.add(accordionMenu);
        this.setOpaque(true);
        this.setBackground(transparent);
    }

Step 5.  See how great it looks

We now have a slide out collapsible application menu provided by JavaFX components. These components can be "skinned" using CSS stylings and as such the menu can be crafted differently for different applications.  (By the way if anyone reading this has some ideas please contact me because I am not a CSS guy at all) 

Best of all we have adapted our application to work nicely with a Heads Up Display or Kiosk view that typically run on touchscreen computers.  This is because we have saved real estate and implemented an interface that is more condusive to single touches versus mouse drag events. 

Hey let's see how it might look with an application that needs all the space it can get?



Published at DZone with permission of Sean Phillips, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Sean Phillips replied on Sat, 2013/05/18 - 8:42am

 It should be noted that the module in this tutorial serves as a drop in plugin.  If you follow the steps in this tutorial properly you should have a plugin that if you drop it in any NetBeans Platform application will automagically convert the MenuBar to an AccordionMenu.

I'd like to extend this article with a follow-up on demonstrating interesting CSS stylings.  If anyone out there has a talent for that, either comment here or contact me directly.  I was hoping there would be a combination of CSS styles that would make the entire Accordion and all the Group children transparent to see through to the underlying Platform TopComponents.

Geertjan Wielenga replied on Mon, 2013/05/20 - 12:35am

Great article, many thanks for writing it, indeed in the context of maps and globes, this is a great solution.

Sean Phillips replied on Mon, 2013/05/20 - 10:08am in response to: Geertjan Wielenga

 Again your article  was the inspiration, thanks

I had some problems with the Accordion layout resizing itself occasionally which over time would cause  some of the Menu groups at the bottom to be hidden.  The problem can be solved by throwing it all into a JScrollPane but I wanted something that would be minimalistic.  I wanted to avoid showing any scroll bars. 

Also I'm not happy with the fixed width/height that I used as a quick solution.  It works but there is probably a better dynamic way of accomplishing the layout for an optimal fit.

Sean Phillips replied on Tue, 2013/06/18 - 8:36pm

 Small fix... when I started to exercise the accordion menu by adding many external plugins (that provide new menu options) I found that occasionally a FileObject such as a Menu/Spacer.Instance would come up in the FileSystem.  This can't be converted to a DataFolder and an exception is thrown.

Since we don't need the spacers anymore I think its safe to just drop those instances on the floor as we come to them.  below is the quick fix:


            try {
                //if this works we know its a data folder that can be loaded into our FlowPane
                DataFolder.findFolder(menuKid);
                FlowPane flowPane = buildFlowPane(menuKid);
                //flowPane.setStyle(transparentCSS);
                TitledPane newTitledPaneFromFileObject = new TitledPane(menuKid.getName(), flowPane);
                newTitledPaneFromFileObject.setAnimated(true);
                //newTitledPaneFromFileObject.setStyle(transparentCSS);
                titledPaneList.add(newTitledPaneFromFileObject);
                
            } catch (Exception ex) {
                //we couldn't load so drop it on the floor
            }

Muhammad Danial replied on Mon, 2014/03/31 - 8:20am

 That article was written by Geertjan Wielenga and it will become clear that much of the base code to accomplish this article was extended from Geertjan's example.

Fridge repair south london

Black Men replied on Fri, 2014/04/11 - 11:35am

 Wouldn't it be great to have the menu slide out from the edge of the screen only when you need it?  Well the NetBeans Platform provides slide-in TopComponents, of course, but a JMenu just isn't going to work out so well inside one.

Greg Aziz

Black Men replied on Sun, 2014/04/13 - 1:08am

 That article was written by Geertjan Wielenga and it will become clear that much of the base code to accomplish this article was extended from Geertjan's example.

soundcloud downloader

Samad Khatri replied on Tue, 2014/04/15 - 10:13am in response to: Sean Phillips

 World stem cell therapy in the USA and offshore are increasingly developing and soon will change the medical field we know. COPD stem cell treatments 

Black Men replied on Wed, 2014/04/16 - 8:14am

Removing the turbines also addressed noise and visual concerns at Lake Mead National Recreation Area; the National Park Service had asked BLM not to allow BP to place new turbines in the northwest corner.
audio software

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.