de.ks.idnadrev.information.chart.ChartDataEditor.java Source code

Java tutorial

Introduction

Here is the source code for de.ks.idnadrev.information.chart.ChartDataEditor.java

Source

/*
 * 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());
        }
    }
}