Wednesday 9 April 2014

FX 2.2 to FX 8.0 Part 1 (3rd party controls)

Java FX 8.0 was released a few weeks ago and since then I have been working at ironing out the glitches in my application, as unfortunately it wasn't 100% compatible. So I thought I'd share my experience for others.

I encountered about 10 problems in getting the app to look and work correctly with FX 8.0, which I detail here in nine short blogs.

My first challenge was actually just getting the app to compile in FX 8.0 without errors !

The problem was that I had some 3rd party controls that were extending SkinBase, which was a non-public class in 2.2  It has now been made public in 8.0 but with API changes, so it isn't directly compatible. Depending on how the control handles any key or mouse events, converting from the one to the other isn't necessarily a straightforward exercise and results in the control only being able to work in FX 8.0 afterwards.  

After some thought I decided that I wanted my app to be able to work correctly in both FX 2.2 and 8.0, so that transitioning my user base over from the former to the latter didn't have to be an all or nothing scenario.  So the offending controls were changed to implement Skin instead of extending SkinBase (thankfully I had access to the control's source code, some of which I had previously customised). 

Implementing Skin is fairly easy and basically involves adding 3 methods which are mostly one liners (getSkinnable, getNode, and dispose), and then removing the super( X,Y ) call in the constructor where X is now returned by getSkinnable(). Also remove the getChildren().add( Z ) line if present, where Z is now returned by getNode().

In both cases, either implementing Skin or making changes for SkinBase, moving behavior is more complex and involves a bit of coding especially for handling any KeyEvent's.

Once I had reworked the offending controls the app successfully compiled :-), but my joy was short lived .... see part 2.

FX 2.2 to FX 8.0 Part 2 (stage issues)

.... so I'd successfully gotten my app to compile. Once the app has started and if the user is a Doctor then an authentication ("Login") dialog is shown. This is what it looks like in FX 2.2:


And this is how it initially looked like in FX 8.0

FX 8 ScenicView VBox highlighted


Basically its composed of a VBox in a TitledPane that is displayed in its own undecorated, modal Stage over the main app stage. Here's the code:
Stage passStage = new Stage( StageStyle.UNDECORATED );
passStage.initModality( Modality.WINDOW_MODAL );
passStage.initOwner( primaryStage );

Button okBut = new Button( "OK" );
Button exitBut = new Button( "Cancel" );

TilePane buttonBox = new TilePane(24,0);
buttonBox.setPadding( new Insets(6,0,6,0) );
buttonBox.getChildren().addAll( okBut, exitBut );
buttonBox.setAlignment( Pos.CENTER );

VBox passBox = new VBox();
passBox.setAlignment( Pos.CENTER );
passBox.setPadding( new Insets( 12,24,12,24 ) );

PasswordField passField = new PasswordField();
passBox.getChildren().addAll( new Label( "Enter Your ID" ), passField, buttonBox );

TitledPane  loginPane = new TitledPane( "Authentication", passBox );
loginPane.setCollapsible( false );

Scene  loginScene = new Scene( loginPane, 200, 110 );
passStage.setScene( loginScene );
passStage.show();

I found that if the Stage style is changed to decorated it displays blank until resized, however if the style is set to utility then the content displays correctly ?

FX 8 Decorated Stage
FX 8 Utility Stage













So with all these clues I fiddled around a bit and found that if I rather set the VBox size and leave out the Scene size it works, like so:
passBox.setPrefSize( 200, 110 );
Scene  loginScene = new Scene( loginPane , 200, 110 );
FX 8.0 Modena

Finally logged in and started to check out the scene .... see part 3.

FX 2.2 to FX 8.0 Part 3 (spacing & layout)


Ok, so now I can login and can start checking out various views and editors. Two things got me here:

1.  The first is that the vertical height of TextFields are different in Modena. So unless your layout can auto grow vertically things become a bit squashed, especially if its compact:


In FX 2.2 it looked like so:


I used a GridPane for the above layout but I hadn't specified a vgap, nor a top and bottom inset padding in FX 2.2 as it wasn't necessary. After adding a vgap="2.0" to GridPane,  and padding of top="4.0" and bottom="2.0" it looked better:


And with FX 8.0 Caspian it looks like so:







