Category Archives: JavaFX

JavaFX Tip 16: Undecorated & Transparent Stages

Last week I had some fun playing around with the “Undecorator” classes from Arnaud Nouard. I was looking into it because I was thinking about writing a JavaFX showcase application that integrates / lists the various resources available for JavaFX development. My goal for this application is to come up with something very slick and sexy, something unconventional, something with a lot of animations and cool effects. So I started by taking over full control of the appearance of the stage by using “Undecorator”.

With the apperance of Yosemite transparent stages / windows are the way to go so I wanted that, too. I modified Arnaud’s CSS files to make the window transparent and then added a pane that contains 12 large icons for a menu.

The icons are animated when the user clicks on one of them. They shrink and are relocated to the toolbar area at the top of the window. This is done via the standard Timeline class of JavaFX. The key values added to the timeline modify the layout x and y coordinates of the icons based on target bounds that are calculated whenever the width or height of the stage changes.

The result can be seen in the video below.

 

 

 

 

JavaFX Resources

I have started putting together a list of JavaFX-related resources such as blogs, open source projects, frameworks, commercial products, etc… You can find it on my company website. I just started this list and will refine it over time. I would very much appreciate it if others send me suggestions on what to add to it.

JavaFX Tip 15: ListView Autoscrolling

I recently had to implement autoscrolling functionality for FlexGanttFX and thought that my solution might be useful for others. You find the basic concepts of it in the listing below. The main idea is that a background thread is used to adjust the pixel location of the virtual flow node used by the list view. The thread starts when a drag over is detected “close” to the top or bottom edges. “Close” is defined by a proximity variable.

This code can obviously be improved by using a property for the proximity value and the types “Task” and “Service” for the threading work.

package com.dlsc;

import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.ListView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;

/*
 * Yes, unfortunately we need to use private API for this.
 */
