/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2015, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.javafx.parameter;
import java.awt.Color;
import java.util.Collection;
import java.util.HashMap;
import java.util.Optional;
import java.util.Set;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXML;
import javafx.geometry.Bounds;
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.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Separator;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import javafx.stage.Popup;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.font.IconBuilder;
import org.geotoolkit.internal.GeotkFX;
import org.geotoolkit.parameter.ParameterGroup;
import org.geotoolkit.utility.parameter.ParametersExt;
import org.opengis.metadata.Identifier;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
/**
* A configurable panel for edition of parameter groups and their contained
* parameter values with a responsive design.
*
* TODO : ADD A "REMOVE" BUTTON FOR PARAMETER EDITOR.
* TODO : ADD a button on root component which show descriptor group structure.
* TODO : IMPROVE RESIZING RULES.
*
* Note : ISO-111 defines {@link ParameterValue} as unitary. However, SIS
* implementation allows for their multiplicity (even if it does not provide any
* commodity method), so we allow it too.
*
* @author Alexis Manin (Geomatys)
*/
public class FXParameterGroupPane extends BorderPane {
private static final String FLAT_BUTTON_CLASS = "flatbutton";
private static final String INFO_BUTTON_CLASS = "infobutton";
private static final String DESCRIPTOR_CONTAINER_CLASS = "descriptor-container";
private static final String DESCRIPTOR_CONTENT_CLASS = "descriptor-content";
private static final String PARAMETER_EDITOR_CLASS = "parameter-editor";
private static final String ROOT_CLASS = "root";
private static final String INFO_LABEL_CLASS = "infolabel";
private static final Image ICON_PLUS = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_PLUS, 16, new Color(74,123,165)), null);
private static final Image ICON_INFO = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_INFO, 16, Color.WHITE), null);
private static final Image ICON_MORE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_TH_LIST, 16, Color.BLACK), null);
private static final String ADVANCED_VIEW_TOOLTIP = GeotkFX.getString("org.geotoolkit.gui.javafx.parameter.advancedViewTooltip");
private static final String SIMPLE_VIEW_TOOLTIP = GeotkFX.getString("org.geotoolkit.gui.javafx.parameter.simpleViewTooltip");
/**
* Flow pane in which we'll add all {@link ParameterValue} editors.
*/
@FXML
private TilePane uiInnerValues;
/**
* Flow pane in which we'll add all {@link ParameterGroup} editors.
*/
@FXML
private FlowPane uiInnerGroups;
@FXML
private MenuButton uiAddBtn;
@FXML
private ToggleButton uiAdvancedBtn;
public final SimpleObjectProperty<ParameterValueGroup> inputGroup = new SimpleObjectProperty<>();
private final HashMap<String, DescriptorPanel> editionGroups = new HashMap<>();
private final SimpleIntegerProperty optionalParameterCount = new SimpleIntegerProperty(0);
/**
* A popup displayed when information button is clicked. It shows chosen parameter description.
*/
private final Popup descriptionPopup = new Popup();
private final Label infoLabel = new Label();
public FXParameterGroupPane() {
super();
GeotkFX.loadJRXML(this, this.getClass(), false);
getStyleClass().add(ROOT_CLASS);
uiAdvancedBtn.setGraphic(new ImageView(ICON_MORE));
uiAdvancedBtn.visibleProperty().bind(optionalParameterCount.greaterThan(0));
uiAdvancedBtn.managedProperty().bind(uiAdvancedBtn.visibleProperty());
uiAdvancedBtn.setTooltip(new Tooltip(ADVANCED_VIEW_TOOLTIP));
uiAdvancedBtn.selectedProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
if (Boolean.TRUE.equals(newValue)) {
uiAdvancedBtn.setTooltip(new Tooltip(SIMPLE_VIEW_TOOLTIP));
} else if (Boolean.FALSE.equals(newValue)) {
uiAdvancedBtn.setTooltip(new Tooltip(ADVANCED_VIEW_TOOLTIP));
} else {
uiAdvancedBtn.setTooltip(null);
}
});
uiAddBtn.setGraphic(new ImageView(ICON_PLUS));
uiAddBtn.visibleProperty().bind(uiAdvancedBtn.selectedProperty().and(Bindings.isNotEmpty(uiAddBtn.getItems())));
uiAddBtn.managedProperty().bind(uiAddBtn.visibleProperty());
inputGroup.addListener(this::updateParameterGroup);
descriptionPopup.setAutoHide(true);
infoLabel.getStylesheets().addAll(this.getStylesheets());
infoLabel.getStyleClass().add(INFO_LABEL_CLASS);
descriptionPopup.getContent().add(infoLabel);
}
public FXParameterGroupPane(ParameterValueGroup group) {
this();
inputGroup.set(group);
}
public void updateParameterGroup(ObservableValue<? extends ParameterValueGroup> observable, ParameterValueGroup oldValue, ParameterValueGroup newValue) {
// Remove all content from panel.
uiInnerValues.getChildren().clear();
uiInnerGroups.getChildren().clear();
uiAddBtn.getItems().clear();
editionGroups.clear();
optionalParameterCount.set(0);
if (newValue != null) {
final ParameterDescriptorGroup newDescriptor = newValue.getDescriptor();
// Optional or multi-occurence descriptors. We allow user to add more in editor.
for (final GeneralParameterDescriptor childDesc : newDescriptor.descriptors()) {
if ((childDesc.getMaximumOccurs() - childDesc.getMinimumOccurs()) > 0) {
uiAddBtn.getItems().add(new AddMenuItem(childDesc));
optionalParameterCount.set(optionalParameterCount.get()+1);
// Default valued parameters are not displayed in simple mode.
} else if ((childDesc instanceof ParameterDescriptor) &&
((ParameterDescriptor)childDesc).getDefaultValue() != null) {
optionalParameterCount.set(optionalParameterCount.get()+1);
}
}
for (GeneralParameterValue parameter : newValue.values()) {
GeneralParameterDescriptor descriptor = parameter.getDescriptor();
final DescriptorPanel descriptorPanel = getOrCreateDescriptorPanel(descriptor);
descriptorPanel.addEditor(parameter);
}
}
}
/**
* Try to get an editor for given input parameter value. If we can find one,
* it's integrated into given grid pane at specified row.
*
* Note : By default, non-editable parameters are ignored, and optional ones
* (and the ones with a default value) are marked as visible only in
* advanced view.
*
* @param value Parameter to find an editor for.
* @return Next available row indice, or the same as input if no editor has
* been inserted.
*/
protected Optional<Node> getValueEditor(final ParameterValue value) {
final ParameterDescriptor descriptor = value.getDescriptor();
Optional<FXValueEditor> opt = FXValueEditorSpi.findEditor(descriptor);
if (opt.isPresent()) {
final FXValueEditor editor = opt.get();
// Bind editor input to parameter value.
if (descriptor.getDefaultValue() != null) {
editor.valueProperty().setValue(descriptor.getDefaultValue());
}
editor.valueProperty().addListener((ObservableValue observable, Object oldValue, Object newValue) -> {
value.setValue(newValue);
});
final Node component = editor.getComponent();
// If it's a locked value, we prevent its edition.
Set validValues = descriptor.getValidValues();
if (validValues != null && validValues.size() < 2) {
component.setDisable(true);
}
final HBox container = new HBox(5, component);
container.setMaxWidth(Double.MAX_VALUE);
container.setFillHeight(true);
HBox.setHgrow(component, Priority.ALWAYS);
return Optional.of(container);
}
return Optional.empty();
}
private String getTitle(final GeneralParameterDescriptor parameter) {
//get name from alias
String name = null;
final Collection<GenericName> aliases = parameter.getAlias();
if(!aliases.isEmpty()){
final GenericName alias = aliases.iterator().next();
name = alias.toInternationalString().toString();
}
//use code if not defined
if(name == null){
name = parameter.getName().getCode();
}
return name;
}
protected DescriptorPanel getOrCreateDescriptorPanel(final GeneralParameterDescriptor descriptor) {
DescriptorPanel panel = editionGroups.get(descriptor.getName().getCode());
if (panel == null) {
panel = new DescriptorPanel(descriptor);
editionGroups.put(descriptor.getName().getCode(), panel);
if (descriptor instanceof ParameterDescriptorGroup) {
uiInnerGroups.getChildren().add(panel);
} else if (descriptor instanceof ParameterDescriptor) {
uiInnerValues.getChildren().add(panel);
}
}
return panel;
}
///////////////////////////////////////////////////////////////////////////
//
// PRIVATE CLASSES
//
///////////////////////////////////////////////////////////////////////////
private class AddMenuItem extends MenuItem {
private final GeneralParameterDescriptor parameter;
public AddMenuItem(final GeneralParameterDescriptor parameter) {
super();
ArgumentChecks.ensureNonNull("input parameter", parameter);
this.parameter = parameter;
setText(getTitle(parameter));
setOnAction(event -> {
final DescriptorPanel panel = getOrCreateDescriptorPanel(parameter);
if (panel.addParameterEditor() >= this.parameter.getMaximumOccurs()) {
uiAddBtn.getItems().remove(this);
}
});
}
}
private class DescriptorPanel extends TitledPane {
private final VBox content = new VBox();
private final Label uiTitle = new Label();
private final Button uiAdd = new Button(null, new ImageView(ICON_PLUS));
final Separator headerExpander = new Separator();
private final HBox uiToolbar = new HBox(10, uiTitle, headerExpander, uiAdd);
private final GeneralParameterDescriptor descriptor;
public DescriptorPanel(final GeneralParameterDescriptor descriptor) {
super();
ArgumentChecks.ensureNonNull("Parameter descriptor", descriptor);
this.descriptor = descriptor;
uiTitle.setText(getTitle(descriptor));
getStyleClass().add(DESCRIPTOR_CONTAINER_CLASS);
content.getStyleClass().add(DESCRIPTOR_CONTENT_CLASS);
content.setFillWidth(true);
// setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
uiAdd.getStyleClass().add(FLAT_BUTTON_CLASS);
uiAdd.managedProperty().bind(uiAdd.visibleProperty());
uiAdd.setVisible((ParametersExt.getParameters(inputGroup.get(), descriptor.getName().getCode()).size() < descriptor.getMaximumOccurs()));
uiAdd.setOnAction(event -> addParameterEditor());
// Make panel visible only in advanced mode for optional / preconfigured parameters.
if (this.descriptor.getMinimumOccurs() < 1
|| (this.descriptor instanceof ParameterDescriptor && ((ParameterDescriptor) this.descriptor).getDefaultValue() != null)) {
visibleProperty().bind(uiAdvancedBtn.selectedProperty());
managedProperty().bind(visibleProperty());
}
final InternationalString description = (descriptor.getDescription() != null)
? descriptor.getDescription() : descriptor.getRemarks();
if (description != null) {
final Button descriptionButton = new Button(null, new ImageView(ICON_INFO));
descriptionButton.setOnAction(event -> {
infoLabel.setText(description.toString());
Bounds localToScreen = descriptionButton.localToScreen(descriptionButton.getBoundsInLocal());
descriptionPopup.show(descriptionButton, localToScreen.getMinX(), localToScreen.getMinY());
});
descriptionButton.setAlignment(Pos.CENTER);
descriptionButton.getStyleClass().add(INFO_BUTTON_CLASS);
uiToolbar.getChildren().add(descriptionButton);
}
setContent(content);
setGraphic(uiToolbar);
/*
* CONFIGURE HEADER POSITION
*/
uiToolbar.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(headerExpander, Priority.ALWAYS);
headerExpander.setVisible(false);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
uiToolbar.setMaxWidth(Double.MAX_VALUE);
uiToolbar.prefWidthProperty().bind(widthProperty().subtract(50));
}
/**
* Check that input editor has not reached maximum occurence number,
* then add an editor for it in current panel.
*
* @param descriptor Descriptor for which we want a new editor.
* @return Current number of parameters of input type in main parameter
* group {@link #inputGroup}.
*/
private int addParameterEditor() {
final int maxOccurs = descriptor.getMaximumOccurs();
int currentOccurs = 0;
if (descriptor instanceof ParameterDescriptorGroup) {
currentOccurs = inputGroup.get().groups(descriptor.getName().getCode()).size();
} else
for (final GeneralParameterValue param : inputGroup.get().values()) {
if (param.getDescriptor().equals(descriptor)) {
currentOccurs++;
}
}
GeneralParameterValue value = null;
if (currentOccurs < maxOccurs) {
if (descriptor instanceof ParameterDescriptorGroup) {
value = inputGroup.get().addGroup(descriptor.getName().getCode());
currentOccurs++;
} else if (descriptor instanceof ParameterDescriptor) {
value = ParametersExt.getOrCreateValue(inputGroup.get(), descriptor.getName().getCode());
currentOccurs++;
}
if (value != null) {
addEditor(value);
}
}
if (currentOccurs >= maxOccurs) {
uiAdd.setVisible(false);
}
return currentOccurs;
}
public Node addEditor(final GeneralParameterValue parameter) {
Node editor = null;
if (parameter instanceof ParameterValueGroup) {
editor = new FXParameterGroupPane((ParameterValueGroup) parameter);
} else if (parameter instanceof ParameterValue) {
final Optional<Node> optional = getValueEditor((ParameterValue)parameter);
if (optional.isPresent()) {
editor = optional.get();
editor.getStyleClass().add(PARAMETER_EDITOR_CLASS);
}
}
if (editor != null) {
content.getChildren().add(editor);
}
return editor;
}
}
}