2.  The second snag was that because of the above some of the screens that I had designed when first using SceneBuilder and the default AnchorPane didn't adjust well to Modena. Labels and TextFields sometimes became ever so slightly out of line changing a GUI that was smart into something sloppy. Other times fields would creep up or encroach on one another because they were too close:


And this is how it looked in FX 2.2:


So some screens needed to be redone using GridPane and others I could just adjust the space between components, like this:



Next some shocking Tree behavior .... see part 4.

FX 2.2 to FX 8.0 Part 4 (trees & tables)

Next I checked how TreeView was holding up to the change to FX 8.0  At first everything looked great until I started collapsing and expanding nodes which then made a mess, see the last two items (they shouldn't even be there, but are leftovers from the previous two items):



This is how it looked in FX 2.2:



I figured something had changed in TreeCell so I had a look at the docs and Oracle tutorials (A BIG thank you for those :-) but actually didn't see anything new. So I decided to mimic one of their examples and see what I'd get. In the end it turns out that I wasn't correctly handling empty tree cells. (It maybe that FX 2.2 did this for me, but FX 8.0 sure wasn't.)  So this is very important for TreeCell as well as TableCell where I had a similar problem:

public void updateItem( Object item, boolean empty )
{
super.updateItem( item, empty );
if ( empty )
{
setGraphic( null );
setText( null );

}
else
{
setGraphic( itemGraphic );
setText( itemText );
}
}

The result after fixing things up:



Next up, where's my carrot ?  Sorry I meant caret .... see part 5.

FX 2.2 to FX 8.0 Part 5 (caret)

I have a sticky note like widget on one of my views. Its actually just a TextArea with some effects on it, here's how it looks in FX 2.2 when its empty:


When you click on it its supposed to receive focus and then the cursor or caret appears so that you know where you are typing, like so in FX 2.2:


But in FX 8.0 there's no caret, you can click all you want and it doesn't appear. So what's the user to think ?  Can I type or can't I type .... that is the question.


Well it turns out you can type and as soon as you start the caret appears. But I don't think that's a good user experience, they need feedback.

Technically what happens when you click on the text area is that some effects and css are applied to the node and "Click here to add Reminder" is replaced with an empty "" string, like so:
styleList = reminderNote.getStyleClass();
styleList.remove( "reminderoff" );
styleList.add( "reminderpresent" );
reminderNote.setEffect( shadowAndSepia ); // Sepia can't be specified in css ?
reminderNote.setText( "" );
An adequate solution I found was to just replace the empty string with a space instead: 
reminderNote.setText( " " );   // note the space
Problem solved, then I had some scroll pains .... see part 6.

FX 2.2 to FX 8.0 Part 6 (scrollpane)

I have a view in which data from previous appointments are displayed side by side in a grid. The entire view is comprised of a number of GridPanes, one of which forms a header grid at the top (think table column header). This GridPane is inside a ScrollPane synchronized with the main data grid's own ScrollPane, below it. In FX 2.2 a section of the header and main grid looks like so:


If you look carefully at the top right hand scroll bar you'll notice that the bottom arrow is missing. The top scroll pane is actually bigger than what is shown but its tucked away behind something. This didn't bother me in FX 2.2, to be honest I think I only noticed it now while doing this blog !  However in FX 8.0 this is no longer the case and it initially looked like so:


See how the bottom arrow of the top scroll bar is now visible but now there's a gap between the top header grid and the main grid under it. Well I didn't like it, the gap had to go !  I used ScenicView (thanks for this tool) to discover that the gap was coming from the scroll pane and not the grid pane. So I figured it must have a minimum or preferred height that is preventing it from being smaller. The answer was:  headerScroll.setMinHeight( 0 )  with the following result:


Unfortunately that isn't the end of my scroll pains. The main or center grid's scroll pane position is bound to a left hand row label grid scroll pane as well as the header pane above it: 
centerScroll.hvalueProperty().bindBidirectional( headerScroll.hvalueProperty() );
centerScroll.vvalueProperty().bindBidirectional( leftScroll.vvalueProperty() );
So if you pan or scroll around in the center pane its supposed to keep the row labels and column headers in sync. Well this works great in FX 2.2 but for some strange reason it sometimes becomes unhinged in FX 8.0 (especially when panning) and then the columns and rows aren't synchronized anymore, like so:



This is related to RT-35783 and is apparently fixed for FX 8u20 :-) 

The GridPane however was also "misbehaving" in this case .... see part 7.

FX 2.2 to FX 8.0 Part 7 (gridpane)

Carrying on from where I left off with my previous view. The user can double click on a row label on the left to include those rows in the header section:


Its like the freeze row functionality in a spreadsheet.  Well this works great in FX 2.2:


But in FX 8.0 the grids don't display or refresh correctly, note that the Doctor seen row is missing:


It turns out that in FX 8.0 I needed to explicitly invoke  headerGrid.autosize();  to get it to display correctly.


And now where's the action gone too ?  ....  see part 8.

FX 2.2 to FX 8.0 Part 8 (button action)

All buttons ready ?  Ok then sound, camera, action ..... sorry only space works now enter has been fired !?

In FX 2.2 when a button has focus and you press the Enter key it animated a press and fired an on action event. This doesn't seem to happen anymore in FX 8.0 !?  Only space works now. It doesn't matter if you've set the action in code,  button.setOnAction( someAction );  or if you've set it in FXML, onAction="#handleAction",  the result is the same - i.e. no action on Enter only Space.

Try it yourself, here's a code snippet to quickly test:
public class ButtonTest extends Application
{
   public void start( Stage primaryStage )
   {
       final Button but = new Button( "Press Me" );
        but.setOnAction( new EventHandler<ActionEvent>()
       {
           private int count = 1;
           public void handle( ActionEvent arg0 )
           {
               but.setText( "Button Pressed: "+ count++ );
           }
       } );

       BorderPane bp = new BorderPane();
       bp.setPadding( new Insets( 10 ) );
       bp.setCenter( but );

       Scene scene = new Scene( bp, 200, 50 );
       primaryStage.setScene( scene );
       primaryStage.show();

       but.requestFocus();
   }
   public static void main( String[] args )
   {
       launch( args );
   }
}
Apparently this change is by design, see RT-28779  Thankfully this isn't a trainsmash in my app, and where I wanted or needed Enter to work I simply added a key pressed handler,  button.setOnKeyPressed( enterAction );  or added the handler into my FXML with onKeyPressed="#handelEnter"  where the handle method is implemented as:

public void handle( KeyEvent KE )
{
   if ( KE.getCode() == KeyCode.ENTER )
   {
      if ( ! button.isArmed() ) button.fire();
   }
}

Note that the isArmed test is needed otherwise in FX 2.2 the action event will be executed twice.

Alternatively you can try  button.setDefaultButton( true )  instead of the above. Note however that the button style changes and that it responds to Enter even when not focused.

Next up, two minor TabPane issues .... see part 9.

FX 2.2 to FX 8.0 Part 9 (tabs & tabpane)

I have a calendar type control that has two vertical tab panes. The one has month tabs and the other day tabs, both on the left-hand side (none of the tabs have any content.) No preferred, min or max sizing was specified and this is how it looked in FX 2.2:



But in FX 8.0 the default calculated width of the tab panes differs from the above with a much more expanded result. (In the image below I've already started clamping the width so that I could show it here, otherwise it would have been too wide):


This seems to be related to RT-31744 (fixed for 8u20) where the width of the tabHeaderArea is included in the prefWidth calculation even when the tabs are vertical. I confirmed this by changing the number of month and day tabs to see if it affected the width, which it did. A temporary fix was to simply specify a maximum width for the two tab panes:



Lastly while trying out FX 8.0 Caspian I noticed a cosmetic change from Caspian FX 2.2 in that a selected tab now has a dotted border when focused:


If you don't like this, which I didn't, you can change it in your apps CSS file with the following entry:
.tab-pane:focused > .tab-header-area > .headers-region > .tab:selected .focus-indicator
{
    -fx-border-style: solid;
    -fx-border-insets: -2 -2 -4 -3;
    -fx-border-width: 1 1 0 1;
    -fx-border-radius: 2 2 0 0;
}
The above is a modified version from Modena, and results in the following in FX 8.0 Caspian:


And this is what is looks like when using Modena:


While this is the original Modena style:



Well that's it for now ....