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).
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 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.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | .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. |