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

JFXtras RadialMenu for NetBeans RCP (Part 1)

06.28.2013
| 5262 views |
  • submit to reddit

This article will walk through adapting an open source JavaFX RadialMenu ( http://jfxtras.org/ ) to replace the NetBeans Platform system menu.  It will build upon previous tutorials that demonstrate complete replacements for the default NetBeans Swing components using JavaFX.  Code examples will feature NetBeans Platform, JavaFX and CSS. 

Part 1 of the article walks through some of the code upgrades necessary for the task.  As of this article's writing the open-source JFXtras RadialMenu needed a few upgrades to act as a system menu replacement for a NetBeans Platform RCP.  Part 2  will show the specific algorithm and code to then utilize the upgraded RadialMenu as described.  When complete you should be able to produce a slick radial system menu like this:

Previous tutorials in the Integrating JavaFX with the NetBeans Platform series we showed how to replace the NetBeans Swing MenuBar with JavaFX components.  You can find these tutorials here: 

And they are extensions themselves of these great articles written by Geertjan Wielenga:

To fully understand and follow this tutorial you will need to start with the previous tutorials mentioned above.

Getting Started
You will need download a recent version of the JFXtras-lab library.  All you really need is the artifact .jar.  For this adventure I used jfxtras-lab-2.2-r5.  Make a Wrapper Library plugin for the jar.  Later you will find that we will have actually change some of the classes, but this will get you started.

Adapt hidden TabDisplayer solution
Like the previous article example found here  a large overlay style JavaFX component will require that you embed it within a slide out control that has all Swing components transparent.  Like in most previous articles we will be using the standard JFXPanel interop pattern.  However this time we will be creating a JFXPanel/Scene component that stretches across the application similar to how the GlassPane component would work.

Create JFXRadialMenu that extends JFXPanel
We will extend the typical JFXPanel pattern by borrowing some of the JFXtras example code.  You can find the original sample by executing their Web Start Ensemble demo app located at: 

http://www.jfxtras.org/resources/Ensemble.jnlp

Below is the JFXRadialMenu class that we will start with:

public class JFXRadialMenu extends JFXPanel {
    protected RadialMenu radialMenu;
    protected boolean show;
    protected double lastOffsetValue;
    protected double lastInitialAngleValue;
    private double gestureAngle = 0;
    public Double menuSize = 15.0;
    public Double containerSize = 30.0;
    public Double initialAngle = -90.0;
    public Double innerRadius = 50.0;
    public Double radius = 150.0;
    public Double offset = 5.0;
    
    private Slider initialAngleSlider;

    public JFXRadialMenu () {
        super();
        Platform.setImplicitExit(false);
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                createScene(); //Standard Swing Interop Pattern
            }
        });         
    }
    
    private void createScene() {
        Group root = new Group();
        Scene scene = new Scene(root, 1200, 1000,new Color(0.0, 0.0, 0.0, 0.0));
        createRadialMenu();
        setRadialMenuHandlers();
        root.getChildren().add(radialMenu);
        setScene(scene);
        validate();
        setOpaque(true);
        setBackground(new java.awt.Color(0.0f, 0.0f, 0.0f, 0.0f));        
    }

    public void createRadialMenu() {
        Color slightlyTrans = new Color(1.0,1.0,1.0,0.6);
        
        final LinearGradient transBackground = LinearGradientBuilder
                .create()
                .startX(0)
                .startY(0)
                .endX(1.0)
                .endY(1.0)
                .cycleMethod(CycleMethod.NO_CYCLE)
                .stops(StopBuilder.create().offset(0.0).color(slightlyTrans)
                .build(),
                StopBuilder.create().offset(0.6)
                .color(slightlyTrans).build())
                .build();
        
        final LinearGradient backgroundMouseOn = LinearGradientBuilder
                .create()
                .startX(0)
                .startY(0)
                .endX(1.0)
                .endY(1.0)
                .cycleMethod(CycleMethod.NO_CYCLE)
                .stops(StopBuilder.create().offset(0.0).color(Color.LIGHTGREY)
                .build(),
                StopBuilder.create().offset(0.8)
                .color(Color.LIGHTGREY.darker()).build())
                .build();
        
        radialMenu = new RadialMenu(initialAngle, innerRadius, radius, offset, transBackground, backgroundMouseOn, 
                Color.DARKGREY.darker().darker(), Color.DARKGREY.darker(), 
                false, RadialMenu.CenterVisibility.ALWAYS, null);
        radialMenu.setTranslateX(400);
        radialMenu.setTranslateY(400);

    } 

    private void hideRadialMenu() {
         final FadeTransition fade = FadeTransitionBuilder.create()
                .node(this.radialMenu).fromValue(1).toValue(0)
                .duration(Duration.millis(300))
                .onFinished(new EventHandler<ActionEvent>() {
                    
            @Override
            public void handle(final ActionEvent arg0) {
                setVisible(false);
            }
        }).build();

        final ParallelTransition transition = ParallelTransitionBuilder
                .create().children(fade).build();

        transition.play();
    }

    private void showRadialMenu(final double x, final double y) {
        if (radialMenu.isVisible()) {
            lastInitialAngleValue = radialMenu.getInitialAngle();
            lastOffsetValue = radialMenu.getOffset();
            radialMenu.setVisible(false);
        }
        radialMenu.setTranslateX(x);
        radialMenu.setTranslateY(y);
        radialMenu.setVisible(true);

        final FadeTransition fade = FadeTransitionBuilder.create()
                .node(radialMenu).duration(Duration.millis(400))
                .fromValue(0).toValue(1.0).build();

        final Animation offset = new Timeline(new KeyFrame(Duration.ZERO,
                new KeyValue(radialMenu.offsetProperty(), 0)),
                new KeyFrame(Duration.millis(300), new KeyValue(radialMenu
                .offsetProperty(), lastOffsetValue)));

        final Animation angle = new Timeline(new KeyFrame(Duration.ZERO,
                new KeyValue(radialMenu.initialAngleProperty(),
                lastInitialAngleValue + 20)), new KeyFrame(
                Duration.millis(300), new KeyValue(
                radialMenu.initialAngleProperty(),
                lastInitialAngleValue)));

        final ParallelTransition transition = ParallelTransitionBuilder
                .create().children(fade, offset, angle).build();

        transition.play();
    }
}

