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.
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<MyTask> view = new TaskProgressView<>(); * 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; }
Reference: | New Custom Control: TaskProgressView from our JCG partner Dirk Lemmermann at the Pixel Perfect blog. |