import com.sun.javafx.scene.control.skin.VirtualFlow;

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

	final double proximity = 20;

	public AutoscrollListView() {
		addEventFilter(MouseEvent.DRAG_DETECTED,
                      evt -> startDrag());
		addEventFilter(DragEvent.DRAG_OVER,
                      evt -> autoscrollIfNeeded(evt));
		addEventFilter(DragEvent.DRAG_EXITED,
                      evt -> stopAutoScrollIfNeeded(evt));
		addEventFilter(DragEvent.DRAG_DROPPED,
                      evt -> stopAutoScrollIfNeeded(evt));
		addEventFilter(DragEvent.DRAG_DONE,
                      evt -> stopAutoScrollIfNeeded(evt));
	}

	private void startDrag() {
		Dragboard db = startDragAndDrop(TransferMode.MOVE);
		ClipboardContent content = new ClipboardContent();

		/*
		 * We have to add some content, otherwise drag over
                 * will not be called.
		 */
		content.putString("dummy");
		db.setContent(content);
	}

	private void autoscrollIfNeeded(DragEvent evt) {
		evt.acceptTransferModes(TransferMode.ANY);

		/*
		 * Determine the "hot" region that will trigger automatic scrolling.
		 * Ideally we use the clipped container of the list view skin but when
		 * the rows are empty the dimensions of the clipped container will be
		 * 0x0. In this case we try to use the virtual flow.
		 */
		Region hotRegion = getClippedContainer();
		if (hotRegion.getBoundsInLocal().getWidth() < 1) {
			hotRegion = this;
			if (hotRegion.getBoundsInLocal().getWidth() < 1) {
				stopAutoScrollIfNeeded(evt);
				return;
			}
		}

		double yOffset = 0;

		// y offset

		double delta = evt.getSceneY() -
                                  hotRegion.localToScene(0, 0).getY();
		if (delta < proximity) {
			yOffset = -(proximity - delta);
		}

		delta = hotRegion.localToScene(0, 0).getY() +
                                  hotRegion.getHeight() -
				  evt.getSceneY();
		if (delta < proximity) {
			yOffset = proximity - delta;
		}

		if (yOffset != 0) {
			autoscroll(yOffset);
		} else {
			stopAutoScrollIfNeeded(evt);
		}
	}

	private VirtualFlow<?> getVirtualFlow() {
		return (VirtualFlow<?>) lookup("VirtualFlow");
	}

	private Region getClippedContainer() {

		/*
		 * Safest way to find the clipped container. lookup() does not work at
		 * all.
		 */
		for (Node child :
                             getVirtualFlow().getChildrenUnmodifiable()) {
			if (child.getStyleClass().
                                       contains("clipped-container")) {
				return (Region) child;
			}
		}

		return null;
	}

	class ScrollThread extends Thread {
		private boolean running = true;
		private double yOffset;

		public ScrollThread() {
			super("Autoscrolling List View");
			setDaemon(true);
		}

		@Override
		public void run() {

			/*
			 * Some initial delay, especially useful when
                         * dragging something in from the outside.
			 */

			try {
				Thread.sleep(300);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}

			while (running) {

				Platform.runLater(() -> {
					scrollY();
				});

				try {
					sleep(15);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		private void scrollY() {
			VirtualFlow<?> flow = getVirtualFlow();
			flow.adjustPixels(yOffset);
		}

		public void stopRunning() {
			this.running = false;
		}

		public void setDelta(double yOffset) {
			this.yOffset = yOffset;
		}
	}

	private ScrollThread scrollThread;

	private void autoscroll(double yOffset) {
		if (scrollThread == null) {
			scrollThread = new ScrollThread();
			scrollThread.start();
		}

		scrollThread.setDelta(yOffset);
	}

	private void stopAutoScrollIfNeeded(DragEvent evt) {
		if (scrollThread != null) {
			scrollThread.stopRunning();
			scrollThread = null;
		}
	}
}

2015: The Year When JavaFX Takes Over

trend

 

I must say that I very much enjoy using Google trends to evaluate the importance of a technology and to see what its future will look like. So today I ran a comparison between “Java Swing” and “JavaFX”. The result shows two things: once we enter the next year JavaFX will be more relevant than Swing but JavaFX will not be that relevant either.

I think this is totally wrong.

Based on the feedback I am getting from the community and also on the projects that I had contact with I foresee a rapid increase in JavaFX usage (and Google searches) over the next 6 to 9 months.

The main reason is the fact that a lot of companies will finally upgrade to Java 8 next year, and JavaFX has really only reached its full potential with this latest Java release. The second reason is the increased availability of third party solutions: open source projects like ControlsFX and JFXtras, and some commercial products (like mine: FlexGanttFX and soon FlexCalendarFX). The third reason will be the availability of JavaFX 8 on mobile devices (Android and iOS). Once managers see that they can ask their developers to use a single UI toolkit for desktop and mobile  development there will be no holding back.

So that’s my prognosis, please make sure to check back in Q2 2015 to see if I was right or wrong.

New Custom Control: TaskProgressView

I have written a new custom control and commited it to the ControlsFX project. It is a highly specialized control for showing a list of background tasks, their current status and progress. This is actually the first control I have written for ControlsFX just for the fun of it, meaning I do not have a use case for it myself (but sure one will come eventually). The screenshot below shows the control in action.

task-monitor

If you are already familiar with the javafx.concurrent.Task class you will quickly grasp that the control shows the value of its title, message, and progress properties. But it also shows an icon, which is not covered by the Task API. I have added an optional graphics factory (a callback) that will be invoked for each task to lookup a graphic node that will be placed on the left-hand side of the list view cell that represents the task.

A video showing the control in action can be found here:

The Control

Since this control is rather simple I figured it would make sense to post the entire source code for it so that it can be used for others to study. The following listing shows the code of the control itself. As expected it extends the Control class and provides an observable list for the monitored tasks and an object property for the graphics factory (the callback).

package org.controlsfx.control;

import impl.org.controlsfx.skin.TaskProgressViewSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.util.Callback;

/**
 * The task progress view is used to visualize the progress of long running
 * tasks. These tasks are created via the {@link Task} class. This view
 * manages a list of such tasks and displays each one of them with their
 * name, progress, and update messages.<p>
 * An optional graphic factory can be set to place a graphic in each row.
 * This allows the user to more easily distinguish between different types
 * of tasks.
 *
 * <h3>Screenshots</h3>
 * The picture below shows the default appearance of the task progress view
 * control:
 * <center><img src="task-monitor.png" /></center>
 *
 * <h3>Code Sample</h3>
 *
 * <pre>
 * TaskProgressView&lt;MyTask&gt; view = new TaskProgressView&lt;&gt;();
 * view.setGraphicFactory(task -> return new ImageView("db-access.png"));
 * view.getTasks().add(new MyTask());
 * </pre>
 */
public class TaskProgressView<T extends Task<?>> extends Control {

    /**
     * Constructs a new task progress view.
     */
    public TaskProgressView() {
        getStyleClass().add("task-progress-view");

        EventHandler<WorkerStateEvent> taskHandler = evt -> {
            if (evt.getEventType().equals(
                    WorkerStateEvent.WORKER_STATE_SUCCEEDED)
                    || evt.getEventType().equals(
                            WorkerStateEvent.WORKER_STATE_CANCELLED)
                    || evt.getEventType().equals(
                            WorkerStateEvent.WORKER_STATE_FAILED)) {
                getTasks().remove(evt.getSource());
            }
        };

        getTasks().addListener(new ListChangeListener<Task<?>>() {
            @Override
            public void onChanged(Change<? extends Task<?>> c) {
                while (c.next()) {
                    if (c.wasAdded()) {
                        for (Task<?> task : c.getAddedSubList()) {
                            task.addEventHandler(WorkerStateEvent.ANY,
                                    taskHandler);
                        }
                    } else if (c.wasRemoved()) {
                        for (Task<?> task : c.getAddedSubList()) {
                            task.removeEventHandler(WorkerStateEvent.ANY,
                                    taskHandler);
                        }
                    }
                }
            }
        });
    }

    @Override
    protected Skin<?> createDefaultSkin() {
        return new TaskProgressViewSkin<>(this);
    }

    private final ObservableList<T> tasks = FXCollections
            .observableArrayList();

    /**
     * Returns the list of tasks currently monitored by this view.
     *
     * @return the monitored tasks
     */
    public final ObservableList<T> getTasks() {
        return tasks;
    }

    private ObjectProperty<Callback<T, Node>> graphicFactory;

    /**
     * Returns the property used to store an optional callback for creating
     * custom graphics for each task.
     *
     * @return the graphic factory property
     */
    public final ObjectProperty<Callback<T, Node>> graphicFactoryProperty() {
        if (graphicFactory == null) {
            graphicFactory = new SimpleObjectProperty<Callback<T, Node>>(
                    this, "graphicFactory");
        }

        return graphicFactory;
    }

    /**
     * Returns the value of {@link #graphicFactoryProperty()}.
     *
     * @return the optional graphic factory
     */
    public final Callback<T, Node> getGraphicFactory() {
        return graphicFactory == null ? null : graphicFactory.get();
    }

    /**
     * Sets the value of {@link #graphicFactoryProperty()}.
     *
     * @param factory an optional graphic factory
     */
    public final void setGraphicFactory(Callback<T, Node> factory) {
        graphicFactoryProperty().set(factory);
    }

The Skin

As you might have expected the skin is using a ListView with a custom cell factory  to display the tasks.

package impl.org.controlsfx.skin;

import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.util.Callback;

import org.controlsfx.control.TaskProgressView;

import com.sun.javafx.css.StyleManager;

public class TaskProgressViewSkin<T extends Task<?>> extends
        SkinBase<TaskProgressView<T>> {

    static {
        StyleManager.getInstance().addUserAgentStylesheet(
                TaskProgressView.class
                        .getResource("taskprogressview.css").toExternalForm()); //$NON-NLS-1$
    }

    public TaskProgressViewSkin(TaskProgressView<T> monitor) {
        super(monitor);

        BorderPane borderPane = new BorderPane();
        borderPane.getStyleClass().add("box");

        // list view
        ListView<T> listView = new ListView<>();
        listView.setPrefSize(500, 400);
        listView.setPlaceholder(new Label("No tasks running"));
        listView.setCellFactory(param -> new TaskCell());
        listView.setFocusTraversable(false);

        Bindings.bindContent(listView.getItems(), monitor.getTasks());
        borderPane.setCenter(listView);

        getChildren().add(listView);
    }

    class TaskCell extends ListCell<T> {
        private ProgressBar progressBar;
        private Label titleText;
        private Label messageText;
        private Button cancelButton;

        private T task;
        private BorderPane borderPane;

        public TaskCell() {
            titleText = new Label();
            titleText.getStyleClass().add("task-title");

            messageText = new Label();
            messageText.getStyleClass().add("task-message");

            progressBar = new ProgressBar();
            progressBar.setMaxWidth(Double.MAX_VALUE);
            progressBar.setMaxHeight(8);
            progressBar.getStyleClass().add("task-progress-bar");

            cancelButton = new Button("Cancel");
            cancelButton.getStyleClass().add("task-cancel-button");
            cancelButton.setTooltip(new Tooltip("Cancel Task"));
            cancelButton.setOnAction(evt -> {
                if (task != null) {
                    task.cancel();
                }
            });

            VBox vbox = new VBox();
            vbox.setSpacing(4);
            vbox.getChildren().add(titleText);
            vbox.getChildren().add(progressBar);
            vbox.getChildren().add(messageText);

            BorderPane.setAlignment(cancelButton, Pos.CENTER);
            BorderPane.setMargin(cancelButton, new Insets(0, 0, 0, 4));

            borderPane = new BorderPane();
            borderPane.setCenter(vbox);
            borderPane.setRight(cancelButton);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        }

        @Override
        public void updateIndex(int index) {
            super.updateIndex(index);

            /*
             * I have no idea why this is necessary but it won't work without
             * it. Shouldn't the updateItem method be enough?
             */
            if (index == -1) {
                setGraphic(null);
                getStyleClass().setAll("task-list-cell-empty");
            }
        }

        @Override
        protected void updateItem(T task, boolean empty) {
            super.updateItem(task, empty);

            this.task = task;

            if (empty || task == null) {
                getStyleClass().setAll("task-list-cell-empty");
                setGraphic(null);
            } else if (task != null) {
                getStyleClass().setAll("task-list-cell");
                progressBar.progressProperty().bind(task.progressProperty());
                titleText.textProperty().bind(task.titleProperty());
                messageText.textProperty().bind(task.messageProperty());
                cancelButton.disableProperty().bind(
                        Bindings.not(task.runningProperty()));

                Callback<T, Node> factory = getSkinnable().getGraphicFactory();
                if (factory != null) {
                    Node graphic = factory.call(task);
                    if (graphic != null) {
                        BorderPane.setAlignment(graphic, Pos.CENTER);
                        BorderPane.setMargin(graphic, new Insets(0, 4, 0, 0));
                        borderPane.setLeft(graphic);
                    }
                } else {
                	/*
                	 * Really needed. The application might have used a graphic
                	 * factory before and then disabled it. In this case the border
                	 * pane might still have an old graphic in the left position.
                	 */
                	borderPane.setLeft(null);
                }

                setGraphic(borderPane);
            }
        }
    }
}

The CSS

The stylesheet below makes sure we use a bold font for the task title, a smaller / thinner progress bar (without rounded corners), and list cells with a fade-in / fade-out divider line in their bottom position.

.task-progress-view  {
       -fx-background-color: white;
}

.task-progress-view > * > .label {
 	-fx-text-fill: gray;
 	-fx-font-size: 18.0;
 	-fx-alignment: center;
 	-fx-padding: 10.0 0.0 5.0 0.0;
}

.task-progress-view > * > .list-view  {
 	-fx-border-color: transparent;
 	-fx-background-color: transparent;
}

.task-title {
	-fx-font-weight: bold;
}

.task-progress-bar .bar {
	-fx-padding: 6px;
	-fx-background-radius: 0;
	-fx-border-radius: 0;
}

.task-progress-bar .track {
	-fx-background-radius: 0;
}

.task-message {
}

.task-list-cell {
    -fx-background-color: transparent;
    -fx-padding: 4 10 8 10;
    -fx-border-color: transparent transparent linear-gradient(from 0.0% 0.0% to 100.0% 100.0%, transparent, rgba(0.0,0.0,0.0,0.2), transparent) transparent;
}

.task-list-cell-empty {
	-fx-background-color: transparent;
    -fx-border-color: transparent;
}

.task-cancel-button {
	-fx-base: red;
	-fx-font-size: .75em;
	-fx-font-weight: bold;
	-fx-padding: 4px;
	-fx-border-radius: 0;
	-fx-background-radius: 0;
}

FlexGanttFX: First JavaFX Component on ComponentSource

Yesterday FlexGanttFX has become the first JavaFX component in the product catalog of ComponentSource.com. You can find the entry here. ComponentSource is an international reseller of software components, basically the “App Store” of custom controls.  The fact that FlexGanttFX has been listed shows that the market is reacting and that JavaFX is gaining acceptance.

Screen Shot 2014-09-20 at 10.54.04

New Release: FlexGanttFX 1.0.0

Gantt Chart
I finally managed to put together a 1.0.0 production-ready release of FlexGanttFX. After two early access releases at the beginning of the year I put in a lot of hours to get the framework to a maturity level that I deem to be high enough for real-world application development. The release can be downloaded from the product website at http://www.flexganttfx.com. I would recommend to run the three standalone demos in this distribution to get an idea of FlexGanttFX’s capabilities.

FlexGanttFX is much bigger than a regular JavaFX control. The view consists of 83 and the model of 58 classes. It could have been even bigger but during development I decided to contribute some of the custom controls to the ControlsFX project (e.g. StatusBar, PopOver, PlusMinusSlider, HiddenSidesPane, MasterDetailPane).

emirates

There were two events in the past that delayed the release: the first one was a requirement from an early adopter to use the graphics area of the Gantt chart standalone, without the tree table view on the left-hand side. They wanted to use it as an editor for creating classroom schedules. Even though this requirement delayed the release it turned out to be very beneficial for the overall design of the framework. In the end I came up with four different ways the graphics can be presented: single row, using a VBox, using a SplitPane, or using a ListView.

The second event was the release of JDK 8u20. Changes were made to the way CSS was applied / processed which caused many warning messages to be shown in the console (“…unable to resolve -fx-background-color …”). While most people only get these warnings I could actually see that the layout of the controls in the Gantt chart was messed up. I had to find work-arounds for these issues which took a long time. I still see these messages but they seem to be harmless now. If anyone can point me to the root of this problem I would very much appreciate it.

msproject

I would like to say a special thank you to Gilberto Gomez, Werner Lehmann, and Christian Hollmann of MINT Media who provided very valuable input during the development of FlexGanttFX. I would also like to say thank you to the entire JavaFX developer community, especially the guys from Oracle and ControlsFX (Jonathan Giles, Eugene Ryzhikov).

JavaFX Tip 14: StackPane Children – Hidden But Not Gone

Another short tip: Swing provides a layout manager called CardLayout, which manages a set of components (cards) inside a container but always only shows one of them. The method CardLayout.show(Container, String) allows to switch between the components / the cards.

The same behaviour can be accomplished in JavaFX by using the StackPane, adding several children (each using the entire width and height of the pane) and calling the Node.toFront() method to switch between the children. However, there is one big difference: the StackPane will always layout all of its children, independent of wether they are currently showing or not. This might result in bad performance of your application and can be noticed when resizing the window that contains the pane.

My advice: manage your “cards” by adding them to or removing them from the scene graph. These operations are fast and flicker-free (this is JavaFX in Java 8, not Swing before Java 6).

JavaFX Tip 13: Study Modena CSS File

This is the easiest and shortest tip so far. If you want to do any of the following things:

  • learn how to use CSS
  • make your custom controls look like the standard controls
  • reuse an SVG path graphic used by a standard control (e.g. scrollbar arrows)
  • figure out how to navigate the structure of the standard controls
  • determine the color used for a specific item
  • consistently modify several standard controls

then simply take a look at the default CSS stylesheet that ships with JavaFX. The file is called modena.css and can be found in the jfxrt.jar file in this location: com/sun/javafx/scene/control/skin/modena/

Pixel Perfect

Where pixels have a home

Tomsondev Blog

Tom's opensource development

Carl's FX Blog

Just Ordinary Users of the API

Jasper Potts's Blog on Java and Life

Where pixels have a home

Exploding Pixels

Exposing the pixels behind beautiful user interfaces

Follow

Get every new post delivered to your Inbox.