This should add an empty Radial Menu to your application, similar to the image below:




Notice the small translucent circle in the upper left?  Congrats you have an empty RadialMenu.  Now we can add our menu items right following our typical recursive build pattern right?  Well what you will find is that the original RadialMenu components in the JFXtras library are quite setup for this.  Specifically the RadialMenuItem class does not support rendering a Text Label out of the box.  This makes some sense because Radial Menu's provide a difficulty of lining up text properly with arbitrary rotational angles of the Menu Items themselves.  What the class does support is rendering a graphic which is typical for radial type controls.
You will also find that the RadialContainerMenuItem does not support nested containers.  Specifically it lacks the code to assign mouse event handlers for adding these new nested container components.  This becomes a problem in larger menus that complex depth.  An example of this would be the NetBeans RCP Menu system.  In fact we can't even know how many MenuItems, Containers or Sub Menu levels we have at any given run so it has to be dynamically added. 


Add Some Test Code

So lets test out the default JFXtras RadialMenu object and observe the effects I mentioned above.  Add the following code to your createRadialMenu() method which should add several nested layers of Menu Items.

	RadialContainerMenuItem level1Container = new RadialContainerMenuItem(containerSize, "Level 1 Container", filler);                                
        RadialMenuItem level1Item = new RadialMenuItem(menuSize, "level 1 item", filler, null);
        
        RadialContainerMenuItem level2Container = new RadialContainerMenuItem(containerSize, "Level 2 Container", filler);                                
        RadialMenuItem level2Item = new RadialMenuItem(menuSize, "level 2 item", filler, null);
        
        RadialContainerMenuItem level3Container = new RadialContainerMenuItem(containerSize, "Level 3 Container", filler);                                
        RadialMenuItem level3Item = new RadialMenuItem(menuSize, "level 3 item", filler, null);
        
        RadialMenuItem level4Item = new RadialMenuItem(menuSize, "level 4 item", filler, null);
      
        //Add all your items in a nested order      
        level1Container.addMenuItem(level2Item);
        level1Container.addMenuItem(level2Container);
        
        level2Container.addMenuItem(level3Item);
        level2Container.addMenuItem(level3Container);

        level3Container.addMenuItem(level4Item);
        
	//Add top level items/containers to actual RadialMenu component
        radialMenu.addMenuItem(level1Item);
        radialMenu.addMenuItem(level1Container); 

It should just compile and work as it is the basic pattern that the good folks at JFXtras provide in their RadialMenu sample code.  Unfortunately you will not see nested menu containers past the first sublevel nor will you see actual text labels, which would be needed for a System Menu replacement.  You'll more likely see something like the screenshot below:





So what we need is to make some updates to the open source RadialMenuItem and RadialContainerMenuItem classes to accomplish our originally intended needs.  However if you were to be satisfied with the default implementation you could probably stop here.

Extend RadialMenuItem to support Text Labels

A quick fix for showing labels would be to extend the RadialMenuItem class and provide the support.  JavaFX makes this easy as we can build a Text object and attach it to the original component's node tree using binding.  All we need is some object or object property that is public and in the original Scene Graph node tree.  We then can render our new graphic object, in this case a simple Text object, by binding a translation to the original node's X,Y coordinates.  I call this "Parasitic Rendering" and to the user there is no perceived difference.  If anyone out there has a better term for this let me know. 
So here is my extended RadialMenuItemSP class code.  I provide a constructor adds an extra parameter for hiding the Menu Item graphic.

