JavaFX Tip 32: Need Icons? Use Ikonli!

Motivation

I have been coding JavaFX applications and libraries since 2013 and one thing they all had in common was that I needed to find good icons / graphics that I could use for them. As a former Swing developer I started by using image files, GIFs or PNGs. Normally I would license a library like the “O-Collection” from IconExperience (www.incors.com). But soon it became apparent to me that using image files is too painful.

Just imagine for a moment that you want to support different pseudo states of your nodes (e.g. “hover”, “pressed”, “focused”). You end up with a different version of the same icon for each state. Even more files are needed if you want to support different sizes (small, medium, large) or screen resolutions (e.g. “Retina Display” on Mac, 2x Icons). Ultimately you end up in image file hell.

At first I didn’t care that much, because I started with framework development. Projects like CalendarFX or FlexGanttFX required only a very small number of icons. So using PNG files for these libraries was not an issue. But once I started working on bigger projects the need to support hundreds of icons became clear.

Icon Fonts

Luckily for all of us the web has already come up with a solution for this problem, and the solution is called “Icon Fonts”. The big advantage of an icon font is the fact that all icons are contained within a single file. That makes managing them in your workspace very easy. The other advantage is that font icons can be styled via CSS. This way a single icon can be displayed in many different colors or sizes.

The most popular icon font, at least in the beginning, was FontAwesome and there is a JavaFX implementation for it called FontAwesomeFX by Jens Deters. I used this library for all of my projects for a very long time and never thought I needed anything else. That was until I stumbled over Andres Almiray’s excellent library called “Ikonli”. You can find it on GitHub. After that I used it for all of my JavaFX-related work. For my large applications but also for my libraries.

What I like about Ikonli is that it integrates so seamlessly with the existing JavaFX API. An icon is simply an extension of the “Text” node (duh!) and it comes with styleable properties. There are properties for the icon itself, the icon “code”, for its color and for its size. The names of these properties in CSS files also follows convention. There they are called -fx-icon-code, -fx-icon-color, and -fx-icon-size.

Integration

Ikonli does not just ship with FontAwesome but with a total of 31 (!) different fonts. Among them material design icons, weather icons, payment icons (credit cards, etc…). Each one of these comes in their own module / artefact and can be imported individually, e.g. via Maven dependencies. The following dependencies need to be added to your Maven project’s POM file if you want to use the Material Design icon font.

