package org.geotoolkit.gui.javafx.util;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Background;
import javafx.scene.layout.Border;
import javafx.scene.layout.HBox;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import javafx.scene.text.Font;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.internal.GeotkFX;
/**
* A JavaFX component which display progress and encountered errors for all tasks
* submitted on a specific task manager.
*
* The last submitted task is displayed in an Hbox. To see other running tasks
* and tasks in error, there's two {@link MenuButton}. Each display custom menu
* items containing information about a task.
*
* The {@link ProgressMonitor} is skinnable using a css stylesheet and the specific
* following css classes:
* {@link ProgressMonitor#CURRENT_TASK_CSS_CLASS}
* {@link ProgressMonitor#CURRENT_TASK_GRAPHIC_CSS_CLASS}
* {@link ProgressMonitor#ERROR_TASK_CSS_CLASS}
* {@link ProgressMonitor#ERROR_TASK_GRAPHIC_CSS_CLASS}
* {@link ProgressMonitor#PROGRESS_MONITOR_CSS_CLASS}
* {@link ProgressMonitor#CANCEL_BUTTON_CSS_CLASS}
* {@link ProgressMonitor#TASK_PROGRESS_CSS_CLASS}
* {@link ProgressMonitor#TASK_PROGRESS_GRAPHIC_CSS_CLASS}
* {@link ProgressMonitor#MENU_ITEM_CSS_CLASS}
* {@link ProgressMonitor#CLEAR_MENU_ITEM_CSS_CLASS}
*
* @author Alexis Manin (Geomatys)
*/
public class ProgressMonitor extends HBox {
private static String ICON_LABEL_FONT_FAMILY = "-fx-font-family: FontAwesome Regular;";
/**
* The css classes associated to the {@link ProgressMonitor} nodes.
*/
public static final String CURRENT_TASK_CSS_CLASS="geotk-progressMonitor-runningTasks";
public static final String CURRENT_TASK_GRAPHIC_CSS_CLASS="geotk-progressMonitor-runningTasks-graphic";
public static final String ERROR_TASK_CSS_CLASS="geotk-progressMonitor-tasksInError";
public static final String ERROR_TASK_GRAPHIC_CSS_CLASS="geotk-progressMonitor-tasksInError-graphic";
public static final String PROGRESS_MONITOR_CSS_CLASS="geotk-progressMonitor";
public static final String CANCEL_BUTTON_CSS_CLASS="geotk-progressMonitor-cancelButton";
public static final String TASK_PROGRESS_CSS_CLASS="geotk-progressMonitor-taskProgress";
public static final String TASK_PROGRESS_GRAPHIC_CSS_CLASS="geotk-progressMonitor-taskProgress-graphic";
public static final String MENU_ITEM_CSS_CLASS="geotk-progressMonitor-menuItem";
public static final String CLEAR_MENU_ITEM_CSS_CLASS="geotk-progressMonitor-clearMenuItem";
private TaskProgress lastTask;
private final MenuButton runningTasks;
private final MenuButton tasksInError;
private final TaskManager taskRegistry;
static {
// Load Font Awesome.
final Font font = FXUtilities.FONTAWESOME;
}
/**
* The base constructor of progress monitors.
*
* @param registry The {@link TaskManager} followed by this progress monitor.
*/
public ProgressMonitor(final TaskManager registry) {
ArgumentChecks.ensureNonNull("Input task registry", registry);
taskRegistry = registry;
final Label runningIcon = new Label(FontAwesomeIcons.ICON_GEARS_ALIAS);
runningIcon.setStyle(ICON_LABEL_FONT_FAMILY);
runningIcon.getStyleClass().add(CURRENT_TASK_GRAPHIC_CSS_CLASS);
runningTasks = new MenuButton(GeotkFX.getString(ProgressMonitor.class, "currentTasksLabel"), runningIcon);
final Label errorIcon = new Label(FontAwesomeIcons.ICON_EXCLAMATION_CIRCLE);
errorIcon.setStyle(ICON_LABEL_FONT_FAMILY);
errorIcon.getStyleClass().add(ERROR_TASK_GRAPHIC_CSS_CLASS);
tasksInError = new MenuButton(GeotkFX.getString(ProgressMonitor.class, "errorTasksLabel"), errorIcon);
final Tooltip runninTasksTooltip = new Tooltip(GeotkFX.getString(ProgressMonitor.class, "currentTasksTooltip"));
runningTasks.setTooltip(runninTasksTooltip);
final Tooltip tasksInErrorTooltip = new Tooltip(GeotkFX.getString(ProgressMonitor.class, "errorTasksTooltip"));
tasksInError.setTooltip(tasksInErrorTooltip);
final SimpleListProperty runningTasksProp = new SimpleListProperty(taskRegistry.getSubmittedTasks());
final SimpleListProperty failedTasksProp = new SimpleListProperty(taskRegistry.getTasksInError());
// Hide list of tasks if there's no information available.
runningTasks.visibleProperty().bind(runningTasksProp.sizeProperty().greaterThan(1));
tasksInError.visibleProperty().bind(failedTasksProp.emptyProperty().not());
// Display number of tasks on menu button.
runningTasks.textProperty().bind(runningTasksProp.sizeProperty().asString());
tasksInError.textProperty().bind(failedTasksProp.sizeProperty().asString());
// Set default visible task the last one submitted.
lastTask = new TaskProgress();
lastTask.taskProperty().bind(runningTasksProp.valueAt(runningTasksProp.sizeProperty().subtract(1)));
// Do not reserve size for hidden components.
runningTasks.managedProperty().bind(runningTasks.visibleProperty());
tasksInError.managedProperty().bind(tasksInError.visibleProperty());
lastTask.managedProperty().bind(lastTask.visibleProperty());
initTasks();
getChildren().addAll(lastTask, runningTasks, tasksInError);
minWidthProperty().bind(prefWidthProperty());
prefWidthProperty().set(USE_COMPUTED_SIZE);
getStyleClass().add(PROGRESS_MONITOR_CSS_CLASS);
runningTasks.getStyleClass().add(CURRENT_TASK_CSS_CLASS);
tasksInError.getStyleClass().add(ERROR_TASK_CSS_CLASS);
}
/**
* Fill panel with currently submitted tasks. Add listeners on
* {@link TaskManager} to be aware of new events.
*/
private void initTasks() {
final MenuItem clearErrorItem = new MenuItem(GeotkFX.getString(ProgressMonitor.class, "cleanErrorList"));
clearErrorItem.setOnAction(evt -> taskRegistry.getTasksInError().clear());
final Label icon = new Label(FontAwesomeIcons.ICON_TRASH_O);
icon.setStyle(ICON_LABEL_FONT_FAMILY);
clearErrorItem.setGraphic(icon);
clearErrorItem.getStyleClass().add(CLEAR_MENU_ITEM_CSS_CLASS);
tasksInError.getItems().add(clearErrorItem);
tasksInError.getItems().add(new SeparatorMenuItem());
// Listen on current running tasks
final ObservableList<Task> tmpSubmittedTasks = taskRegistry.getSubmittedTasks();
tmpSubmittedTasks.addListener((ListChangeListener.Change<? extends Task> c) -> {
final Set<Task> toAdd = new HashSet<>();
final Set<Task> toRemove = new HashSet<>();
storeChanges(c, toAdd, toRemove);
Platform.runLater(() -> {
for (final Task task : toAdd) {
final CustomMenuItem item = new CustomMenuItem(new TaskProgress(task));
item.setHideOnClick(false);
runningTasks.getItems().add(item);
}
// remove Ended tasks
runningTasks.getItems().removeIf(new GetItemsForTask(toRemove));
});
});
// Check failed tasks.
final ObservableList<Task> tmpTasksInError = taskRegistry.getTasksInError();
tmpTasksInError.addListener((ListChangeListener.Change<? extends Task> c) -> {
final Set<Task> toAdd = new HashSet<>();
final Set<Task> toRemove = new HashSet<>();
storeChanges(c, toAdd, toRemove);
Platform.runLater(() -> {
for (final Task task : toAdd) {
tasksInError.getItems().add(new ErrorMenuItem(task));
}
// remove Ended tasks
tasksInError.getItems().removeIf(new GetItemsForTask(toRemove));
});
});
final Runnable initPanel = () -> {
final int nbSubmitted = tmpSubmittedTasks.size();
// do not add last task to our menu, it will be used on main display.
for (int i = 0; i < nbSubmitted; i++) {
final CustomMenuItem item = new CustomMenuItem(new TaskProgress(tmpSubmittedTasks.get(i)));
item.setHideOnClick(false);
runningTasks.getItems().add(item);
}
for (final Task t : tmpTasksInError) {
tasksInError.getItems().add(new ErrorMenuItem(t));
}
};
if (Platform.isFxApplicationThread()) {
initPanel.run();
} else {
Platform.runLater(initPanel);
}
}
/**
* Store all objects depicted by a {@link ListChangeListener} into given
* collections.
*
* @param c The {@link ListChangeListener.Change} containing list content delta.
* @param added The collection to store new added objects into.
* @param removed Add removed objects in it.
*/
private static void storeChanges(final ListChangeListener.Change c, final Collection added, final Collection removed) {
while (c.next()) {
final List<? extends Task> addedSubList = c.getAddedSubList();
final List<? extends Task> removeSubList = c.getRemoved();
if (addedSubList != null && !addedSubList.isEmpty()) {
added.addAll(addedSubList);
}
final Iterator<? extends Task> it = removeSubList.iterator();
while (it.hasNext()) {
final Task current = it.next();
if (!added.remove(current)) {
removed.add(current);
}
}
}
}
/**
* The node giving information about a specific task. Allow to see title,
* description and current progress, as to cancel the task.
*/
private static class TaskProgress extends HBox {
private final Label title = new Label();
private final Tooltip description = new Tooltip();
private final ProgressBar progress = new ProgressBar();
private final Button cancelButton;
private final ObjectProperty<Task> taskProperty = new SimpleObjectProperty<>();
TaskProgress() {
this(null);
}
TaskProgress(final Task t) {
final Label icon = new Label(FontAwesomeIcons.ICON_BAN);
icon.setStyle(ICON_LABEL_FONT_FAMILY);
icon.getStyleClass().add(TASK_PROGRESS_GRAPHIC_CSS_CLASS);
cancelButton = new Button("", icon);
taskProperty.addListener((ObservableValue<? extends Task> observable, Task oldValue, Task newValue) -> {
if (Platform.isFxApplicationThread()) {
taskUpdated();
} else {
Platform.runLater(()->taskUpdated());
}
});
taskProperty.set(t);
getChildren().addAll(title, progress, cancelButton);
getStyleClass().add(TASK_PROGRESS_CSS_CLASS);
cancelButton.getStyleClass().add(CANCEL_BUTTON_CSS_CLASS);
}
public ObjectProperty<Task> taskProperty() {
return taskProperty;
}
public Task getTask() {
return taskProperty.get();
}
public synchronized void taskUpdated() {
title.textProperty().unbind();
progress.progressProperty().unbind();
description.textProperty().unbind();
cancelButton.setOnAction(null);
final Task t =taskProperty.get();
if (t != null) {
title.textProperty().bind(t.titleProperty());
description.textProperty().bind(t.messageProperty());
progress.progressProperty().bind(t.progressProperty());
cancelButton.setOnAction((ActionEvent e) -> t.cancel());
setVisible(true);
} else {
setVisible(false);
}
}
}
/**
* A simple menu items for failed tasks. Display an exception Dialog when clicked.
*/
private class ErrorMenuItem extends MenuItem {
final Task failedTask;
ErrorMenuItem(final Task failedTask) {
ArgumentChecks.ensureNonNull("task in error", failedTask);
this.failedTask = failedTask;
// No need for binding here. Task failed, its state should not change anymore.
String title = failedTask.getTitle();
if (title == null || title.isEmpty())
title = GeotkFX.getString(ProgressMonitor.class, "anonymOperation");
setText(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"))+" - "+title);
final Label icon = new Label(FontAwesomeIcons.ICON_TRASH_O);
icon.setStyle(ICON_LABEL_FONT_FAMILY);
final Button deleteButton = new Button("", icon);
deleteButton.setBorder(Border.EMPTY);
deleteButton.setPadding(Insets.EMPTY);
deleteButton.setBackground(Background.EMPTY);
deleteButton.setOnAction(e -> {
taskRegistry.getTasksInError().remove(failedTask);
e.consume();
});
setGraphic(deleteButton);
final Dialog d = GeotkFX.newExceptionDialog(failedTask.getMessage(), failedTask.getException());
d.setResizable(true);
setOnAction((ActionEvent ae) -> d.show());
getStyleClass().add(MENU_ITEM_CSS_CLASS);
}
public Task getTask() {
return failedTask;
}
}
/**
* A simple {@link Predicate} which return current monitor progress bars
* which are focused on one of the given tasks.
*/
private static class GetItemsForTask implements Predicate<MenuItem> {
private final Collection<? extends Task> tasks;
GetItemsForTask(final Collection<? extends Task> taskFilter) {
ArgumentChecks.ensureNonNull("Input filter tasks", taskFilter);
tasks = taskFilter;
}
@Override
public boolean test(MenuItem item) {
if (item instanceof CustomMenuItem) {
final Node content = ((CustomMenuItem) item).getContent();
return (content instanceof TaskProgress
&& tasks.contains(((TaskProgress) content).getTask()));
} else if (item instanceof ErrorMenuItem) {
return tasks.contains(((ErrorMenuItem) item).getTask());
}
return false;
}
}
}