public class RadialMenuItemSP extends RadialMenuItem {

    private Text menuItemText = TextBuilder.create().text("").build();
    private Double textScaleX;
    private Double textScaleY;
    private Double textTranslateX;
    private Double textTranslateY;
    
    public RadialMenuItemSP(final double menuSize, final String text,
	    final Node graphic, final Boolean renderGraphic, final EventHandler<ActionEvent> actionHandler) {
        super(menuSize, graphic, actionHandler);
	super.text = text;
        menuItemText = TextBuilder.create()
                .fill(Color.BLUE)
                .managed(false)
                .textOrigin(VPos.CENTER)
                .onMouseEntered(new EventHandler<MouseEvent>() {
                    @Override
                    public void handle(MouseEvent t) {
                        menuItemText.setStroke(Color.POWDERBLUE);
                        menuItemText.setScaleX(textScaleX + (textScaleX*0.25));
                        menuItemText.setScaleY(textScaleY + (textScaleY*0.25));
                    }
                })
                .onMouseExited(new EventHandler<MouseEvent>() {
                    @Override
                    public void handle(MouseEvent t) {
                        menuItemText.setStroke(Color.BLUE);
                        menuItemText.setScaleX(textScaleX);
                        menuItemText.setScaleY(textScaleY);                        
                    }
                })
                .text(super.text)
                .build();
        textScaleX = menuItemText.getScaleX();
        textScaleY = menuItemText.getScaleY();
        menuItemText.setVisible(renderGraphic);
        super.getChildren().add(menuItemText);
        menuItemText.textProperty().bind(new SimpleStringProperty(super.text));
        this.graphic.setVisible(renderGraphic);
        this.redraw();
    }
    
    @Override
    protected void redraw() {
        super.redraw();
        if(null != menuItemText) {
            calculateTextXY();
            menuItemText.setTranslateX(textTranslateX);
            menuItemText.setTranslateY(textTranslateY);
        }
    }
    private void calculateTextXY() {
        final double graphicAngle = super.startAngle.get() + (super.menuSize / 2.0);
        final double radiusValue = this.radius.get();
        final double innerRadiusValue = this.innerRadius.get();
        final double graphicRadius = innerRadiusValue + (radiusValue - innerRadiusValue) / 2.0;
        final double textRadius = graphicRadius + (radiusValue - graphicRadius) / 2.0;
        textTranslateX =  textRadius * Math.cos(Math.toRadians(graphicAngle)) - (menuItemText.getBoundsInParent().getWidth()/2.0);
        
        if (!this.clockwise.get()) {   
            textTranslateY = -textRadius * Math.sin(Math.toRadians(graphicAngle)) - (menuItemText.getBoundsInParent().getHeight()/2.0);
        } else {
            textTranslateY = textRadius * Math.sin(Math.toRadians(graphicAngle)) - (menuItemText.getBoundsInParent().getHeight()/2.0);
        }
    }    
    
}

Notice the calculateTextXY() method that is used to determine where to translate the Text object to.  I borrowed most of those calculations from the JFXtras RadialMenuItem.computeCoordinates() method.

Upgrade RadialContainerMenuItem to support Nested Containers

So I wanted to do the same type of extension of the RadialContainerMenuItem class and get on with my original task.  However unfortunately the original JFXtras class has protected the inner objects I would need to @Override() in order to do this.  Maybe a better JavaFX developer than I out there can think of a simple means for extending the Container class like I'm about to show you.  If so please let me know.  Otherwise the only solution is to update the class code itself.
The upgrade is surprisingly simple.  All the RadialContainerMenuItem class needed was the following:


  • implement EventHandler<MouseEvent>
  • in addMenuItem() add this line : 
    item.setOnMouseClicked(this);
     
  •  in removeMenuItem add this line: 
    item.removeEventHandler(MouseEvent.MOUSE_CLICKED, this);
  • Add the necessary event handler:
        @Override
        public void handle(MouseEvent event) {
    	if (event.getButton() == MouseButton.PRIMARY) {
    	    final RadialMenuItem item = (RadialMenuItem) event.getSource();
    	    item.setSelected(!item.isSelected());
    	    for (final RadialMenuItem it : this.items) {
    		if (it != item) {
    		    it.setSelected(false);
    		}
    	    }
    	    event.consume();
    	}        
        }


Now every item, whether container or menu item will be assigned the proper MouseClick event handler.  This of course was the problem before... the nested containers were not recognizing MouseClick events and subsequently were not rendering their contained Menu Items. 

Adding this code and running with our original multi layer test code refactored to use the new classes we should get something like below:


Hey that is an UPGRAYEDD !  We can now render custom menus using text, without requiring images and these menus can be dynamically large or deep.  In Part 2  of this article we will explore how to leverage this in a NetBeans RCP application along with the Pro's and Con's of doing so.

 

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.)