[fusion_builder_container hundred_percent=”yes” overflow=”visible”][fusion_builder_row][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

<dependencies>
    <dependency>
        <groupId>org.kordamp.ikonli</groupId>
        <artifactId>ikonli-javafx</artifactId>
        <version>11.3.5</version>
    </dependency>
</dependencies>

<dependency>
    <groupId>org.kordamp.ikonli</groupId>
    <artifactId>ikonli-materialdesign-pack</artifactId>
    <version>11.3.5</version>
</dependency>

Cheat Sheets

Icon fonts often come with a lot of icons in them. So finding the right one is hard. Ikonli makes this easy by providing a “cheat sheet” for each font. The one for “Material Design” icons can be seen below.

Coding

Once you have created a FontIcon node / instance you can use it anywhere in the JavaFX scenegraph. Below you see an example for setting it on a button via code.

[/fusion_builder_column][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

    Button button = new Button("User Account");
    button.setGraphic(new FontIcon());
    button.setId("account-button");

To style the icon you add the following to your CSS file:

[/fusion_builder_column][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

#account-button .ikonli-font-icon {
    -fx-icon-code: "mdi-account";
    -fx-icon-color: blue;
    -fx-icon-size: 1.2em;
}

To use an icon inside an FXML file you can write this:

[/fusion_builder_column][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

<Button text="User Account" id="account-button">
  <graphic>
     <FontIcon iconLiteral="mdi-account"/>
  </graphic>
<Button>

Custom Fonts

You can also create your own custom icon font for JavaFX based on Ikonli. Obviously the first thing you will need is the font file. There are several online services available that allow you to create such a font. They let you choose icons from various already existing fonts but they also let you upload your own SVG files. The one I used is called Fontello. It has three main areas: icon selection and / or upload, customize names, customize codes.

Once you have configured your icons properly online you can then download the result as a ZIP file. Inside the ZIP file you will find the icon font in various formats.

To verify that all of your icons are actually inside the font you can open the file via “Font Book” (on Mac). It should look something like this:

Icon Enum

To make this font available inside your JavaFX application you have to first implement an enumeration with a value for each icon. It will look similar to this:

[/fusion_builder_column][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

package com.acme.font;

import org.kordamp.ikonli.Ikon;

public enum MyIcon implements Ikon {
    HOUSE("my-icon-house", '\ue815'),
    CAR("my-icon-car", '\ue816'),
    DOG("my-icon-dog", '\ue817'),
    CAT("my-icon-cat", '\ue818'),
    KID("my-icon-kid", '\ue819');

    private String description;
    private char icon;

    MyIcon(String description, char icon) {
        this.description = description;
        this.icon = icon;
    }

    public String getDescription() {
        return description;
    }

    public char getCode() {
        return icon;
    }

    public static MyIcon findByDescription(String description) {
        for (MyIcon icon : values()) {
            if (icon.description.equals(description)) {
                return icon;
            }
        }
        throw new IllegalArgumentException("Icon not supported: " + description);
    }
}

Ikon Handler

The next thing you have to implement is an extension of AbstractIkonHandler. For this example where all icon literals are prefixed with “my-icon” the “supports” method returns exactly that prefix.

[/fusion_builder_column][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

package com.acme.font;
import org.kordamp.ikonli.AbstractIkonHandler;
import org.kordamp.ikonli.Ikon;

public class MyIkonliHandler extends AbstractIkonHandler {

    public boolean supports(String description) {
        return description != null && description.startsWith("my-icon-");
    }

    public Ikon resolve(String description) {
        return MyIcon.findByDescription(description);
    }

    public String getFontResourcePath() {
        return "com/acme/fonts/my-icons.ttf";
    }

    public String getFontFamily() {
        return "my-icons";
    }
}

Service Lookup

Now all that is left to do is to make the font available to the world. This is done via a service lookup, which means that you have to create a file called “org.kordamp.ikonli.IkonHandler” inside the folder META-INF/services. Inside this file you need to add the full class name of your handler class (com.acme.font.MyIkonliHandler).

Example

The following screenshot shows the font module we use in our Maven project. Your project should look similar to that.

That’s it! Happy coding everyone!

 [/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]

JavaFX / OpenJFX 11 Interview Series

I was recently interviewed by jaxenter.com regarding JavaFX 11 / OpenJFX. The interview was done together with Johan Vos, Jonathan Giles, and Donald Smith. The interview was published in three parts. They can be found here:

JavaFX Days Zurich – Sessions

The holidays are over, time to get serious, time to fill the session catalogue for the JavaFX Days Zurich (Website). We managed to convince some of the best JavaFX experts to come to Switzerland and to show us the things they work on or work with. So without further ado here is a list of the currently scheduled sessions:

 

JavaFX State of the Union

Wolfgang Weigend, Oracle Corp.

The current state of JavaFX UI development will be explained from the perspective of Oracle, with an inventory of existing development resources and the continuation of JavaFX in a free ecosystem. With the release of the OpenJFX and the decoupling of the Oracle JDK, the Java module system has created new possibilities for the integration of JavaFX modules into OpenJDK. The companies involved in the JavaFX ecosystem are creating additional functionalities and shaping the transition from active Oracle engineering to the year 2022, thereby securing the long-term technological viability of JavaFX. The organizational developer participation takes place via the OpenJDK and from there the open source software could be redistributed. Depending on the expertise of the respective development projects, the involved development companies offer independent support and thus close the gap to new innovative JavaFX features and customer requirements that go beyond the current state of development. JavaFX support is available with the commercial Java SE subscription for JDK 8 (LTS) until March 2025 and could be extended as desired.

Building Mobile Apps with Gluon

Johan Vos, Gluon

Building cross-platform mobile applications for iOS and Android with Java is fairly simple with the Gluon open source and commercial tools. Development teams can quickly build beautiful apps leveraging their Java skills, without extra budget or external teams. This session revisits the state of the latest developments (JDK, Gluon VM, JavaFX). It also demonstrates how you can build applications with one cross-platform Java API and deploy to mobile platforms with compelling UI, native services integration, and seamless connection with the cloud and enterprise back end. You will profit from improved security and common mobile features such as push notifications, authentication, and data synchronization or persistency, among others.

 

The JavaFX Ecosystem

Andres Almiray, Trivadis AG

For the past 7 years we have seen open source libraries and JavaFX projects popping up slowly, however the pace at which new projects appear has increased. We’ll cover a wide range of libraries that will ensure your next JavaFX project becomes a success. Make the most of layouts with MigPane. Spice up your control repertoire with JideFX, Medusa and ControlsFX. Change the looks of your application with the flick of a CSS switch, thanks to JFoenix and BootstrapFX. Decorate your screens with a wide variety of icons from Ikonli. And these are but a few of the libraries we’ll cover.

Developing Business Applications on top of e(fx)clipse

Tom Schindl, BestSolution

In this talk we’ll introduce you to e(fx)clipse which is a JavaFX application framework built on top of OSGi and the UI-agnostic parts of Eclipse 4. We’ll show you applications we built with customers around the world, ranging from 3d modeling tools to sophisticated form applications and PDF-Viewers.

The TilesFX and Medusa Frameworks

Gerrit Grunwald, Karakun, Blog: Harmonic Code

Gerrit presents his two open source frameworks TilesFX and Medusa. TilesFX is used for creating professional and sophisticated dashboards. Medusa delivers a huge set of custom controls that implement gauges, ideal for monitoring applications. Gerrit will share many tricks on how to accomplish eye candy effects.

Extreme GUI Makeover

Dirk Lemmermann, Hendrik Ebbers, Karakun, Blog: GuiGarage

Come and see Dirk and Hendrik take you step by step through the process of turning a dull movie database application into a sexy app filled with eye candy.

JPro in Production

Hans-Henry Sandbaek, Florian Kirmaier, Sandec, JPro One

This session will show how to develop and deploy with JPro, which enables Java programs to run in standard web browsers without a plugin.  By taking a deeper look into some real-world applications, the audience will learn how Java can be used for cross platform development, to write applications for not only desktops, but also for mobile devices and web browsers.  The audience will learn how a typical web page can be created with pure Java.  And the code for the web page runs not only in browsers, but also as native apps (desktops, iOS or Android).  A new portal, a web-based JavaFX ensemble, will be announced and presented, which already consolidates a number of prominent JavaFX libraries, such as ControlsFXand JFoenixand can hopefully serve as a common API Portal for many more libraries in time to come.  Attendees will also learn how to let Java code interoperate with currently popular web technologies such as Angular and React.

JavaFX Tip 31: Masking / Clipping / Alpha Channel

Selection Strip

I recently had to implement a custom control that lets the user select a single item out of a list of items. This “SelectionStrip” control had to lay out the items horizontally and in case of too many items allow the user to scroll horizontally left and right. The control was to be used in a space-constrained area, so the buttons for scrolling should only appear when needed. They also should not waste any additional space when showing. So I decided to place them on top of the control on the left and right sides. All of this was easily achieved, except that it was now difficult to distinguish the scroll buttons from the items. This can be seen in the three images below.



Alpha Channel?

So I thought it would be nice to somehow fade out the items when they are close to the left or right edge. This kind of behavior can normally be accomplished by using the alpha channel. It could decrease the opacity of pixels as their distance to the edges decreases. OK ….. but how is this done in JavaFX? For quite some time I was looking at the various “blend modes” that can be used to define how two overlapping nodes are drawn on top of each other. However, this was the wrong direction to look. As it turned out I already could have known how to do it because I once wrote a blog article talking about clipping and the difference between a filled and a non-filled clip. But I guess this was too long ago and I did not make the connection between “filled” and “filled with opacity less than 1”.

Complex Clip!

So far most of the clips I used for custom controls were simple rectangles. They usually made sure that children nodes that reached outside the layout bounds of their parent control were not or only partially visible. But this clip was different, it was more complex. It had to define three different zones. A “fade-in” area on the left-hand side, a “full opacity” area in the center, and a “fade-out” area on the right-hand side. For this to work I defined a “Group” that consists of three filled “Rectangle” nodes. While the fill color of the center rectangle is a solid black, the fill colors of the other two rectangles are linear gradients going from transparent to black and vice versa. The image below illustrates this.

With this setup we can now add any node as a child to the stack pane and it will be drawn with fade-in and fade-out effects on its sides.

Result

When applied to the “SelectionStrip” control from the beginning the scroll arrows / buttons are now always nicely visible and the overall user experience has become a little bit more pleasing. It is these little details that make the difference between a UI that is considered a “student project” or a “commercial application”. So sometimes it is really worth investing time in them.



Source Code

I put the masking logic into a custom control called “MaskedView”. At the bottom of this post you will see the Gist (or the link to the Gist) that contains the source code of this control. Think of it as a wrapper around a given content node.


import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public class MaskedView extends Control {
public MaskedView(Node content) {
setContent(content);
}
@Override
protected Skin<?> createDefaultSkin() {
return new MaskedViewSkin(this);
}
private final SimpleObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
public final Node getContent() {
return content.get();
}
public final SimpleObjectProperty<Node> contentProperty() {
return content;
}
public final void setContent(Node content) {
this.content.set(content);
}
private final DoubleProperty fadingSize = new SimpleDoubleProperty(this, "fadingSize", 120);
public final double getFadingSize() {
return fadingSize.get();
}
public final DoubleProperty fadingSizeProperty() {
return fadingSize;
}
public final void setFadingSize(double fadingSize) {
this.fadingSize.set(fadingSize);
}
}

view raw

MaskedView.java

hosted with ❤ by GitHub


import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Rectangle;
public class MaskedViewSkin extends SkinBase<MaskedView> {
private final Rectangle leftClip;
private final Rectangle rightClip;
private final Rectangle centerClip;
private final Group group;
private final StackPane stackPane;
public MaskedViewSkin(MaskedView view) {
super(view);
leftClip = new Rectangle();
rightClip = new Rectangle();
centerClip = new Rectangle();
centerClip.setFill(Color.BLACK);
leftClip.setManaged(false);
centerClip.setManaged(false);
rightClip.setManaged(false);
group = new Group(leftClip, centerClip, rightClip);
stackPane = new StackPane();
stackPane.setManaged(false);
stackPane.setClip(group);
getChildren().add(stackPane);
view.contentProperty().addListener((observable, oldContent, newContent) -> buildView(oldContent, newContent));
buildView(null, view.getContent());
view.widthProperty().addListener(it -> updateClip());
view.fadingSizeProperty().addListener(it -> updateClip());
}
private final InvalidationListener translateXListener = it -> updateClip();
private final WeakInvalidationListener weakTranslateXListener = new WeakInvalidationListener(translateXListener);
private void buildView(Node oldContent, Node newContent) {
if (oldContent != null) {
stackPane.getChildren().clear();
oldContent.translateXProperty().removeListener(weakTranslateXListener);
}
if (newContent != null) {
stackPane.getChildren().setAll(newContent);
newContent.translateXProperty().addListener(weakTranslateXListener);
}
updateClip();
}
private void updateClip() {
final MaskedView view = getSkinnable();
Node content = view.getContent();
if (content != null) {
final double fadingSize = view.getFadingSize();
if (content.getTranslateX() < 0) {
leftClip.setFill(new LinearGradient(0, 0, fadingSize, 0, false, CycleMethod.NO_CYCLE, new Stop(0, Color.TRANSPARENT), new Stop(1, Color.BLACK)));
} else {
leftClip.setFill(Color.BLACK);
}
if (content.getTranslateX() + content.prefWidth(-1) > view.getWidth()) {
rightClip.setFill(new LinearGradient(0, 0, fadingSize, 0, false, CycleMethod.NO_CYCLE, new Stop(0, Color.BLACK), new Stop(1, Color.TRANSPARENT)));
} else {
rightClip.setFill(Color.BLACK);
}
}
view.requestLayout();
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
final double fadingSize = Math.min(contentWidth / 2, getSkinnable().getFadingSize());
stackPane.resizeRelocate(snapPosition(contentX), snapPosition(contentY), snapSpace(contentWidth), snapSpace(contentHeight));
resizeRelocate(leftClip, snapPosition(contentX), snapPosition(contentY), snapSpace(fadingSize), snapSpace(contentHeight));
resizeRelocate(centerClip, snapPosition(contentX + fadingSize), snapPosition(contentY), snapSpace(contentWidth2 * fadingSize), snapSpace(contentHeight));
resizeRelocate(rightClip, snapPosition(contentX + contentWidthfadingSize), snapPosition(contentY), snapSpace(fadingSize), snapSpace(contentHeight));
}
private void resizeRelocate(Rectangle rect, double x, double y, double w, double h) {
rect.setLayoutX(x);
rect.setLayoutY(y);
rect.setWidth(w);
rect.setHeight(h);
}
}

I hope you will find a good use case for this control.

Happy coding everyone!

JavaFX Tip 30: ScrollPane with DropShadow

In one of my projects I recently noticed that it was hard for the user to see whether the content of a ScrollPane instance was currently scrolled or not. One way of making this more clear is to add a drop shadow to the top of the scroll pane. This is also something suggested by Google’s Material Design. So I gave it a try. In my solution I simply added a region to the ScrollPane and when laying it out I am moving it out of the viewport bounds of the ScrollPane so that only the shadow effect applied to the region still reaches into it. To really make sure that the region is not visible I also had to set a clip on the ScrollPane. This works quite well although I must admit I am not 100% sure this is the best way to do it. So if anybody has any suggestions / alternativ approaches then please leave a comment.

Below you see before and after scrolling screenshots of one of the screens of our application.

Before Scrolling

After Scrolling

BTW: I implemented this in such a way that the drop shadow doesn’t just appear all of a sudden, but it moves into the viewport step by step, depending on how far the user has scrolled. To see this you need to scroll down very slowly.

The code for the ShadowScrollPane can be found in this gist on GitHub:


package uk.co.senapt.desktop.shell;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
/**
* Created by lemmi on 23.08.17.
*/
public class ShadowScrollPane extends ScrollPane {
private Region shadow = new Region();
public ShadowScrollPane() {
super();
init();
}
public ShadowScrollPane(Node content) {
super(content);
init();
}
private void init() {
skinProperty().addListener(it -> {
getChildren().addAll(shadow);
});
setFitToWidth(true);
setVbarPolicy(ScrollBarPolicy.NEVER);
setHbarPolicy(ScrollBarPolicy.NEVER);
shadow.setManaged(false);
shadow.setStyle("-fx-pref-height: 10;" +
"-fx-background-color: black;" +
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .75), 20, 0.19, 0, 6);");
shadow.getStyleClass().add("shadow");
shadow.visibleProperty().bind(showShadowProperty());
shadow.setMouseTransparent(true);
shadow.visibleProperty().bind(vvalueProperty().greaterThan(0));
Rectangle clip = new Rectangle();
clip.widthProperty().bind(widthProperty());
clip.heightProperty().bind(heightProperty());
setClip(clip);
vvalueProperty().addListener(it -> {
if (lastOffset != computeOffset()) {
requestLayout();
}
});
showShadowProperty().addListener(it -> requestLayout());
}
private final BooleanProperty showShadow = new SimpleBooleanProperty(this, "showShadow", true);
public final BooleanProperty showShadowProperty() {
return showShadow;
}
public final boolean isShowShadow() {
return showShadow.get();
}
public final void setShowShadow(boolean show) {
showShadow.set(show);
}
private final int SHADOW_HEIGHT = 30;
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isShowShadow()) {
Insets insets = getInsets();
double w = getWidth();
double offset = computeOffset();
shadow.resizeRelocate(-10, insets.getTop() – shadow.prefHeight(-1) – SHADOW_HEIGHT + offset, w + 20, shadow.prefHeight(-1) – 1);
lastOffset = offset;
}
}
private double lastOffset = 0;
private double computeOffset() {
if (getContent() != null) {
return Math.min(getVvalue() * getContent().prefHeight(-1), SHADOW_HEIGHT);
}
return 0;
}
}

JavaFX Tip 29: Make Layouts Ignore Invisible Nodes

Back in the days when I was still implementing UIs in Swing I used to be a big fan of MigLayout (“one layout manager to rule them all”, right Mikael?). One of the features I really liked was the possibility to define different behaviors when a component became invisible. MigLayout allowed me to either preserve the space that the now invisible component occupied or to make it available for the still visible components. So how can I do this in JavaFX?

Even though the answer is quite simple it is not obvious by looking at the API. JavaFX uses layout panes such as VBox, HBox, BorderPane, FlowPane, or GridPane, to lay out managed children nodes. The keyword here is “managed”. The layout panes only consider those nodes inside their layout algorithms that are flagged as managed (default is true). The same is true for the the code that computes the pref, min, max widths of a pane. This code also only considers managed nodes.

Let’s look at an example. We create an HBox with four labels. Initially it looks like this.

We now set the visibility of label 2 to false and we receive this layout.

To reuse the space that used to be occupied by the label we now set the “managed” property of label 2 to false. As you can see below the remaining three labels are now filling the entire width of the HBox and the width of the HBox was adjusted properly.

That’s it for today. May the code be with you!

P.S.: the demo code is below


package uk.co.senapt.desktop;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* A little demo showing how the "visible" and "managed" property can be used
* to make a node disappear in such a way that the layout pane / container can
* reclaim the space previously occupied by the now invisible node.
*/
public class VisibleManagedDemo extends Application {
@Override
public void start(Stage primaryStage) {
Label label1 = createLabel("Label 1");
Label label2 = createLabel("Label 2");
Label label3 = createLabel("Label 3");
Label label4 = createLabel("Label 4");
CheckBox visibleBox = new CheckBox("Visible");
CheckBox managedBox = new CheckBox("Managed");
visibleBox.setSelected(true);
managedBox.setSelected(true);
label2.visibleProperty().bind(visibleBox.selectedProperty());
label2.managedProperty().bind(managedBox.selectedProperty());
HBox hBox1 = new HBox(10, new Label("Label 2 settings:"), visibleBox, managedBox);
HBox hBox2 = new HBox(10, label1, label2, label3, label4);
hBox2.setStyle("-fx-background-color: lightgray; -fx-padding: 20");
VBox vBox = new VBox(20, hBox1, hBox2);
vBox.setFillWidth(false);
vBox.setPadding(new Insets(20));
primaryStage.setTitle("Visible / Managed Demo");
primaryStage.setScene(new Scene(vBox));
primaryStage.sizeToScene();
primaryStage.centerOnScreen();
primaryStage.show();
}
private Label createLabel(String text) {
Label label = new Label(text);
label.setStyle("-fx-background-color: orange; -fx-background-radius: 4; -fx-padding: 20;");
label.setPrefSize(200, 200);
return label;
}
public static void main(String[] args) {
launch();
}
}

 

CalendarFX goes Open Source

I am happy to announce that CalendarFX has found a new home on GitHub. As of today it is no longer a commercial product but an open source project. Over the last two years I have noticed that the developer community has resisted to use CalendarFX for their projects as the source code was not freely available. So now that CalendarFX has been released into the wild I am hoping that it will live a long and happy live.

New kid on the blog: FormsFX

I am very happy to announce the immediate availability of FormsFX, a framework for (surprise!) creating forms in JavaFX. FormsFX is the result of a student project at the “Fachhochschule Nordwestschweiz” (FHNW) in Switzerland. The project was initiated and sponsored by me. I believe that the result of this project is of high quality and that other projects should be able to benefit from it, hence I am contributing it as an open source project to the Java community today.

The source code and documentation can be found on GitHub at this location: https://github.com/dlemmermann/formsfx

A binary distribution (JAR file) can be downloaded from my website.

Credits

The design and the implementation were done by Sacha Schmid and Rinesch Murugathas as part of their studies for their bachelor degree.

View, Model View, Model

The form renderer of FormsFX will automatically create a form for you based on a view model. The view model defines bindings to the properties of the domain model / business model. The renderer utilises a twelve column layout (similar to bootstrap). Labels and their fields can be placed on one or more of those columns. Each field defines its own column span. If there is not enough space left for a field then it will be automatically placed on the next row. The screenshot below shows you the form created by the demo application that ships with FormsFX.

 

Fluent API

The fluent API of FormsFX allows you to create a view model for a form that consists of several groups and sections. A group and a section can consist of several fields. The difference between a group and a section is simply the additional title property and the fact that sections can be expanded / collapsed. The code below shows how the first three fields of the screenshot above were defined.

[fusion_builder_container hundred_percent=”yes” overflow=”visible”][fusion_builder_row][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

Form form = Form.of(
        Group.of(
                Field.ofStringType(country.nameProperty())
                     .label("country_label")
                     .placeholder("country_placeholder")
                     .required("required_error_message")
                     .validate(StringLengthValidator.atLeast(2, "country_error_message")),
                Field.ofStringType(country.isoProperty())
                     .label("ISO_3166_label")
                     .placeholder("ISO_3166_placeholder")
                     .required("required_error_message")
                     .validate(StringLengthValidator.exactly(2, "ISO_3166_error_message")),
                Field.ofBooleanType(country.independenceProperty())
                     .label("independent_label")
                     .required("required_error_message")
        )).title("My Form").i18n(resourceBundle);

Fields

The following things can be defined for each field:

  • the type (string, number, boolean, ….)
  • a label
  • a placeholder text
  • whether the field is required
  • one or more validators
  • a tooltip
  • a column span
  • a format (e.g. decimal numbers)
  • a renderer (normally default used, e.g. textfield for text)
  • single- or multiline
  • editable or not
  • CSS style classes
  • an ID

i18n

Any form can also be associated with a resource bundle. This allows the application to switch field labels on the fly.

So please take FormsFX for a spin and let us know what you think.

Happy coding everyone![/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]

JavaFX Tip 28: Pretty List View

When I look at the list views on my mobile phone I always notice that they display their scrollbar (normally a vertical one) very differently than JavaFX does. The same is true for applications running on MacOS X. Below you can see a snapshot of Apple’s calendar app. You will notice that the scrollbar is actually on top of the content.

In JavaFX the scrollbar would be placed to the right of the view. However on mobile devices in order to save space the scrollbar shows up on top of the content.

So obviously I wanted to see how I could achieve the same thing for my JavaFX apps. The trick to do this is to first “style away” the original scrollbars. Then you add your own scrollbar and lay it out in such a way that it will be on top of the content (the ListView). Next you style your own scrollbar to look nice and lightweight, and finally you bind your scrollbar to the original scrollbar.

In the end the result will look like this:

Below you can see the class PrettyListView, which extends a standard ListView.

[fusion_builder_container hundred_percent=”yes” overflow=”visible”][fusion_builder_row][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;

import java.util.Set;

public class PrettyListView<T> extends ListView<T> {

    private ScrollBar vBar = new ScrollBar();
    private ScrollBar hBar = new ScrollBar();

    public PrettyListView() {
        super();

        skinProperty().addListener(it -> {
            // first bind, then add new scrollbars, otherwise the new bars will be found
            bindScrollBars();
            getChildren().addAll(vBar, hBar);
        });

        getStyleClass().add("pretty-list-view");

        vBar.setManaged(false);
        vBar.setOrientation(Orientation.VERTICAL);
        vBar.getStyleClass().add("pretty-scroll-bar");
        vBar.visibleProperty().bind(vBar.visibleAmountProperty().isNotEqualTo(0));

        hBar.setManaged(false);
        hBar.setOrientation(Orientation.HORIZONTAL);
        hBar.getStyleClass().add("pretty-scroll-bar");
        hBar.visibleProperty().bind(hBar.visibleAmountProperty().isNotEqualTo(0));
    }

    private void bindScrollBars() {
        final Set<Node> nodes = lookupAll("VirtualScrollBar");
        for (Node node : nodes) {
            if (node instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) node;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    bindScrollBars(vBar, bar);
                } else if (bar.getOrientation().equals(Orientation.HORIZONTAL)) {
                    bindScrollBars(hBar, bar);
                }
            }
        }
    }

    private void bindScrollBars(ScrollBar scrollBarA, ScrollBar scrollBarB) {
        scrollBarA.valueProperty().bindBidirectional(scrollBarB.valueProperty());
        scrollBarA.minProperty().bindBidirectional(scrollBarB.minProperty());
        scrollBarA.maxProperty().bindBidirectional(scrollBarB.maxProperty());
        scrollBarA.visibleAmountProperty().bindBidirectional(scrollBarB.visibleAmountProperty());
        scrollBarA.unitIncrementProperty().bindBidirectional(scrollBarB.unitIncrementProperty());
        scrollBarA.blockIncrementProperty().bindBidirectional(scrollBarB.blockIncrementProperty());
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();

        Insets insets = getInsets();
        double w = getWidth();
        double h = getHeight();
        final double prefWidth = vBar.prefWidth(-1);
        vBar.resizeRelocate(w - prefWidth - insets.getRight(), insets.getTop(), prefWidth, h - insets.getTop() - insets.getBottom());

        final double prefHeight = hBar.prefHeight(-1);
        hBar.resizeRelocate(insets.getLeft(), h - prefHeight - insets.getBottom(), w - insets.getLeft() - insets.getRight(), prefHeight);
    }
}

The CSS looks like this:

[/fusion_builder_column][fusion_builder_column type=”1_1″ background_position=”left top” background_color=”” border_size=”” border_color=”” border_style=”solid” spacing=”yes” background_image=”” background_repeat=”no-repeat” padding=”” margin_top=”0px” margin_bottom=”0px” class=”” id=”” animation_type=”” animation_speed=”0.3″ animation_direction=”left” hide_on_mobile=”no” center_content=”no” min_height=”none”]

.pretty-list-view > .virtual-flow > .scroll-bar,
.pretty-list-view > .virtual-flow > .scroll-bar .decrement-arrow,
.pretty-list-view > .virtual-flow > .scroll-bar .increment-arrow,
.pretty-list-view > .virtual-flow > .scroll-bar .decrement-button,
.pretty-list-view > .virtual-flow > .scroll-bar .increment-button {
    -fx-pref-width: 0;
    -fx-pref-height: 0;
}

.pretty-list-view .pretty-scroll-bar:vertical .thumb {
    -fx-background-insets: 0 2 0 0;
}

.pretty-list-view .pretty-scroll-bar:horizontal .thumb {
    -fx-background-insets: 0 0 2 0;
}

.pretty-list-view .pretty-scroll-bar .decrement-arrow,
.pretty-list-view .pretty-scroll-bar .increment-arrow {
    -fx-pref-width: 0;
    -fx-pref-height: 0;
}

.pretty-list-view .pretty-scroll-bar {
    -fx-background-color: transparent;
    -fx-pref-width: 12;
    -fx-pref-height: 12;
    -fx-padding: 2;
}

.pretty-list-view .pretty-scroll-bar .thumb {
    -fx-background-color: rgba(0, 0, 0, .2);
    -fx-background-radius: 1000;
    -fx-pref-width: 12;
    -fx-pref-height: 12;
}

[/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]