/*
* Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com]
* 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 de.ks.idnadrev.information.chart;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import de.ks.BaseController;
import de.ks.i18n.Localized;
import de.ks.idnadrev.entity.information.ChartData;
import de.ks.idnadrev.entity.information.ChartInfo;
import de.ks.validation.ValidationMessage;
import de.ks.validation.validators.DoubleValidator;
import de.ks.validation.validators.NotEmptyValidator;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.Clipboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.validation.ValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class ChartDataEditor extends BaseController<ChartInfo> {
private static final Logger log = LoggerFactory.getLogger(ChartDataEditor.class);
private static final int ROW_OFFSET = 1;
private static final int COLUMN_OFFSET = 1;
@FXML
public TextField xaxisTitle;
@FXML
protected GridPane dataContainer;
@FXML
protected GridPane root;
protected final ObservableList<ChartRow> rows = FXCollections.observableArrayList();
protected final ObservableList<SimpleStringProperty> columnHeaders = FXCollections.observableArrayList();
protected final List<TextField> headers = new ArrayList<>();
protected final List<TextField> categoryEditors = new ArrayList<>();
protected final Table<Integer, Integer, TextField> valueEditors = HashBasedTable.create();
protected Consumer<ChartData> callback;
@Override
public void initialize(URL location, ResourceBundle resources) {
columnHeaders.addListener((ListChangeListener<SimpleStringProperty>) c -> onColumnsChanged(c));
rows.addListener((ListChangeListener<ChartRow>) c -> onRowsChanged(c));
reset();
validationRegistry.registerValidator(xaxisTitle, new NotEmptyValidator());
}
protected void onRowsChanged(ListChangeListener.Change<? extends ChartRow> c) {
while (c.next()) {
List<? extends ChartRow> addedSubList = c.getAddedSubList();
for (ChartRow chartRow : addedSubList) {
int rowNum = rows.indexOf(chartRow);
TextField categoryEditor = createCategoryEditor(chartRow, rowNum);
addRowConstraint();
dataContainer.add(categoryEditor, 0, rowNum + ROW_OFFSET);
for (int i = 0; i < columnHeaders.size(); i++) {
TextField editor = createValueEditor(chartRow, rowNum, i);
SimpleStringProperty value = chartRow.getValue(i);
editor.textProperty().bindBidirectional(value);
}
}
}
}
private TextField createCategoryEditor(ChartRow chartRow, int rowNum) {
TextField categoryEditor = new TextField();
categoryEditor.textProperty().bindBidirectional(chartRow.getCategory());
categoryEditor.focusedProperty().addListener(getEditorFocusListener(rowNum, categoryEditor));
categoryEditor.textProperty().addListener((p, o, n) -> {
categoryEditor.setUserData(true);
});
BiFunction<Integer, Integer, TextField> nextCategoryField = (row, column) -> {
if (categoryEditors.size() > row) {
return categoryEditors.get(row);
} else {
return null;
}
};
BiConsumer<Integer, Integer> clipBoardHandler = (row, col) -> {
String string = Clipboard.getSystemClipboard().getString();
if (StringUtils.containsWhitespace(string)) {
List<String> datas = Arrays.asList(StringUtils.split(string, "\n"));
int missingRows = (row + datas.size()) - rows.size();
if (missingRows > 0) {
for (int i = 0; i < missingRows; i++) {
rows.add(new ChartRow());
}
}
for (int i = row; i < row + datas.size(); i++) {
ChartRow currentChartRow = rows.get(i);
String data = datas.get(i - row);
currentChartRow.setCategory(data);
}
}
};
categoryEditor.setOnKeyReleased(getInputKeyHandler(rowNum, -1, nextCategoryField, clipBoardHandler));
validationRegistry.registerValidator(categoryEditor, (control, value) -> {
if (value != null) {
Set<String> values = categoryEditors.stream()//
.filter(e -> e != categoryEditor)//
.map(e -> e.textProperty().getValueSafe())//
.filter(v -> !v.isEmpty())//
.collect(Collectors.toSet());
if (values.contains(value)) {
ValidationMessage message = new ValidationMessage("validation.noDuplicates", control, value);
return ValidationResult.fromMessages(message);
}
}
return null;
});
categoryEditors.add(categoryEditor);
return categoryEditor;
}
protected void onColumnsChanged(ListChangeListener.Change<? extends SimpleStringProperty> c) {
while (c.next()) {
List<? extends SimpleStringProperty> added = c.getAddedSubList();
List<? extends SimpleStringProperty> removed = c.getRemoved();
for (SimpleStringProperty column : added) {
int columnIndex = columnHeaders.indexOf(column);
addColumnConstraint();
TextField title = new TextField();
title.textProperty().bindBidirectional(column);
title.getStyleClass().add("editorViewLabel");
MenuItem deleteColumnItem = new MenuItem(Localized.get("column.delete"));
deleteColumnItem.setOnAction(e -> {
columnHeaders.remove(column);
});
title.setContextMenu(new ContextMenu(deleteColumnItem));
headers.add(title);
dataContainer.add(title, columnIndex + COLUMN_OFFSET, 0);
for (int i = 0; i < rows.size(); i++) {
ChartRow chartRow = rows.get(i);
SimpleStringProperty value = chartRow.getValue(columnIndex);
TextField editor = createValueEditor(chartRow, i, columnIndex);
editor.textProperty().bindBidirectional(value);
}
}
for (SimpleStringProperty column : removed) {
Optional<Integer> first = dataContainer.getChildren().stream().filter(n -> GridPane.getRowIndex(n) == 0).map(n -> (TextField) n).filter(t -> t.getText().equals(column.getValue())).map(t -> GridPane.getColumnIndex(t)).findFirst();
if (first.isPresent()) {
int columnIndex = first.get();
rows.forEach(r -> {
SimpleStringProperty value = r.getValue(columnIndex);
value.set("");
value.unbind();
});
List<Node> childrenToRemove = dataContainer.getChildren().stream().filter(n -> GridPane.getColumnIndex(n) == columnIndex).collect(Collectors.toList());
dataContainer.getChildren().removeAll(childrenToRemove);
dataContainer.getColumnConstraints().remove(dataContainer.getColumnConstraints().size() - 1);
}
}
sortGridPane();
}
}
protected void sortGridPane() {
ArrayList<Node> childrensToSort = new ArrayList<>(dataContainer.getChildren());
Comparator<Node> rowCompare = Comparator.comparing(GridPane::getRowIndex);
Comparator<Node> columnCompare = Comparator.comparing(GridPane::getColumnIndex);
Collections.sort(childrensToSort, rowCompare.thenComparing(columnCompare));
dataContainer.getChildren().clear();
dataContainer.getChildren().addAll(childrensToSort);
}
protected void addRowConstraint() {
dataContainer.getRowConstraints().add(new RowConstraints(30, Control.USE_COMPUTED_SIZE, Control.USE_COMPUTED_SIZE, Priority.NEVER, VPos.CENTER, true));
}
protected void addColumnConstraint() {
dataContainer.getColumnConstraints().add(new ColumnConstraints(Control.USE_COMPUTED_SIZE, Control.USE_COMPUTED_SIZE, Control.USE_COMPUTED_SIZE, Priority.SOMETIMES, HPos.CENTER, true));
}
protected TextField createValueEditor(ChartRow chartRow, int rowNum, int column) {
TextField editor = new TextField();
valueEditors.put(rowNum, column, editor);
validationRegistry.registerValidator(editor, new DoubleValidator());
dataContainer.add(editor, column + COLUMN_OFFSET, rowNum + ROW_OFFSET);
editor.focusedProperty().addListener(getEditorFocusListener(rowNum, editor));
editor.textProperty().addListener((p, o, n) -> {
editor.setUserData(true);
});
BiFunction<Integer, Integer, TextField> nextTextField = (row, col) -> valueEditors.row(row).get(col);
BiConsumer<Integer, Integer> clipBoardHandler = (row, col) -> {
String string = Clipboard.getSystemClipboard().getString();
if (StringUtils.containsWhitespace(string)) {
List<String> datas = Arrays.asList(StringUtils.split(string));
int missingRows = (row + datas.size()) - rows.size();
if (missingRows > 0) {
for (int i = 0; i < missingRows; i++) {
rows.add(new ChartRow());
}
}
for (int i = row; i < row + datas.size(); i++) {
ChartRow currentChartRow = rows.get(i);
String data = datas.get(i - row);
currentChartRow.setValue(column, data);
}
}
};
editor.setOnKeyReleased(getInputKeyHandler(rowNum, column, nextTextField, clipBoardHandler));
return editor;
}
private EventHandler<KeyEvent> getInputKeyHandler(int rowNum, int column, BiFunction<Integer, Integer, TextField> nextTextField, BiConsumer<Integer, Integer> clipBoardHandler) {
return e -> {
KeyCode code = e.getCode();
if (e.isControlDown() && code == KeyCode.V) {
clipBoardHandler.accept(rowNum, column);
e.consume();
}
boolean selectNext = false;
if (e.getCode() == KeyCode.ENTER && !e.isControlDown()) {
selectNext = true;
}
if (selectNext) {
int next = rowNum + 1;
TextField textField = nextTextField.apply(next, column);
if (textField != null) {
textField.requestFocus();
}
e.consume();
}
};
}
private ChangeListener<Boolean> getEditorFocusListener(int rowNum, TextField editor) {
return (p, o, n) -> {
if (n) {
if (!isRowEmpty(rowNum) && rowNum + ROW_OFFSET == rows.size()) {
rows.add(new ChartRow());
}
editor.setUserData(false);
} else if (o && !n) {
boolean edited = (Boolean) (editor.getUserData() == null ? false : editor.getUserData());
if (edited) {
triggerRedraw();
editor.setUserData(false);
}
}
};
}
private boolean isRowEmpty(int rowNum) {
ChartRow row = rows.get(rowNum);
return row.getCategory().getValueSafe().trim().isEmpty();
}
protected void triggerRedraw() {
if (callback != null && !validationRegistry.isInvalid()) {
callback.accept(getData());
}
}
public void addColumnHeader(String title) {
columnHeaders.add(new SimpleStringProperty(title));
}
public ObservableList<ChartRow> getRows() {
return rows;
}
public ObservableList<SimpleStringProperty> getColumnHeaders() {
return columnHeaders;
}
public List<TextField> getHeaders() {
return headers;
}
public List<TextField> getCategoryEditors() {
return categoryEditors;
}
public ChartData getData() {
ChartData data = new ChartData();
this.rows.forEach(r -> {
data.getCategories().add(r.getCategory().getValueSafe());
});
for (int i = 0; i < columnHeaders.size(); i++) {
SimpleStringProperty header = columnHeaders.get(i);
LinkedList<Double> values = new LinkedList<>();
for (int rowNum = 0; rowNum < rows.size(); rowNum++) {
ChartRow row = rows.get(rowNum);
if (row.getCategory().getValueSafe().trim().isEmpty()) {
continue;
}
SimpleStringProperty value = row.getValue(i);
if (value != null && !value.getValueSafe().trim().isEmpty()) {
double val = Double.parseDouble(value.getValueSafe());
values.add(val);
} else {
values.add(0d);
}
}
data.addSeries(header.getValueSafe(), values);
}
data.setXAxisTitle(xaxisTitle.getText());
return data;
}
public void setData(ChartData data) {
int row = 0;
for (String category : data.getCategories()) {
if (rows.size() < row + 1) {
rows.add(new ChartRow());
}
rows.get(row).setCategory(category);
row++;
}
int column = 0;
for (ChartData.DataSeries dataSeries : data.getSeries()) {
if (columnHeaders.size() < column + 1) {
columnHeaders.add(new SimpleStringProperty());
}
columnHeaders.get(column).set(dataSeries.getTitle());
for (int valueIndex = 0; valueIndex < dataSeries.getValues().size(); valueIndex++) {
Double value = dataSeries.getValues().get(valueIndex);
rows.get(valueIndex).setValue(column, value);
}
column++;
}
xaxisTitle.setText(data.getXAxisTitle());
}
public void setCallback(Consumer<ChartData> callback) {
this.callback = callback;
}
@Override
public void duringSave(ChartInfo model) {
model.setChartData(getData());
}
@Override
public void duringLoad(ChartInfo model) {
model.getChartData();//deserialize async
}
public void reset() {
dataContainer.getChildren().clear();
dataContainer.getChildren().remove(xaxisTitle);//FXML loader doesn't set row and column in gridpane :(
dataContainer.add(xaxisTitle, 0, 0);
rows.clear();
columnHeaders.clear();
categoryEditors.clear();
valueEditors.clear();
headers.clear();
rows.add(new ChartRow());
columnHeaders.add(new SimpleStringProperty(Localized.get("col", 1)));
columnHeaders.add(new SimpleStringProperty(Localized.get("col", 2)));
if (callback != null) {
callback.accept(getData());
}
}
}