/**
*
* Copyright (c) 2006-2017, Speedment, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); You may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.speedment.plugins.enums.internal.ui;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import com.speedment.tool.propertyeditor.item.AbstractLabelTooltipItem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;
import static javafx.application.Platform.runLater;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
/**
* Item for generating a comma-separated string.
* <p>
* We parse what values an enum should be able to take from a string, where
* each element is separated by a comma. This editor item allows the user
* to easily edit such a string.
*
* @author Simon Jonasson
* @since 1.0.0
*/
public final class AddRemoveStringItem extends AbstractLabelTooltipItem {
//***********************************************************
// VARIABLES
//***********************************************************
private final static Logger LOGGER = LoggerManager.getLogger(AddRemoveStringItem.class);
private final ObservableList<String> strings;
private final ObservableBooleanValue enabled;
@SuppressWarnings("FieldCanBeLocal")
private final StringProperty cache;
private final String DEFAULT_FIELD = "ENUM_CONSTANT_";
private final double SPACING = 10.0;
private final int LIST_HEIGHT = 200;
//***********************************************************
// CONSTRUCTOR
//***********************************************************
public AddRemoveStringItem(
String label,
StringProperty value,
String tooltip,
ObservableBooleanValue enableThis) {
super(label, tooltip, NO_DECORATOR);
final String currentValue = value.get();
if (currentValue == null) {
this.strings = FXCollections.observableArrayList();
} else {
this.strings = FXCollections.observableArrayList(
Stream.of(currentValue.split(","))
.filter(s -> !s.isEmpty())
.toArray(String[]::new)
);
}
this.enabled = enableThis;
this.cache = new SimpleStringProperty();
this.strings.addListener((ListChangeListener.Change<? extends String> c) -> {
@SuppressWarnings("unchecked")
final List<String> list = (List<String>) c.getList();
value.setValue(getFormatedString(list));
});
}
//***********************************************************
// PUBLIC
//***********************************************************
@Override
public Node createLabel() {
Node node = super.createLabel();
hideShowBehaviour(node);
return node;
}
@Override
protected Node createUndecoratedEditor() {
final VBox container = new VBox();
final ListView<String> listView = new ListView<>(strings);
listView.setCellFactory(view -> new EnumCell(strings));
listView.setEditable(true);
listView.setMaxHeight(USE_PREF_SIZE);
listView.setPrefHeight(LIST_HEIGHT);
final HBox controls = new HBox(SPACING);
controls.setAlignment(Pos.CENTER);
controls.getChildren().addAll(addButton(listView), removeButton(listView));
container.setSpacing(SPACING);
container.getChildren().addAll(listView, controls);
hideShowBehaviour(container);
return container;
}
//***********************************************************
// PRIVATE
//***********************************************************
/**
* Removes any empty substrings and makes sure the entire string is either
* {@code null} or non-empty.
*
* @return the formatted string
*/
private String getFormatedString(List<String> newValue) {
final String formated = newValue.stream()
.filter(v -> !v.isEmpty())
.collect(Collectors.joining(","));
if (formated == null || formated.isEmpty()) {
return null;
} else {
return formated;
}
}
private void setValue(String value) {
if (value == null) {
strings.clear();
} else {
strings.setAll(value.split(","));
}
}
private void hideShowBehaviour(Node node){
node.visibleProperty().bind(enabled);
node.managedProperty().bind(enabled);
node.disableProperty().bind(Bindings.not(enabled));
}
private Button removeButton(final ListView<String> listView) {
final Button button = new Button("Remove Selected");
button.setOnAction(e -> {
final int selectedIdx = listView.getSelectionModel().getSelectedIndex();
if (selectedIdx != -1 && listView.getItems().size() > 1) {
final int newSelectedIdx = (selectedIdx == listView.getItems().size() - 1) ? selectedIdx - 1
: selectedIdx;
listView.getItems().remove(selectedIdx);
listView.getSelectionModel().select(newSelectedIdx);
}
});
return button;
}
private Button addButton(final ListView<String> listView) {
final Button button = new Button("Add Item");
button.setOnAction(e -> {
final int newIndex = listView.getItems().size();
final Set<String> set = strings.stream().collect(toSet());
final AtomicInteger i = new AtomicInteger(0);
while (!set.add(DEFAULT_FIELD + i.incrementAndGet())) {}
listView.getItems().add(DEFAULT_FIELD + i.get());
listView.scrollTo(newIndex);
listView.getSelectionModel().select(newIndex);
/* There is a strange behavior in JavaFX if you try to start editing
* a field on the same animation frame as another field lost focus.
*
* Therefore, we wait one animation cycle before setting the field
* into the editing state
*/
runLater(() -> listView.edit(newIndex));
});
return button;
}
//***********************************************************
// PRIVATE CLASS : ENUM CELL
//***********************************************************
private final static class EnumCell extends TextFieldListCell<String> {
private final ObservableList<String> strings;
private String labelString;
private EnumCell(ObservableList<String> strings) {
super();
this.strings = requireNonNull(strings);
setConverter(myConverter());
}
@Override
public void startEdit() {
labelString = getText();
super.startEdit();
}
private StringConverter<String> myConverter() {
return new StringConverter<String>() {
@Override
public String toString(String value) {
return (value != null) ? value : "";
}
@Override
public String fromString(String value) {
// Avoid false positives (ie showing an error that we match ourselves)
if (value.equalsIgnoreCase(labelString)) {
return value;
} else if (value.isEmpty()) {
LOGGER.info("An enum field cannot be empty. Please remove the field instead.");
return labelString;
}
// Make sure this is not a douplicate entry
final AtomicBoolean douplicate = new AtomicBoolean(false);
strings.stream()
.filter(elem -> elem.equalsIgnoreCase(value))
.forEach(elem -> douplicate.set(true));
if (douplicate.get()){
LOGGER.info("Enum cannot contain the same constant twice");
return labelString;
// Make sure this entry contains only legal characters
} else if ( !value.matches("([\\w\\-\\_\\ ]+)")) {
LOGGER.info("Enum should only contain letters, number, underscore and/or dashes");
return labelString;
// Warn if it contains a space
} else if (value.contains(" ")) {
LOGGER.warn("Enum spaces will be converted to underscores in Java");
return value;
} else {
return value;
}
}
};
}
}
}