org.cirdles.squid.gui.expressions.ExpressionBuilderController.java Source code

Java tutorial

Introduction

Here is the source code for org.cirdles.squid.gui.expressions.ExpressionBuilderController.java

Source

/*
 * Copyright 2018 James F. Bowring and CIRDLES.org.
 *
 * 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 org.cirdles.squid.gui.expressions;

import com.google.common.collect.Lists;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.*;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Callback;
import javafx.util.StringConverter;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import org.cirdles.ludwig.squid25.Utilities;
import org.cirdles.squid.ExpressionsForSquid2Lexer;
import org.cirdles.squid.constants.Squid3Constants.*;
import org.cirdles.squid.exceptions.SquidException;
import org.cirdles.squid.gui.SquidUI;
import org.cirdles.squid.gui.utilities.BrowserControl;
import org.cirdles.squid.gui.utilities.fileUtilities.FileHandler;
import org.cirdles.squid.shrimp.ShrimpFractionExpressionInterface;
import org.cirdles.squid.shrimp.SquidRatiosModel;
import org.cirdles.squid.tasks.TaskInterface;
import org.cirdles.squid.tasks.expressions.Expression;
import org.cirdles.squid.tasks.expressions.ExpressionPublisher;
import org.cirdles.squid.tasks.expressions.OperationOrFunctionInterface;
import org.cirdles.squid.tasks.expressions.constants.ConstantNode;
import org.cirdles.squid.tasks.expressions.expressionTrees.*;
import org.cirdles.squid.tasks.expressions.functions.Function;
import org.cirdles.squid.tasks.expressions.functions.ShrimpSpeciesNodeFunction;
import org.cirdles.squid.tasks.expressions.functions.WtdMeanACalc;
import org.cirdles.squid.tasks.expressions.isotopes.ShrimpSpeciesNode;
import org.cirdles.squid.tasks.expressions.parsing.ShuntingYard;
import org.cirdles.squid.tasks.expressions.parsing.ShuntingYard.TokenTypes;
import org.cirdles.squid.tasks.expressions.spots.SpotSummaryDetails;
import org.cirdles.squid.tasks.expressions.variables.VariableNodeForSummary;
import org.cirdles.squid.utilities.IntuitiveStringComparator;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;

import static javafx.scene.paint.Color.RED;
import static org.cirdles.squid.constants.Squid3Constants.*;
import static org.cirdles.squid.gui.SquidUI.*;
import static org.cirdles.squid.gui.SquidUIController.createCopyToClipboardContextMenu;

import static org.cirdles.squid.gui.SquidUIController.squidProject;
import static org.cirdles.squid.gui.constants.Squid3GuiConstants.*;
import org.cirdles.squid.shrimp.SquidSpeciesModel;
import static org.cirdles.squid.tasks.expressions.functions.Function.*;
import static org.cirdles.squid.tasks.expressions.operations.Operation.OPERATIONS_MAP;
import static org.cirdles.squid.utilities.conversionUtilities.CloningUtilities.clone2dArray;

import org.cirdles.squid.tasks.Task;
import org.cirdles.squid.tasks.expressions.expressionTrees.ExpressionTreeBuilderInterface;
import org.cirdles.squid.tasks.expressions.operations.Value;
import org.cirdles.squid.tasks.expressions.spots.SpotFieldNode;
import static org.cirdles.squid.tasks.expressions.spots.SpotFieldNode.buildSpotNode;
import org.cirdles.squid.tasks.expressions.variables.VariableNodeForIsotopicRatios;
import static org.cirdles.squid.utilities.conversionUtilities.RoundingUtilities.squid3RoundedToSize;

/**
 * FXML Controller class
 *
 * @author James F. Bowring
 */
public class ExpressionBuilderController implements Initializable {

    // handle for closing stage when Squid closes
    public static final Stage EXPRESSION_NOTES_STAGE = new Stage();

    //BUTTONS
    @FXML
    private Button expressionCopyBtn;
    @FXML
    private Button expressionPasteBtn;
    @FXML
    private Button expressionAsTextBtn;
    @FXML
    private Button expressionClearBtn;
    @FXML
    private Button expressionUndoBtn;
    @FXML
    private Button expressionRedoBtn;
    @FXML
    private Button saveBtn;
    @FXML
    private Button cancelBtn;
    @FXML
    private Button createExpressionBtn;
    @FXML
    private Button editExpressionBtn;
    @FXML
    private Button copyExpressionIntoCustomBtn;
    @FXML
    private Button showCurrentExpressionBtn;
    @FXML
    private Button showNotesBtn;
    @FXML
    private Button toggleWhiteSpacesBtn;
    @FXML
    private Button fontMinusBtn;
    @FXML
    private Button fontPlusBtn;

    //TEXTS
    @FXML
    private TextFlow expressionTextFlow;
    @FXML
    private TextField expressionNameTextField;
    @FXML
    private TextArea auditTextArea;
    @FXML
    private TextArea unPeekTextArea;
    @FXML
    private TextArea rmPeekTextArea;
    @FXML
    private Text hintHoverText;
    @FXML
    private Text hintSelectText;
    @FXML
    private ToggleGroup expressionsSortToggleGroup;

    private final TextArea expressionAsTextArea = new TextArea();

    {
        expressionAsTextArea.setFont(
                Font.font(expressionAsTextArea.getFont().getFamily(), EXPRESSION_BUILDER_DEFAULT_FONTSIZE));
    }

    //RADIOS
    ToggleGroup toggleGroup;
    @FXML
    private RadioButton dragndropRightRadio;
    @FXML
    private RadioButton dragndropReplaceRadio;
    @FXML
    private RadioButton dragndropLeftRadio;

    //CHECKBOXES
    @FXML
    private CheckBox refMatSwitchCheckBox;
    @FXML
    private CheckBox unknownsSwitchCheckBox;
    @FXML
    private CheckBox concRefMatSwitchCheckBox;
    @FXML
    private CheckBox summaryCalculationSwitchCheckBox;
    @FXML
    private CheckBox specialUPbThSwitchCheckBox;
    @FXML
    private CheckBox NUSwitchCheckBox;
    @FXML
    private CheckBox showGraphCheckBox;
    @FXML
    private CheckBox graphBrowserCheckBox;

    //LISTVIEWS
    @FXML
    private ListView<Expression> nuSwitchedExpressionsListView;
    @FXML
    private TitledPane nuSwitchedExpressionsTitledPane;

    @FXML
    private ListView<Expression> builtInExpressionsListView;
    @FXML
    private TitledPane builtInExpressionsTitledPane;

    @FXML
    private ListView<Expression> brokenExpressionsListView;
    @FXML
    private TitledPane brokenExpressionsTitledPane;

    @FXML
    private ListView<Expression> customExpressionsListView;
    @FXML
    private TitledPane customExpressionsTitledPane;

    @FXML
    private ListView<SquidRatiosModel> ratioExpressionsListView;

    @FXML
    private ListView<SquidSpeciesModel> isotopesExpressionsListView;

    @FXML
    private ListView<ExpressionTreeInterface> spotMetaDataExpressionsListView;

    @FXML
    private ListView<String> operationsListView;
    @FXML
    private TitledPane operationsTitledPane;

    @FXML
    private ListView<String> mathFunctionsListView;

    @FXML
    private ListView<String> squidFunctionsCommonListView;

    @FXML
    private ListView<String> squidFunctionsListView;

    @FXML
    private ListView<String> logicFunctionsListView;

    @FXML
    private ListView<String> constantsListView;

    @FXML
    private ListView<String> presentationListView;

    @FXML
    private ListView<Expression> referenceMaterialsListView;
    @FXML
    private TitledPane referenceMaterialsTitledPane;

    @FXML
    private ListView<Expression> parametersListView;
    @FXML
    private TitledPane parametersTitledPane;

    //MISC
    @FXML
    private TitledPane graphPane;
    @FXML
    private TitledPane auditPane;
    @FXML
    private TitledPane peekPane;
    @FXML
    private AnchorPane expressionPane;
    @FXML
    private Accordion expressionsAccordion;
    @FXML
    private Accordion othersAccordion;
    @FXML
    private WebView graphView;
    @FXML
    private SplitPane mainPane;
    @FXML
    private SplitPane bigSplitPane;
    @FXML
    private SplitPane smallSplitPane;
    @FXML
    private SplitPane leftSplitPane;
    @FXML
    private VBox editorVBox;
    @FXML
    private HBox graphTitleHbox;
    @FXML
    private HBox toolBarHBox;
    @FXML
    private VBox selectSpotsVBox;
    @FXML
    private ScrollPane expressionScrollPane;
    @FXML
    private ComboBox<String> unknownGroupsComboBox;

    //PEEK TABS
    @FXML
    private Tab unkTab;
    @FXML
    private Tab refMatTab;
    @FXML
    private Tab selectSpotsTab;
    @FXML
    private TabPane spotTabPane;

    private static final String OPERATION_FLAG_DELIMITER = " : ";
    private static final String NUMBERSTRING = "NUMBER";
    private final BooleanProperty whiteSpaceVisible = new SimpleBooleanProperty(true);
    private static final String INVISIBLENEWLINEPLACEHOLDER = " \n";
    private static final String VISIBLENEWLINEPLACEHOLDER = "\u23CE\n";
    private static final String INVISIBLETABPLACEHOLDER = "  ";
    private static final String VISIBLETABPLACEHOLDER = " \u21E5";
    private static final String VISIBLEWHITESPACEPLACEHOLDER = "\u2423";
    private static final String INVISIBLEWHITESPACEPLACEHOLDER = " ";
    private final Map<String, String> presentationMap = new HashMap<>();

    {
        presentationMap.put("New line", VISIBLENEWLINEPLACEHOLDER);
        presentationMap.put("Tab", VISIBLETABPLACEHOLDER);
        presentationMap.put("White space", VISIBLEWHITESPACEPLACEHOLDER);
        whiteSpaceVisible.addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                if (newValue) {
                    presentationMap.replace("White space", VISIBLEWHITESPACEPLACEHOLDER);
                    presentationMap.replace("Tab", VISIBLETABPLACEHOLDER);
                    presentationMap.replace("New line", VISIBLENEWLINEPLACEHOLDER);
                } else {
                    presentationMap.replace("White space", INVISIBLEWHITESPACEPLACEHOLDER);
                    presentationMap.replace("Tab", INVISIBLETABPLACEHOLDER);
                    presentationMap.replace("New line", INVISIBLENEWLINEPLACEHOLDER);
                }
            }
        });
    }

    private int fontSizeModifier = 0;

    private final TextArea notesTextArea = new TextArea();

    {
        AnchorPane pane = new AnchorPane();
        pane.getChildren().setAll(notesTextArea);
        AnchorPane.setBottomAnchor(notesTextArea, 0.0);
        AnchorPane.setTopAnchor(notesTextArea, 0.0);
        AnchorPane.setRightAnchor(notesTextArea, 0.0);
        AnchorPane.setLeftAnchor(notesTextArea, 0.0);
        notesTextArea.setWrapText(true);
        EXPRESSION_NOTES_STAGE.setScene(new Scene(pane, 600, 150));
        EXPRESSION_NOTES_STAGE.setAlwaysOnTop(true);
    }

    private final ObjectProperty<String> dragOperationOrFunctionSource = new SimpleObjectProperty<>();
    private final ObjectProperty<String> dragNumberSource = new SimpleObjectProperty<>();
    private final ObjectProperty<String> dragPresentationSource = new SimpleObjectProperty<>();

    private final ListProperty<String> undoListForExpression = new SimpleListProperty<>(
            FXCollections.observableArrayList());
    private final ListProperty<String> redoListForExpression = new SimpleListProperty<>(
            FXCollections.observableArrayList());

    private final ObjectProperty<Expression> selectedExpression = new SimpleObjectProperty<>();
    private final StringProperty expressionString = new SimpleStringProperty();
    private final BooleanProperty selectedExpressionIsEditable = new SimpleBooleanProperty(false);
    // Boolean to prevent editing of names of built-in expressions
    private final BooleanProperty selectedExpressionIsBuiltIn = new SimpleBooleanProperty(false);
    //Boolean to save whether or not the expression has been saved since the last modification
    private final BooleanProperty expressionIsSaved = new SimpleBooleanProperty(true);
    //Boolean to save whether the expression is currently edited as a textArea or with drag and drop
    private final BooleanProperty editAsText = new SimpleBooleanProperty(false);

    private final BooleanProperty hasRatioOfInterest = new SimpleBooleanProperty(false);

    private final ObjectProperty<Mode> currentMode = new SimpleObjectProperty<>(Mode.EDIT);

    private enum Mode {

        EDIT("Edit"), CREATE("Create"), VIEW("View");

        private final String printString;

        private Mode(String printString) {
            this.printString = printString;
        }

        @Override
        public String toString() {
            return printString;
        }
    }

    //List of operator used to detect if a string should be an operator node or not
    private final List<String> listOperators = new ArrayList<>();
    //List of all the expressions
    ObservableList<Expression> namedExpressions;

    List<Expression> removedExpressions = new ArrayList<>();

    private Expression selectedBeforeCreateOrCopy;

    boolean changeFromUndoRedo = false;

    boolean needUpdateExpressions = false;

    Text insertIndicator = new Text("|");

    {
        insertIndicator.setFill(Color.RED);
        insertIndicator.setStyle(EXPRESSION_LIST_CSS_STYLE_SPECS);
    }

    public static Expression expressionToHighlightOnInit = null;

    private Map<String, Tooltip> tooltipsMap = new HashMap<>();

    private Map<KeyCode, Boolean> keyMap = new HashMap<>();

    private ObservableList<ExpressionTextNode> selectedNodes = FXCollections.observableArrayList();

    private TaskInterface task;

    //INIT
    @Override
    public void initialize(URL url, ResourceBundle rb) {

        task = squidProject.getTask();
        // update
        task.setupSquidSessionSpecsAndReduceAndReport(false);

        initPropertyBindings();
        initListViews();
        initRadios();
        initExpressionTextFlowAndTextArea();
        initGraph();
        initExpressionSelection();
        initNodeSelection();
        initKey();
        initPeekAreas();

        currentMode.set(Mode.VIEW);

        expressionAsTextArea.setWrapText(true);

        if (expressionToHighlightOnInit != null) {
            selectInAllPanes(expressionToHighlightOnInit, true);
            expressionToHighlightOnInit = null;
        } else if (!customExpressionsListView.getItems().isEmpty()) {
            selectInAllPanes(customExpressionsListView.getItems().get(0), true);
        }
    }

    private void initPeekAreas() {
        rmPeekTextArea.setStyle(SquidUI.PEEK_LIST_CSS_STYLE_SPECS);
        createCopyToClipboardContextMenu(rmPeekTextArea);
        unPeekTextArea.setStyle(SquidUI.PEEK_LIST_CSS_STYLE_SPECS);
        createCopyToClipboardContextMenu(unPeekTextArea);
    }

    private void initPropertyBindings() {
        //Disable bindings
        editorVBox.disableProperty().bind(selectedExpression.isNull());
        expressionUndoBtn.disableProperty()
                .bind(undoListForExpression.sizeProperty().lessThan(1).or(currentMode.isEqualTo(Mode.VIEW)));
        expressionRedoBtn.disableProperty()
                .bind(redoListForExpression.sizeProperty().lessThan(1).or(currentMode.isEqualTo(Mode.VIEW)));
        editExpressionBtn.disableProperty()
                .bind(currentMode.isNotEqualTo(Mode.VIEW).or(selectedExpressionIsEditable.not()));
        copyExpressionIntoCustomBtn.disableProperty()
                .bind(currentMode.isNotEqualTo(Mode.VIEW).or(selectedExpression.isNull()));
        createExpressionBtn.disableProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        expressionClearBtn.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW));
        expressionPasteBtn.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW));
        saveBtn.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW)
                .or(expressionNameTextField.textProperty().isEmpty()).or(expressionIsSaved));

        expressionAsTextBtn.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW));

        refMatSwitchCheckBox.disableProperty()
                .bind(currentMode.isEqualTo(Mode.VIEW).or(selectedExpressionIsBuiltIn));
        unknownsSwitchCheckBox.disableProperty()
                .bind(currentMode.isEqualTo(Mode.VIEW).or(selectedExpressionIsBuiltIn));
        concRefMatSwitchCheckBox.disableProperty()
                .bind(currentMode.isEqualTo(Mode.VIEW).or(selectedExpressionIsBuiltIn));
        //specialUPbThSwitchCheckBox.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW));
        summaryCalculationSwitchCheckBox.disableProperty()
                .bind(currentMode.isEqualTo(Mode.VIEW).or(selectedExpressionIsBuiltIn));
        NUSwitchCheckBox.setDisable(true);//NUSwitchCheckBox.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW).or(hasRatioOfInterest.not()));

        unknownGroupsComboBox.disableProperty()
                .bind(currentMode.isEqualTo(Mode.VIEW).or(selectedExpressionIsBuiltIn));
        unknownGroupsComboBox.visibleProperty().bind(unknownsSwitchCheckBox.selectedProperty()
                .and(refMatSwitchCheckBox.selectedProperty().not()).and(selectedExpressionIsBuiltIn.not()));
        unknownGroupsComboBox.setItems(FXCollections.observableArrayList(
                (String[]) task.getMapOfUnknownsBySampleNames().keySet().toArray(new String[0])));
        unknownGroupsComboBox.setValue(SpotTypes.UNKNOWN.getPlotType());
        unknownGroupsComboBox.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
            @Override
            public ListCell<String> call(ListView<String> param) {
                ListCell<String> cell = new ListCell<String>() {
                    @Override
                    protected void updateItem(String item, boolean empty) {
                        super.updateItem(item, empty);
                        setText(item);
                        setTextFill(RED);
                    }
                };
                return cell;
            }
        });
        unknownGroupsComboBox.setButtonCell(new TextFieldListCell<String>(new StringConverter<String>() {
            @Override
            public String toString(String object) {
                return object;
            }

            @Override
            public String fromString(String string) {
                return string;
            }
        }));

        expressionNameTextField.editableProperty()
                .bind(currentMode.isNotEqualTo(Mode.VIEW).and(selectedExpressionIsBuiltIn.not()));

        showCurrentExpressionBtn.disableProperty()
                .bind(selectedExpression.isNull().or(currentMode.isEqualTo(Mode.CREATE)));
        //        cancelBtn.disableProperty().bind(selectedExpression.isNull());
        cancelBtn.disableProperty().bind(editExpressionBtn.disabledProperty().not()
                // .or(selectedExpressionIsEditable.not()
                .or(currentMode.isEqualTo(Mode.VIEW)));
        othersAccordion.disableProperty().bind(currentMode.isEqualTo(Mode.VIEW));
        hintHoverText.visibleProperty().bind(editAsText.not());
        hintSelectText.visibleProperty().bind(editAsText.not().and(currentMode.isNotEqualTo(Mode.VIEW)));
        BooleanProperty containsWhiteSpaces = new SimpleBooleanProperty(false);
        expressionString.addListener((observable, oldValue, newValue) -> {
            if (newValue != null && (newValue.contains(" ") || newValue.contains("\t") || newValue.contains("\r")
                    || newValue.contains("\n"))) {
                containsWhiteSpaces.set(true);
            } else {
                containsWhiteSpaces.set(false);
            }
        });
        toggleWhiteSpacesBtn.visibleProperty().bind(editAsText.not().and(containsWhiteSpaces));

        notesTextArea.editableProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        notesTextArea.textProperty().addListener((observable, oldValue, newValue) -> {
            refreshSaved();
        });
        EXPRESSION_NOTES_STAGE.titleProperty()
                .bind(new SimpleStringProperty("Notes on ").concat(expressionNameTextField.textProperty()));
        EXPRESSION_NOTES_STAGE.setOnCloseRequest((event) -> {
            showNotesBtn.setText("Show notes");
        });
        mainPane.visibleProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue == false) {
                EXPRESSION_NOTES_STAGE.hide();
            }
        });

        toolBarHBox.visibleProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        expressionClearBtn.visibleProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        expressionPasteBtn.visibleProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        expressionAsTextBtn.visibleProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        expressionUndoBtn.visibleProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));
        expressionRedoBtn.visibleProperty().bind(currentMode.isNotEqualTo(Mode.VIEW));

        currentMode.addListener((observable, oldValue, newValue) -> {
            //Updating peeks
            if (expressionString.isNotNull().get()) {
                populatePeeks(makeExpression(expressionNameTextField.getText(), expressionString.get()));
            }
            //Showing and hiding elements following mode
            if (oldValue == Mode.VIEW) {
                //toolBarVBox.getChildren().add(toolBarHBox);
                //toolBarHBox.setVisible(true);
                othersAccordion.setExpandedPane(operationsTitledPane);
                leftSplitPane.setDividerPositions(0.5);
            } else if (newValue == Mode.VIEW) {
                //toolBarVBox.getChildren().remove(toolBarHBox);
                //toolBarHBox.setVisible(false);
                othersAccordion.setExpandedPane(null);
                leftSplitPane.setDividerPositions(1.0);
            }

        });

        leftSplitPane.getDividers().get(0).positionProperty().addListener((o, ol, n) -> {
            //Block left pane divider in edit mode
            if (currentMode.get().equals(Mode.VIEW)) {
                leftSplitPane.setDividerPositions(1.0);
            }
        });

        //Accordions are growing only when they are expanded to avoid empty spaces
        othersAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue == null) {
                VBox.setVgrow(othersAccordion, null);
            } else {
                VBox.setVgrow(othersAccordion, Priority.ALWAYS);
            }
        });
        expressionsAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue == null) {
                VBox.setVgrow(expressionsAccordion, null);
            } else {
                VBox.setVgrow(expressionsAccordion, Priority.ALWAYS);
            }
        });

        //Prevent from clipping
        expressionTextFlow.maxWidthProperty().bind(expressionPane.widthProperty());
    }

    @FXML
    public void showExpressionDetailsOnAction(ActionEvent actionEvent) {
        File expressionFile = new File("Expression_Details.html");
        ExpressionPublisher.createHTMLDocumentFromExpression(expressionFile, selectedExpression.getValue(),
                squidProject);
        BrowserControl.showURI(expressionFile.getAbsolutePath());
    }

    @FXML
    private void expressionSortToggleAction(ActionEvent event) {
        String flag = ((RadioButton) event.getSource()).getId();
        orderExpressionListsByFlag(flag);
    }

    private void orderExpressionListsByFlag(String flag) {
        orderListViewByFlag(customExpressionsListView, flag);
        orderListViewByFlag(nuSwitchedExpressionsListView, flag);
        orderListViewByFlag(builtInExpressionsListView, flag);
        orderListViewByFlag(brokenExpressionsListView, flag);

        // special cases
        orderListViewByFlag(referenceMaterialsListView, "NAME");
        orderListViewByFlag(parametersListView, "NAME");
    }

    private void orderListViewByFlag(ListView<Expression> listView, String flag) {
        ObservableList<Expression> items = listView.getItems();
        IntuitiveStringComparator<String> intuitiveStringComparator = new IntuitiveStringComparator<>();

        switch (flag) {
        case "EXEC":
            listView.setItems(items.sorted((exp1, exp2) -> {
                if ((exp1.amHealthy() && exp2.amHealthy()) || (!exp1.amHealthy() && !exp2.amHealthy())) {
                    return namedExpressions.indexOf(exp1) - namedExpressions.indexOf(exp2);
                } else if (!exp1.amHealthy() && exp2.amHealthy()) {
                    return 1;
                } else {
                    return -1;
                }
            }));
            break;

        case "TARGET":
            // order by ConcRefMat then RU then R then U
            listView.setItems(items.sorted((exp1, exp2) -> {
                // ConcRefMat
                if (exp1.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()
                        && !exp2.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()) {
                    return -1;
                    // ConcRefMat
                } else if (!exp1.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()
                        && exp2.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()) {
                    return 1;
                    //RU
                } else if (exp1.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && exp1.getExpressionTree().isSquidSwitchSAUnknownCalculation()
                        && exp2.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && exp2.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                    return intuitiveStringComparator.compare(exp1.getName(), exp2.getName());
                    // RU
                } else if (exp1.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && exp1.getExpressionTree().isSquidSwitchSAUnknownCalculation()
                        && (!exp2.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                                || !exp2.getExpressionTree().isSquidSwitchSAUnknownCalculation())) {
                    return -1;
                    // R
                } else if (exp1.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && !exp1.getExpressionTree().isSquidSwitchSAUnknownCalculation()
                        && exp2.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && !exp2.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                    return intuitiveStringComparator.compare(exp1.getName(), exp2.getName());
                    // R
                } else if (exp1.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && !exp1.getExpressionTree().isSquidSwitchSAUnknownCalculation()
                        && !exp2.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && exp2.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                    return -1;
                    // U
                } else if (!exp1.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && exp1.getExpressionTree().isSquidSwitchSAUnknownCalculation()
                        && !exp2.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()
                        && exp2.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                    return intuitiveStringComparator.compare(exp1.getName(), exp2.getName());
                } else {
                    return 1;
                }

            }));
            break;

        default://"NAME":
            listView.setItems(items.sorted((exp1, exp2) -> {
                return exp1.getName().toLowerCase().compareTo(exp2.getName().toLowerCase());
            }));
            break;
        }
    }

    private void initRadios() {
        toggleGroup = new ToggleGroup();
        dragndropLeftRadio.setToggleGroup(toggleGroup);
        dragndropReplaceRadio.setToggleGroup(toggleGroup);
        dragndropRightRadio.setToggleGroup(toggleGroup);
        toggleGroup.selectToggle(dragndropRightRadio);
    }

    private void customizeBrokenExpressionsTitledPane() {
        if ((brokenExpressionsListView.getItems() == null) || (brokenExpressionsListView.getItems().isEmpty())) {
            brokenExpressionsTitledPane
                    .setStyle("-fx-font-size: 12; -fx-text-fill: black; -fx-font-family: SansSerif;");
        } else {
            brokenExpressionsTitledPane
                    .setStyle("-fx-font-size: 12; -fx-text-fill: red; -fx-font-family: SansSerif;");
        }
    }

    private void initListViews() {
        //EXPRESSIONS
        brokenExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        brokenExpressionsListView.setCellFactory(new ExpressionCellFactory(true));
        brokenExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        //Listener to update the filter tab when a new value is selected in the broken expression category
        brokenExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<Expression>() {
                    @Override
                    public void changed(ObservableValue<? extends Expression> observable, Expression oldValue,
                            Expression newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                selectedExpressionIsEditable.set(true);
                                selectedExpressionIsBuiltIn.set(false);
                                selectedExpression.set(newValue);
                            }
                            selectInAllPanes(newValue, false);
                        }
                        customizeBrokenExpressionsTitledPane();
                    }
                });
        customizeBrokenExpressionsTitledPane();

        nuSwitchedExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        nuSwitchedExpressionsListView.setCellFactory(new ExpressionCellFactory());
        nuSwitchedExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        //Same listener for each category
        nuSwitchedExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<Expression>() {
                    @Override
                    public void changed(ObservableValue<? extends Expression> observable, Expression oldValue,
                            Expression newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                selectedExpressionIsEditable.set(true);
                                selectedExpressionIsBuiltIn
                                        .set(newValue.getExpressionTree().isSquidSpecialUPbThExpression());
                                selectedExpression.set(newValue);
                            }
                            selectInAllPanes(newValue, false);
                        }
                    }
                });

        builtInExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        builtInExpressionsListView.setCellFactory(new ExpressionCellFactory());
        builtInExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        builtInExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<Expression>() {
                    @Override
                    public void changed(ObservableValue<? extends Expression> observable, Expression oldValue,
                            Expression newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                selectedExpressionIsEditable.set(true);
                                selectedExpressionIsBuiltIn.set(true);
                                selectedExpression.set(newValue);
                            }
                            selectInAllPanes(newValue, false);
                        }
                    }
                });

        customExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        customExpressionsListView.setCellFactory(new ExpressionCellFactory());
        customExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        customExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<Expression>() {
                    @Override
                    public void changed(ObservableValue<? extends Expression> observable, Expression oldValue,
                            Expression newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                selectedExpressionIsEditable.set(true);
                                selectedExpressionIsBuiltIn.set(false);
                                selectedExpression.set(newValue);

                            }
                            selectInAllPanes(newValue, false);
                        }
                    }
                });

        // REFERERENCE MATERIAL VALUES        
        referenceMaterialsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        referenceMaterialsListView.setCellFactory(new ExpressionCellFactory());
        referenceMaterialsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        referenceMaterialsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<Expression>() {
                    @Override
                    public void changed(ObservableValue<? extends Expression> observable, Expression oldValue,
                            Expression newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                selectedExpressionIsEditable.set(false);
                                selectedExpressionIsBuiltIn.set(false);
                                selectedExpression.set(newValue);
                            }
                            selectInAllPanes(newValue, false);
                        }
                    }
                });
        // PARAMETER VALUES        
        parametersListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        parametersListView.setCellFactory(new ExpressionCellFactory());
        parametersListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        parametersListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Expression>() {
            @Override
            public void changed(ObservableValue<? extends Expression> observable, Expression oldValue,
                    Expression newValue) {
                if (newValue != null) {
                    if (currentMode.get().equals(Mode.VIEW)) {
                        selectedExpressionIsEditable.set(false);
                        selectedExpressionIsBuiltIn.set(false);
                        selectedExpression.set(newValue);
                    }
                    selectInAllPanes(newValue, false);
                }
            }
        });

        populateExpressionListViews();

        //RATIOS
        ratioExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        ratioExpressionsListView.setCellFactory(new SquidRatiosModelCellFactory());
        ratioExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        ratioExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<SquidRatiosModel>() {
                    @Override
                    public void changed(ObservableValue<? extends SquidRatiosModel> observable,
                            SquidRatiosModel oldValue, SquidRatiosModel newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                Expression expr = new Expression(
                                        task.getNamedExpressionsMap().get(newValue.getRatioName()),
                                        "[\"" + newValue.getRatioName() + "\"]", false, false, false);
                                expr.getExpressionTree().setSquidSpecialUPbThExpression(true);
                                expr.getExpressionTree().setSquidSwitchSTReferenceMaterialCalculation(true);
                                expr.getExpressionTree().setSquidSwitchSAUnknownCalculation(true);
                                selectedExpressionIsEditable.set(false);
                                selectedExpressionIsBuiltIn.set(false);
                                selectedExpression.set(expr);
                            }
                            selectInAllPanes(null, false);
                        }
                    }
                });

        populateRatiosListView();

        //ISOTOPES
        isotopesExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        isotopesExpressionsListView.setCellFactory(new SquidSpeciesModelCellFactory());
        isotopesExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        isotopesExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<SquidSpeciesModel>() {
                    @Override
                    public void changed(ObservableValue<? extends SquidSpeciesModel> observable,
                            SquidSpeciesModel oldValue, SquidSpeciesModel newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                Expression expr = new Expression(
                                        task.getNamedExpressionsMap().get(newValue.getIsotopeName()),
                                        "TotalCPS([\"" + newValue.getIsotopeName() + "\"])", false, false, false);
                                expr.getExpressionTree().setSquidSpecialUPbThExpression(true);
                                expr.getExpressionTree().setSquidSwitchSTReferenceMaterialCalculation(true);
                                expr.getExpressionTree().setSquidSwitchSAUnknownCalculation(true);
                                selectedExpressionIsEditable.set(false);
                                selectedExpressionIsBuiltIn.set(false);
                                selectedExpression.set(expr);
                            }
                            selectInAllPanes(null, false);
                        }
                    }
                });

        populateIsotopesListView();

        //Spot Meta Data
        spotMetaDataExpressionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        spotMetaDataExpressionsListView.setCellFactory(new ExpressionTreeCellFactory());
        spotMetaDataExpressionsListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        spotMetaDataExpressionsListView.getSelectionModel().selectedItemProperty()
                .addListener(new ChangeListener<ExpressionTreeInterface>() {
                    @Override
                    public void changed(ObservableValue<? extends ExpressionTreeInterface> observable,
                            ExpressionTreeInterface oldValue, ExpressionTreeInterface newValue) {
                        if (newValue != null) {
                            if (currentMode.get().equals(Mode.VIEW)) {
                                Expression expr = new Expression(
                                        task.getNamedExpressionsMap().get(newValue.getName()), newValue.getName(),
                                        false, false, false);
                                expr.getExpressionTree().setSquidSpecialUPbThExpression(true);
                                expr.getExpressionTree().setSquidSwitchSTReferenceMaterialCalculation(true);
                                expr.getExpressionTree().setSquidSwitchSAUnknownCalculation(true);
                                selectedExpressionIsEditable.set(false);
                                selectedExpressionIsBuiltIn.set(false);
                                selectedExpression.set(expr);
                            }
                            selectInAllPanes(null, false);
                        }
                    }
                });

        populateSpotMetaDataListView();

        //OPERATIONS AND FUNCTIONS
        operationsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        operationsListView.setCellFactory(new StringCellFactory(dragOperationOrFunctionSource));

        mathFunctionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        mathFunctionsListView.setCellFactory(new StringCellFactory(dragOperationOrFunctionSource));

        squidFunctionsCommonListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        squidFunctionsCommonListView.setCellFactory(new StringCellFactory(dragOperationOrFunctionSource));

        squidFunctionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        squidFunctionsListView.setCellFactory(new StringCellFactory(dragOperationOrFunctionSource));

        logicFunctionsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        logicFunctionsListView.setCellFactory(new StringCellFactory(dragOperationOrFunctionSource));

        populateOperationOrFunctionListViews();

        //NUMBERS
        constantsListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        constantsListView.setCellFactory(new StringCellFactory(dragNumberSource));
        populateNumberListViews();

        //PRESENTATION
        presentationListView.setStyle(SquidUI.EXPRESSION_LIST_CSS_STYLE_SPECS);
        presentationListView.setCellFactory(new StringCellFactory(dragPresentationSource));
        presentationListView.getItems().addAll(presentationMap.keySet());
    }

    private void selectInAllPanes(Expression exp, boolean scrollIfAlreadySelected) {
        Boolean found = false;
        //If nothing is selected or the selected value is not the new one
        if (brokenExpressionsListView.getSelectionModel().getSelectedItem() == null
                || !brokenExpressionsListView.getSelectionModel().getSelectedItem().equals(exp)) {
            //Clear selection
            brokenExpressionsListView.getSelectionModel().clearSelection();
            //If the new value is on this pane then select it
            if (brokenExpressionsListView.getItems().contains(exp)) {

                brokenExpressionsListView.getSelectionModel().select(exp);
                brokenExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(brokenExpressionsTitledPane);
            }
        } else {
            found = true;
            if (scrollIfAlreadySelected) {
                brokenExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(brokenExpressionsTitledPane);
            }
        }

        //Same thing for the other panes
        if (nuSwitchedExpressionsListView.getSelectionModel().getSelectedItem() == null
                || !nuSwitchedExpressionsListView.getSelectionModel().getSelectedItem().equals(exp)) {
            nuSwitchedExpressionsListView.getSelectionModel().clearSelection();
            if (nuSwitchedExpressionsListView.getItems().contains(exp)) {
                nuSwitchedExpressionsListView.getSelectionModel().select(exp);
                nuSwitchedExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(nuSwitchedExpressionsTitledPane);
            }
        } else {
            found = true;
            if (scrollIfAlreadySelected) {
                nuSwitchedExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(nuSwitchedExpressionsTitledPane);
            }
        }

        if (builtInExpressionsListView.getSelectionModel().getSelectedItem() == null
                || !builtInExpressionsListView.getSelectionModel().getSelectedItem().equals(exp)) {
            builtInExpressionsListView.getSelectionModel().clearSelection();
            if (builtInExpressionsListView.getItems().contains(exp)) {
                builtInExpressionsListView.getSelectionModel().select(exp);
                builtInExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(builtInExpressionsTitledPane);
            }
        } else {
            found = true;
            if (scrollIfAlreadySelected) {
                builtInExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(builtInExpressionsTitledPane);
            }
        }

        if (customExpressionsListView.getSelectionModel().getSelectedItem() == null
                || !customExpressionsListView.getSelectionModel().getSelectedItem().equals(exp)) {
            customExpressionsListView.getSelectionModel().clearSelection();
            if (customExpressionsListView.getItems().contains(exp)) {
                customExpressionsListView.getSelectionModel().select(exp);
                customExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(customExpressionsTitledPane);
            }
        } else {
            found = true;
            if (scrollIfAlreadySelected) {
                customExpressionsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(customExpressionsTitledPane);
            }
        }

        if (referenceMaterialsListView.getSelectionModel().getSelectedItem() == null
                || !referenceMaterialsListView.getSelectionModel().getSelectedItem().equals(exp)) {
            referenceMaterialsListView.getSelectionModel().clearSelection();
            if (referenceMaterialsListView.getItems().contains(exp)) {
                referenceMaterialsListView.getSelectionModel().select(exp);
                referenceMaterialsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(referenceMaterialsTitledPane);
            }
        } else {
            found = true;
            if (scrollIfAlreadySelected) {
                referenceMaterialsListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(referenceMaterialsTitledPane);
            }
        }

        if (parametersListView.getSelectionModel().getSelectedItem() == null
                || !parametersListView.getSelectionModel().getSelectedItem().equals(exp)) {
            parametersListView.getSelectionModel().clearSelection();
            if (parametersListView.getItems().contains(exp)) {
                parametersListView.getSelectionModel().select(exp);
                parametersListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(parametersTitledPane);
            }
        } else {
            found = true;
            if (scrollIfAlreadySelected) {
                parametersListView.scrollTo(exp);
                expressionsAccordion.setExpandedPane(parametersTitledPane);
            }
        }
        if (found) {
            //If found in the expressions then it is not a ratio
            ratioExpressionsListView.getSelectionModel().clearSelection();
            isotopesExpressionsListView.getSelectionModel().clearSelection();
            spotMetaDataExpressionsListView.getSelectionModel().clearSelection();
        }
    }

    private void initExpressionTextFlowAndTextArea() {

        //Init of the textarea
        expressionAsTextArea.setFont(Font.font("Monospaced"));

        expressionAsTextArea.textProperty().bindBidirectional(expressionString);
        expressionString.addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                refreshSaved();
                if (!makeStringFromTextFlow().equals(newValue)) {
                    makeTextFlowFromString(newValue);
                }
                if (!changeFromUndoRedo) {
                    if (oldValue != null) {
                        saveUndo(oldValue);
                    }
                } else {
                    changeFromUndoRedo = false;
                }
                updateEditor();
            }
        });

        expressionNameTextField.textProperty().addListener((observable, oldValue, newValue) -> {
            if ((newValue != null) && newValue.compareTo(oldValue) != 0) {
                updateEditor();
                refreshSaved();
            }
        });

        editorVBox.setOnDragOver((event) -> {
            expressionTextFlow.getChildren().remove(insertIndicator);
        });

        //Init of the textflow following the mode
        currentMode.addListener((observable, oldValue, newValue) -> {
            switch (newValue) {
            case VIEW:
                disableExpressionTextFlowDragAndDrop();
                break;
            case CREATE:
            case EDIT:
                activeExpressionTextFlowDragAndDrop();
            }
        });

    }

    private void activeExpressionTextFlowDragAndDrop() {

        expressionScrollPane.setOnDragOver((DragEvent event) -> {
            if (event.getDragboard().hasString()) {
                if (event.getGestureSource() instanceof ExpressionTextNode) {
                    //Move an other text node
                    event.acceptTransferModes(TransferMode.MOVE);
                } else {
                    //Or copy a new one from the lists
                    event.acceptTransferModes(TransferMode.COPY);
                }
            }
            event.consume();
        });

        expressionScrollPane.setOnDragEntered((event) -> {
            //Show insert position indicator on enter
            expressionTextFlow.getChildren().remove(insertIndicator);
            if (dragndropLeftRadio.isSelected()) {
                expressionTextFlow.getChildren().add(0, insertIndicator);
            } else {
                expressionTextFlow.getChildren().add(insertIndicator);
            }
        });

        expressionScrollPane.setOnDragExited((event) -> {
            //Remove insert position indicator on exit
            expressionTextFlow.getChildren().remove(insertIndicator);
        });

        expressionScrollPane.setOnDragDropped((DragEvent event) -> {

            expressionTextFlow.getChildren().remove(insertIndicator);

            Dragboard db = event.getDragboard();
            boolean success = false;

            //By default, the node will be placed at the end of the textflow
            double ord = expressionTextFlow.getChildren().size();
            if (toggleGroup.getSelectedToggle() == dragndropLeftRadio) {
                //But if the dragndropLeftRadio is selected, the node will be placed at the begining of the textflow
                ord = -1.0;
            }

            // if moving a node
            if (event.getGestureSource() instanceof ExpressionTextNode) {
                if (selectedNodes.size() > 1) {
                    //If multiple nodes update all indexes
                    for (ExpressionTextNode etn : selectedNodes) {
                        etn.setOrdinalIndex(ord + 0.001 * etn.getOrdinalIndex());
                    }
                } else {
                    //Just change the index and then update
                    ((ExpressionTextNode) event.getGestureSource()).setOrdinalIndex(ord);
                }
                updateExpressionTextFlowChildren();
                success = true;
            } // if copying a node from the lists, insert depending of the type of node
            else if (db.hasString()) {
                String content = db.getString().split(OPERATION_FLAG_DELIMITER)[0];
                if ((dragOperationOrFunctionSource.get() != null)
                        && (!db.getString().contains(OPERATION_FLAG_DELIMITER))) {
                    // case of function, make a series of objects
                    insertFunctionIntoExpressionTextFlow(content, ord);
                } else if ((dragOperationOrFunctionSource.get() != null)
                        && (db.getString().contains(OPERATION_FLAG_DELIMITER))) {
                    // case of operation
                    insertOperationIntoExpressionTextFlow(content, ord);
                } else if ((dragNumberSource.get() != null) && content.contains(NUMBERSTRING)) {
                    // case of "NUMBER"
                    insertNumberIntoExpressionTextFlow(NUMBERSTRING, ord);
                } else if (dragPresentationSource.get() != null
                        && presentationMap.containsKey(dragPresentationSource.get())) {
                    // case of presentation (new line, tab)
                    insertPresentationIntoExpressionTextFlow(presentationMap.get(dragPresentationSource.get()),
                            ord);
                } else {
                    // case of expression
                    insertExpressionIntoExpressionTextFlow(content, ord);
                }

                success = true;
            }

            event.setDropCompleted(success);

            event.consume();
            resetDragSources();
        });
    }

    private void disableExpressionTextFlowDragAndDrop() {
        expressionScrollPane.setOnDragOver((DragEvent event) -> {
            //Nothing
        });

        expressionScrollPane.setOnDragDropped((DragEvent event) -> {
            //Nothing
        });
    }

    private void initExpressionSelection() {
        //Listener that updates the whole builder when the expression to edit is changed
        selectedExpression.addListener((observable, oldValue, newValue) -> {
            if (needUpdateExpressions) {
                task.updateAllExpressions(true);
                needUpdateExpressions = false;
            }
            if (editAsText.get()) {
                expressionAsTextAction(new ActionEvent());
            }
            if (newValue != null) {
                expressionNameTextField.textProperty().set(newValue.getName());
                notesTextArea.setText(newValue.getNotes());
                refMatSwitchCheckBox.setSelected(((ExpressionTree) newValue.getExpressionTree())
                        .isSquidSwitchSTReferenceMaterialCalculation());
                unknownsSwitchCheckBox.setSelected(
                        ((ExpressionTree) newValue.getExpressionTree()).isSquidSwitchSAUnknownCalculation());
                unknownGroupsComboBox
                        .setValue(((ExpressionTree) newValue.getExpressionTree()).getUnknownsGroupSampleName());
                concRefMatSwitchCheckBox.setSelected(((ExpressionTree) newValue.getExpressionTree())
                        .isSquidSwitchConcentrationReferenceMaterialCalculation());
                summaryCalculationSwitchCheckBox.setSelected(
                        ((ExpressionTree) newValue.getExpressionTree()).isSquidSwitchSCSummaryCalculation());
                specialUPbThSwitchCheckBox.setSelected(
                        ((ExpressionTree) newValue.getExpressionTree()).isSquidSpecialUPbThExpression());
                NUSwitchCheckBox.setSelected(newValue.isSquidSwitchNU());
                expressionString.set(null);
                expressionString.set(newValue.getExcelExpressionString());
                hasRatioOfInterest.set(((ExpressionTree) newValue.getExpressionTree()).hasRatiosOfInterest());
                selectedExpressionIsBuiltIn.set(newValue.getExpressionTree().isSquidSpecialUPbThExpression());
                populateSpotsSelection(newValue);
            } else {
                expressionNameTextField.clear();
                expressionTextFlow.getChildren().clear();
                refMatSwitchCheckBox.setSelected(false);
                unknownsSwitchCheckBox.setSelected(false);
                unknownGroupsComboBox.setValue(SpotTypes.UNKNOWN.getPlotType());
                concRefMatSwitchCheckBox.setSelected(false);
                selectedExpressionIsEditable.set(false);
                selectedExpressionIsBuiltIn.set(false);
                expressionString.set("");
                hasRatioOfInterest.set(false);
            }
            undoListForExpression.clear();
            redoListForExpression.clear();
        });
    }

    private void initGraph() {

        //Update graph when change in preferences
        showGraphCheckBox.setOnAction((event) -> {
            graphExpressionTree(
                    makeExpression(expressionNameTextField.getText(), expressionString.get()).getExpressionTree());
        });
        graphBrowserCheckBox.setOnAction((event) -> {
            graphExpressionTree(
                    makeExpression(expressionNameTextField.getText(), expressionString.get()).getExpressionTree());
        });
    }

    private void initKey() {
        for (KeyCode key : KeyCode.values()) {
            keyMap.put(key, false);
        }
        mainPane.setOnKeyPressed((event) -> {
            keyMap.put(event.getCode(), true);
        });
        mainPane.setOnKeyReleased((event) -> {
            keyMap.put(event.getCode(), false);
        });
    }

    //CREATE EDIT DELETE SAVE CANCEL ACTIONS
    @FXML
    private void newCustomExpressionAction(ActionEvent event) {
        if (currentMode.get().equals(Mode.VIEW)) {
            selectedBeforeCreateOrCopy = selectedExpression.get();
            Expression exp = new Expression("", "");
            selectedExpression.set(exp);
            currentMode.set(Mode.CREATE);
            selectedExpressionIsEditable.setValue(true);
            refreshSaved();
            expressionNameTextField.requestFocus();
        }
    }

    @FXML
    private void copyIntoCustomExpressionAction(ActionEvent event) {
        if (currentMode.get().equals(Mode.VIEW)) {
            selectedBeforeCreateOrCopy = selectedExpression.get();
            Expression exp = copySelectedExpression();
            exp.setName("copy of " + exp.getName());
            selectedExpression.set(exp);
            currentMode.set(Mode.CREATE);
            refreshSaved();
            expressionNameTextField.requestFocus();
        }
    }

    @FXML
    private void editCustomExpressionAction(ActionEvent event) {
        if (selectedExpressionIsEditable.get() && currentMode.get().equals(Mode.VIEW)) {
            currentMode.set(Mode.EDIT);
            refreshSaved();
        }
    }

    @FXML
    private void cancelAction(ActionEvent event) {
        if (!currentMode.get().equals(Mode.EDIT)) {
            currentMode.set(Mode.VIEW);
            selectedExpression.set(null);
            selectedExpression.set(selectedBeforeCreateOrCopy);
            selectInAllPanes(selectedBeforeCreateOrCopy, true);
            selectedBeforeCreateOrCopy = null;
        } else {
            currentMode.set(Mode.VIEW);
            //Reselect the unmodified expression
            Expression exp = selectedExpression.get();
            selectedExpression.set(null);
            selectedExpression.set(exp);
            selectInAllPanes(exp, true);
            selectedExpressionIsEditable.set(true);
        }
    }

    @FXML
    private void saveAction(ActionEvent event) {

        boolean nameExists = task.expressionExists(new Expression(expressionNameTextField.getText(), ""));

        if (!nameExists || (currentMode.get().equals(Mode.EDIT)
                && selectedExpression.get().getName().equals(expressionNameTextField.getText()))) {
            save();
        } else {
            //Case name already exists -> ask for replacing
            Alert alert = new Alert(Alert.AlertType.WARNING,
                    "An expression already exists with this name. Do you want to replace it?", ButtonType.YES,
                    ButtonType.NO);
            alert.setX(SquidUI.primaryStageWindow.getX() + (SquidUI.primaryStageWindow.getWidth() - 200) / 2);
            alert.setY(SquidUI.primaryStageWindow.getY() + (SquidUI.primaryStageWindow.getHeight() - 150) / 2);
            alert.showAndWait().ifPresent((t) -> {
                if (t.equals(ButtonType.YES)) {
                    save();
                }
            });
        }
    }

    //EXPRESSION ACTIONS
    @FXML
    private void expressionClearAction(ActionEvent event) {
        //Clear the textflow
        if (!currentMode.get().equals(Mode.VIEW)) {
            expressionString.set("");
        }
    }

    @FXML
    private void expressionCopyAction(ActionEvent event) {
        //Copy in clipboard
        String fullText = expressionString.get();
        Clipboard clipboard = Clipboard.getSystemClipboard();
        ClipboardContent content = new ClipboardContent();
        content.putString(fullText);
        clipboard.setContent(content);
    }

    @FXML
    private void expressionPasteAction(ActionEvent event) {
        //Build textflow from clipboard
        if (!currentMode.get().equals(Mode.VIEW)) {
            Clipboard clipboard = Clipboard.getSystemClipboard();
            String content = clipboard.getString();
            expressionString.set(content);
        }
    }

    @FXML
    private void expressionUndoAction(ActionEvent event) {
        //Try to restore the last saved state
        try {
            String last = undoListForExpression.get(undoListForExpression.size() - 1);
            redoListForExpression.add(expressionString.get());
            changeFromUndoRedo = true;
            expressionString.set(last);
            undoListForExpression.remove(last);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
        }
    }

    @FXML
    private void expressionRedoAction(ActionEvent event) {
        try {
            String last = redoListForExpression.get(redoListForExpression.size() - 1);
            undoListForExpression.add(expressionString.get());
            changeFromUndoRedo = true;
            expressionString.set(last);
            redoListForExpression.remove(last);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
        }
    }

    @FXML
    private void expressionAsTextAction(ActionEvent event) {
        if (editAsText.get() == false) {
            //Case was editing with drag and drop -> switch to textArea

            editAsText.set(true);

            expressionPane.getChildren().setAll(expressionAsTextArea);
            AnchorPane.setBottomAnchor(expressionAsTextArea, 0.0);
            AnchorPane.setTopAnchor(expressionAsTextArea, 0.0);
            AnchorPane.setRightAnchor(expressionAsTextArea, 0.0);
            AnchorPane.setLeftAnchor(expressionAsTextArea, 0.0);
            expressionAsTextBtn.setText("Edit as d&d");
            expressionAsTextArea.requestFocus();

        } else {
            //Case was editing as textArea -> switch to drag and drop

            editAsText.set(false);

            expressionPane.getChildren().setAll(expressionScrollPane);
            AnchorPane.setBottomAnchor(expressionScrollPane, 0.0);
            AnchorPane.setTopAnchor(expressionScrollPane, 0.0);
            AnchorPane.setRightAnchor(expressionScrollPane, 0.0);
            AnchorPane.setLeftAnchor(expressionScrollPane, 0.0);
            expressionAsTextBtn.setText("Edit as text");

            //Rebuild because CSS doesn't apply
            expressionTextFlow.getChildren().clear();
            // remove spurious spaces from [expr]__[n] and expr  [n] ==> [][]
            String expression = expressionString.get();
            expression = expression.replaceAll("( )*\\[", "[");
            makeTextFlowFromString(expression);
        }
    }

    @FXML
    private void showCurrentExpressionAction() {
        if (selectedExpression.isNotNull().get()) {
            selectInAllPanes(selectedExpression.get(), true);
        }
    }

    @FXML
    private void showNotesAction() {
        if (!EXPRESSION_NOTES_STAGE.isShowing()) {
            showNotesBtn.setText("Hide notes");
            EXPRESSION_NOTES_STAGE
                    .setX(SquidUI.primaryStageWindow.getX() + (SquidUI.primaryStageWindow.getWidth() - 600) / 2);
            EXPRESSION_NOTES_STAGE
                    .setY(SquidUI.primaryStageWindow.getY() + (SquidUI.primaryStageWindow.getHeight() - 150) / 2);
            EXPRESSION_NOTES_STAGE.show();
        } else {
            EXPRESSION_NOTES_STAGE.hide();
            showNotesBtn.setText("Show notes");
        }
    }

    //CHECKBOX ACTIONS
    @FXML
    private void referenceMaterialCheckBoxAction(ActionEvent event) {
        concRefMatSwitchCheckBox.setSelected(false);
        updateEditor();
        refreshSaved();
    }

    @FXML
    private void unknownSamplesCheckBoxAction(ActionEvent event) {
        concRefMatSwitchCheckBox.setSelected(false);
        updateEditor();
        refreshSaved();
    }

    @FXML
    private void unknownGroupsComboBoxAction(ActionEvent event) {
        updateEditor();
        refreshSaved();
    }

    @FXML
    private void concRefMatCheckBoxAction(ActionEvent event) {
        unknownsSwitchCheckBox.setSelected(false);
        refMatSwitchCheckBox.setSelected(false);
        updateEditor();
        refreshSaved();
    }

    @FXML
    private void summaryCalculationCheckBoxAction(ActionEvent event) {
        NUSwitchCheckBox.setSelected(false);
        updateEditor();
        refreshSaved();
    }

    @FXML
    private void specialUPbThCheckBoxAction(ActionEvent event) {
        refreshSaved();
    }

    @FXML
    private void NUSwitchCheckBoxAction(ActionEvent event) {
        summaryCalculationSwitchCheckBox.setSelected(false);
        refreshSaved();
    }

    private void howToUseAction(ActionEvent event) {
        BrowserControl.showURI("https://www.youtube.com/playlist?list=PLfF8bcNRe2WTWx2IuDaHW_XpLh36bWkUc");
    }

    @FXML
    private void fontMinusAction(ActionEvent event) {
        if (EXPRESSION_BUILDER_DEFAULT_FONTSIZE + this.fontSizeModifier > EXPRESSION_BUILDER_MIN_FONTSIZE) {
            this.fontSizeModifier += -1;
            expressionAsTextArea.setFont(Font.font(expressionAsTextArea.getFont().getFamily(),
                    EXPRESSION_BUILDER_DEFAULT_FONTSIZE + fontSizeModifier));
            for (Node n : expressionTextFlow.getChildren()) {
                if (n instanceof ExpressionTextNode) {
                    ((ExpressionTextNode) n).updateFontSize();
                }
            }
            if (EXPRESSION_BUILDER_DEFAULT_FONTSIZE + this.fontSizeModifier <= EXPRESSION_BUILDER_MIN_FONTSIZE) {
                fontMinusBtn.setDisable(true);
            }
            if (EXPRESSION_BUILDER_DEFAULT_FONTSIZE + this.fontSizeModifier < EXPRESSION_BUILDER_MAX_FONTSIZE) {
                fontPlusBtn.setDisable(false);
            }
        }
    }

    @FXML
    private void fontPlusAction(ActionEvent event) {
        if (EXPRESSION_BUILDER_DEFAULT_FONTSIZE + this.fontSizeModifier < EXPRESSION_BUILDER_MAX_FONTSIZE) {
            this.fontSizeModifier += 1;
            expressionAsTextArea.setFont(Font.font(expressionAsTextArea.getFont().getFamily(),
                    EXPRESSION_BUILDER_DEFAULT_FONTSIZE + fontSizeModifier));
            for (Node n : expressionTextFlow.getChildren()) {
                if (n instanceof ExpressionTextNode) {
                    ((ExpressionTextNode) n).updateFontSize();
                }
            }
            if (EXPRESSION_BUILDER_DEFAULT_FONTSIZE + this.fontSizeModifier >= EXPRESSION_BUILDER_MAX_FONTSIZE) {
                fontPlusBtn.setDisable(true);
            }
            if (EXPRESSION_BUILDER_DEFAULT_FONTSIZE + this.fontSizeModifier > EXPRESSION_BUILDER_MIN_FONTSIZE) {
                fontMinusBtn.setDisable(false);
            }
        }
    }

    @FXML
    private void toggleWhiteSpacesAction(ActionEvent event) {
        whiteSpaceVisible.set(!whiteSpaceVisible.get());
        if (whiteSpaceVisible.get()) {
            toggleWhiteSpacesBtn.setText("Hide white spaces tokens");
        } else {
            toggleWhiteSpacesBtn.setText("Show white spaces tokens");
        }
        for (Node n : expressionTextFlow.getChildren()) {
            if (n instanceof ExpressionTextNode) {
                ((ExpressionTextNode) n).updateFontSize();
            }
        }
    }

    //POPULATE LISTS
    private void populateExpressionListViews() {

        tooltipsMap.clear();

        namedExpressions = FXCollections.observableArrayList(task.getTaskExpressionsOrdered());

        List<Expression> sortedNUSwitchedExpressionsList = new ArrayList<>();
        List<Expression> sortedBuiltInExpressionsList = new ArrayList<>();
        List<Expression> sortedCustomExpressionsList = new ArrayList<>();
        List<Expression> sortedBrokenExpressionsList = new ArrayList<>();
        List<Expression> sortedReferenceMaterialValuesList = new ArrayList<>();
        List<Expression> sortedParameterValuesList = new ArrayList<>();

        for (Expression exp : namedExpressions) {
            if (exp.amHealthy() && exp.isSquidSwitchNU() && !exp.aliasedExpression()) {
                sortedNUSwitchedExpressionsList.add(exp);
            } else if (exp.isReferenceMaterialValue() && exp.amHealthy()) {
                sortedReferenceMaterialValuesList.add(exp);
            } else if (exp.isParameterValue() && exp.amHealthy()) {
                sortedParameterValuesList.add(exp);
            } else if (exp.getExpressionTree().isSquidSpecialUPbThExpression() && exp.amHealthy()
                    && !exp.isSquidSwitchNU() && !exp.aliasedExpression()) {
                sortedBuiltInExpressionsList.add(exp);
            } else if (exp.isCustom() && exp.amHealthy()) {
                sortedCustomExpressionsList.add(exp);
            } else if (!exp.amHealthy()) {
                sortedBrokenExpressionsList.add(exp);
            }
        }

        ObservableList<Expression> items = FXCollections.observableArrayList(sortedNUSwitchedExpressionsList);
        nuSwitchedExpressionsListView.setItems(null);
        nuSwitchedExpressionsListView.setItems(items);

        items = FXCollections.observableArrayList(sortedBuiltInExpressionsList);
        builtInExpressionsListView.setItems(null);
        builtInExpressionsListView.setItems(items);

        items = FXCollections.observableArrayList(sortedCustomExpressionsList);
        customExpressionsListView.setItems(null);
        customExpressionsListView.setItems(items);

        items = FXCollections.observableArrayList(sortedBrokenExpressionsList);
        brokenExpressionsListView.setItems(null);
        brokenExpressionsListView.setItems(items);
        customizeBrokenExpressionsTitledPane();

        items = FXCollections.observableArrayList(sortedReferenceMaterialValuesList);
        referenceMaterialsListView.setItems(null);
        referenceMaterialsListView.setItems(items);

        items = FXCollections.observableArrayList(sortedParameterValuesList);
        parametersListView.setItems(null);
        parametersListView.setItems(items);

        // sort everyone
        String flag = ((RadioButton) expressionsSortToggleGroup.getSelectedToggle()).getId();
        orderExpressionListsByFlag(flag);

    }

    private void populateRatiosListView() {
        List<SquidRatiosModel> ratiosList = task.getSquidRatiosModelList();

        ObservableList<SquidRatiosModel> items = FXCollections.observableArrayList(ratiosList);
        items = items.sorted((ratio1, ratio2) -> {
            return ratio1.getRatioName().compareToIgnoreCase(ratio2.getRatioName());
        });
        ratioExpressionsListView.setItems(items);
    }

    private void populateIsotopesListView() {
        List<SquidSpeciesModel> isotopesList = task.getSquidSpeciesModelList();

        ObservableList<SquidSpeciesModel> items = FXCollections.observableArrayList(isotopesList);
        items = items.sorted((isotope1, isotope2) -> {
            return isotope1.getIsotopeName().compareToIgnoreCase(isotope2.getIsotopeName());
        });

        isotopesExpressionsListView.setItems(items);
    }

    private void populateSpotMetaDataListView() {
        List<ExpressionTreeInterface> spotMetaDataExprList = new ArrayList<>();

        for (Map.Entry<String, ExpressionTreeInterface> entry : task.getNamedSpotLookupFieldsMap().entrySet()) {
            spotMetaDataExprList.add(entry.getValue());
        }

        ObservableList<ExpressionTreeInterface> items = FXCollections.observableArrayList(spotMetaDataExprList);
        items = items.sorted((spotField1, spotField2) -> {
            return spotField1.getName().compareToIgnoreCase(spotField2.getName());
        });

        spotMetaDataExpressionsListView.setItems(items);
    }

    private void populateOperationOrFunctionListViews() {
        // operations ==========================================================
        List<String> operationStrings = new ArrayList<>();
        for (Map.Entry<String, String> op : OPERATIONS_MAP.entrySet()) {
            //Operator '$$' is not available to users
            if (!op.getKey().equals("$$")) {
                operationStrings.add(op.getKey() + OPERATION_FLAG_DELIMITER + op.getValue());
                listOperators.add(op.getKey());
            }
        }

        ObservableList<String> items = FXCollections.observableArrayList(operationStrings);
        operationsListView.setItems(items);

        // Math Functions ======================================================
        List<String> mathFunctionStrings = new ArrayList<>();
        for (Map.Entry<String, String> op : MATH_FUNCTIONS_MAP.entrySet()) {
            try {
                int argumentCount = Function.operationFactory(op.getValue()).getArgumentCount();
                StringBuilder args = new StringBuilder();
                args.append(op.getKey()).append("(");
                for (int i = 0; i < argumentCount; i++) {
                    args.append("Arg").append(i).append(i < (argumentCount - 1) ? "," : ")");
                }

                mathFunctionStrings.add(args.toString());
            } catch (Exception e) {
                // function does not have a class definition
            }
        }

        items = FXCollections.observableArrayList(mathFunctionStrings);
        items = items.sorted();
        mathFunctionsListView.setItems(items);

        // Squid Common Functions ======================================================
        List<String> squidCommonFunctionStrings = new ArrayList<>();
        for (Map.Entry<String, String> op : SQUID_COMMMON_FUNCTIONS_MAP.entrySet()) {
            if (!ALIASED_FUNCTIONS_MAP.containsKey(op.getKey())) {
                int argumentCount = Function.operationFactory(op.getValue()).getArgumentCount();
                String[] inputLabels = Function.operationFactory(op.getValue()).getLabelsForInputValues();
                StringBuilder args = new StringBuilder();
                args.append(op.getKey()).append("(");
                for (int i = 0; i < argumentCount; i++) {
                    if ((inputLabels.length > i) && inputLabels[i].indexOf("default") > 0) {
                        String[] defaultValue = inputLabels[i].split("=");
                        args.append(defaultValue[1].trim());
                    } else {
                        args.append("Arg").append(i);
                    }
                    args.append(i < (argumentCount - 1) ? "," : ")");
                }

                squidCommonFunctionStrings.add(args.toString());
            }
        }

        items = FXCollections.observableArrayList(squidCommonFunctionStrings);
        IntuitiveStringComparator<String> intuitiveStringComparator = new IntuitiveStringComparator<>();
        items = items.sorted((String func1, String func2) -> {
            return intuitiveStringComparator.compare(func1, func2);
        });
        squidFunctionsCommonListView.setItems(items);

        // Squid Functions ======================================================
        List<String> squidFunctionStrings = new ArrayList<>();
        for (Map.Entry<String, String> op : SQUID_FUNCTIONS_MAP.entrySet()) {
            if (!ALIASED_FUNCTIONS_MAP.containsKey(op.getKey())) {
                int argumentCount = Function.operationFactory(op.getValue()).getArgumentCount();
                String[] inputLabels = Function.operationFactory(op.getValue()).getLabelsForInputValues();
                StringBuilder args = new StringBuilder();
                args.append(op.getKey()).append("(");
                for (int i = 0; i < argumentCount; i++) {
                    if ((inputLabels.length > i) && inputLabels[i].indexOf("default") > 0) {
                        String[] defaultValue = inputLabels[i].split("=");
                        args.append(defaultValue[1].trim());
                    } else {
                        args.append("Arg").append(i);
                    }
                    args.append(i < (argumentCount - 1) ? "," : ")");
                }

                squidFunctionStrings.add(args.toString());
            }
        }

        items = FXCollections.observableArrayList(squidFunctionStrings);
        items = items.sorted((String func1, String func2) -> {
            return intuitiveStringComparator.compare(func1, func2);
        });
        squidFunctionsListView.setItems(items);

        // Logic Functions ======================================================
        List<String> logicFunctionStrings = new ArrayList<>();
        for (Map.Entry<String, String> op : LOGIC_FUNCTIONS_MAP.entrySet()) {
            if (!op.getKey().equalsIgnoreCase("sqIf")) {
                int argumentCount = Function.operationFactory(op.getValue()).getArgumentCount();
                StringBuilder args = new StringBuilder();
                args.append(op.getKey()).append("(");
                for (int i = 0; i < argumentCount; i++) {
                    args.append("Arg").append(i).append((i < (argumentCount - 1) ? "," : ")"));
                }

                logicFunctionStrings.add(args.toString());
            }
        }

        items = FXCollections.observableArrayList(logicFunctionStrings);
        items = items.sorted();
        logicFunctionsListView.setItems(items);
    }

    private void populateNumberListViews() {
        // constants and numbers ===============================================
        List<String> constantStrings = new ArrayList<>();
        constantStrings.add(NUMBERSTRING + OPERATION_FLAG_DELIMITER + "placeholder for number");

        for (Map.Entry<String, ExpressionTreeInterface> constant : task.getNamedConstantsMap().entrySet()) {
            constantStrings.add(
                    constant.getKey() + OPERATION_FLAG_DELIMITER + ((ConstantNode) constant.getValue()).getValue());
        }

        for (Map.Entry<String, ExpressionTreeInterface> constant : task.getNamedParametersMap().entrySet()) {
            constantStrings.add(
                    constant.getKey() + OPERATION_FLAG_DELIMITER + ((ConstantNode) constant.getValue()).getValue());
        }

        ObservableList<String> items = FXCollections.observableArrayList(constantStrings);
        constantsListView.setItems(items);
    }

    private String createPeekRM(Expression exp) {
        String result;
        if ((exp == null) || (!exp.amHealthy())) {
            result = "No expression.";
        } else {
            List<ShrimpFractionExpressionInterface> refMatSpots = task.getReferenceMaterialSpots();
            List<ShrimpFractionExpressionInterface> concRefMatSpots = task.getConcentrationReferenceMaterialSpots();
            if (exp.getExpressionTree() instanceof ConstantNode) {
                result = "Not used";
                if (exp.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()) {
                    try {
                        rmPeekTextArea.setText(exp.getName() + " = " + squid3RoundedToSize(
                                (Double) ((ConstantNode) exp.getExpressionTree()).getValue(), 15));
                    } catch (Exception e) {
                    }
                }
            } else if (exp.getExpressionTree().isSquidSwitchSCSummaryCalculation()) {
                SpotSummaryDetails spotSummary = task.getTaskExpressionsEvaluationsPerSpotSet()
                        .get(exp.getExpressionTree().getName());
                result = "No Summary";
                if (spotSummary != null) {
                    if (exp.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()) {
                        if (spotSummary.getSelectedSpots().size() > 0) {
                            result = peekDetailsPerSummary(spotSummary);
                        } else {
                            result = "No Reference Materials";
                        }
                    }
                    if (exp.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()) {
                        if (spotSummary.getSelectedSpots().size() > 0) {
                            result = peekDetailsPerSummary(spotSummary);
                        } else {
                            result = "No Concentration Reference Materials";
                        }
                    }
                }
            } else {
                result = "Reference Materials not processed.";
                if (exp.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()) {
                    if (refMatSpots.size() > 0) {
                        result = peekDetailsPerSpot(refMatSpots, exp.getExpressionTree());
                    } else {
                        result = "No Reference Materials";
                    }
                } else if (exp.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()) {
                    if (concRefMatSpots.size() > 0) {
                        result = peekDetailsPerSpot(concRefMatSpots, exp.getExpressionTree());
                    } else {
                        result = "No Concentration Reference Materials";
                    }
                }
            }
        }
        return result;
    }

    private String createPeekUN(Expression exp) {
        String res;
        if ((exp == null) || (!exp.amHealthy())) {
            res = "No expression.";
        } else {
            List<ShrimpFractionExpressionInterface> unSpots = task.getMapOfUnknownsBySampleNames()
                    .get(exp.getExpressionTree().getUnknownsGroupSampleName());
            if (exp.getExpressionTree() instanceof ConstantNode) {
                res = "Not used";
                if (exp.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                    try {
                        unPeekTextArea.setText(exp.getName() + " = " + squid3RoundedToSize(
                                (Double) ((ConstantNode) exp.getExpressionTree()).getValue(), 15));
                    } catch (Exception e) {
                    }
                }
            } else if (exp.getExpressionTree().isSquidSwitchSCSummaryCalculation()) {
                SpotSummaryDetails spotSummary = task.getTaskExpressionsEvaluationsPerSpotSet()
                        .get(exp.getExpressionTree().getName());
                res = "No Summary";
                if (spotSummary != null) {
                    if (exp.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                        if (spotSummary.getSelectedSpots().size() > 0) {
                            res = peekDetailsPerSummary(spotSummary);
                        } else {
                            res = "No Unknowns";
                        }
                    }
                }
            } else {
                res = "Unknowns not processed.";
                if (exp.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                    if (unSpots.size() > 0) {
                        res = peekDetailsPerSpot(unSpots, exp.getExpressionTree());
                    } else {
                        res = "No Unknowns";
                    }
                }
            }
        }
        return res;
    }

    private void populateSpotsSelection(Expression exp) {
        selectSpotsVBox.getChildren().clear();
        if (exp.getExpressionTree().isSquidSwitchSCSummaryCalculation()) {
            if (!spotTabPane.getTabs().contains(selectSpotsTab)) {
                spotTabPane.getTabs().add(selectSpotsTab);
            }
            selectSpotsTab.setDisable(false);
            SpotSummaryDetails spotSummaryDetail = task.getTaskExpressionsEvaluationsPerSpotSet()
                    .get(exp.getExpressionTree().getName());
            if (spotSummaryDetail != null) {

                String columnsFormat1 = "%-4s   %-10s   %-19s   %-17s";
                String columnsFormat2 = "%-4s   %-10s";

                List<ShrimpFractionExpressionInterface> selectedSpots = spotSummaryDetail.getSelectedSpots();
                ExpressionTree expTree = (ExpressionTree) exp.getExpressionTree();
                ExpressionTreeInterface etWMChild1 = null;
                ExpressionTreeInterface etWMChild2 = null;
                if (expTree.getOperation() instanceof WtdMeanACalc && exp.amHealthy()
                        && expTree.getChildrenET().size() >= 2) {
                    etWMChild1 = expTree.getChildrenET().get(0);
                    etWMChild2 = expTree.getChildrenET().get(1);
                }

                CheckBox mainCB;
                List<CheckBox> cbs = new ArrayList<>();

                if (etWMChild1 == null || etWMChild2 == null) {
                    mainCB = new CheckBox(String.format(columnsFormat2, "All", "Spot name"));
                } else {
                    mainCB = new CheckBox(String.format(columnsFormat1, "All", "Spot name", "Value", "%err"));
                }
                mainCB.setOnAction((event) -> {
                    if (mainCB.isSelected()) {
                        cbs.forEach((t) -> {
                            t.setSelected(true);
                        });
                    } else {
                        cbs.forEach((t) -> {
                            t.setSelected(false);
                        });
                    }
                });
                selectSpotsVBox.getChildren().add(mainCB);
                mainCB.setStyle(PEEK_LIST_CSS_STYLE_SPECS);
                mainCB.setDisable(!spotSummaryDetail.isManualRejectionEnabled());
                mainCB.setOpacity(0.99);

                for (int i = 0; i < selectedSpots.size(); i++) {
                    int index = i;
                    ShrimpFractionExpressionInterface spot = selectedSpots.get(i);
                    String value = "";
                    String err = "";

                    CheckBox cb;

                    if (etWMChild1 == null || etWMChild2 == null) {
                        cb = new CheckBox(String.format(columnsFormat2, "#" + i, spot.getFractionID()));
                    } else {
                        Map<ExpressionTreeInterface, double[][]> map = spot.getTaskExpressionsEvaluationsPerSpot();
                        for (Entry<ExpressionTreeInterface, double[][]> entry : map.entrySet()) {
                            if (entry.getKey().getName().equals(etWMChild1.getName())) {
                                value = "" + squid3RoundedToSize(entry.getValue()[0][0], 15);
                            }
                            if (entry.getKey().getName().equals(etWMChild2.getName())) {
                                err = "" + squid3RoundedToSize(entry.getValue()[0][0], 15);
                            }
                        }
                        cb = new CheckBox(String.format(columnsFormat1, "#" + i, spot.getFractionID(), value, err));
                    }
                    cbs.add(cb);

                    cb.setStyle(PEEK_LIST_CSS_STYLE_SPECS);
                    if (spotSummaryDetail.getRejectedIndices().length > i) {
                        cb.setSelected(!spotSummaryDetail.getRejectedIndices()[i]);
                    } else {
                        cb.setSelected(true);
                    }
                    cb.selectedProperty().addListener((observable, oldValue, newValue) -> {
                        try {
                            boolean[] reji = spotSummaryDetail.getRejectedIndices();
                            reji[index] = !newValue;
                            spotSummaryDetail.setRejectedIndices(reji);
                            spotSummaryDetail.setValues(spotSummaryDetail.eval(task));
                            populatePeeks(exp);
                            needUpdateExpressions = true;
                        } catch (SquidException ex) {
                            Logger.getLogger(ExpressionBuilderController.class.getName()).log(Level.SEVERE, null,
                                    ex);
                        }

                        boolean allSelected = true;
                        boolean anySelected = false;
                        for (CheckBox c : cbs) {
                            if (c.isSelected()) {
                                anySelected = true;
                            } else {
                                allSelected = false;
                            }
                            if (anySelected == true && allSelected == false) {
                                break;
                            }
                        }
                        if (allSelected) {
                            mainCB.setIndeterminate(false);
                            mainCB.setSelected(true);
                        } else if (anySelected) {
                            mainCB.setSelected(false);
                            mainCB.setIndeterminate(true);
                        } else {
                            mainCB.setIndeterminate(false);
                            mainCB.setSelected(false);
                        }
                    });
                    cb.setDisable(!spotSummaryDetail.isManualRejectionEnabled());
                    cb.setOpacity(0.99);
                    selectSpotsVBox.getChildren().add(cb);

                }
                boolean allSelected = true;
                boolean anySelected = false;
                for (CheckBox c : cbs) {
                    if (c.isSelected()) {
                        anySelected = true;
                    } else {
                        allSelected = false;
                    }
                    if (anySelected == true && allSelected == false) {
                        break;
                    }
                }
                if (allSelected) {
                    mainCB.setIndeterminate(false);
                    mainCB.setSelected(true);
                } else if (anySelected) {
                    mainCB.setSelected(false);
                    mainCB.setIndeterminate(true);
                } else {
                    mainCB.setIndeterminate(false);
                    mainCB.setSelected(false);
                }

            } else {
                selectSpotsTab.setDisable(true);
                spotTabPane.getTabs().remove(selectSpotsTab);
            }
        } else {
            selectSpotsTab.setDisable(true);
            spotTabPane.getTabs().remove(selectSpotsTab);
        }
    }

    private void populatePeeks(Expression exp) {
        SingleSelectionModel<Tab> selectionModel = spotTabPane.getSelectionModel();

        if ((exp == null) || (!exp.amHealthy())) {
            rmPeekTextArea.setText("No expression.");
            unPeekTextArea.setText("No expression.");
        } else if (!currentMode.get().equals(Mode.VIEW)) {
            rmPeekTextArea.setText("You must save the expression to get a peek.");
            unPeekTextArea.setText("You must save the expression to get a peek.");
        } else {
            // choose peek tab
            if (refMatTab.isSelected() && !refMatSwitchCheckBox.isSelected()
                    && !concRefMatSwitchCheckBox.isSelected()) {
                selectionModel.select(unkTab);
            } else if (unkTab.isSelected() && !unknownsSwitchCheckBox.isSelected()) {
                selectionModel.select(refMatTab);
            } else if (selectSpotsTab.isSelected() && !summaryCalculationSwitchCheckBox.isSelected()) {
                if (unknownsSwitchCheckBox.isSelected()) {
                    selectionModel.select(unkTab);
                } else {
                    selectionModel.select(refMatTab);
                }
            }

            rmPeekTextArea.setText(createPeekRM(exp));
            unPeekTextArea.setText(createPeekUN(exp));
        }
    }

    private String peekDetailsPerSummary(SpotSummaryDetails spotSummary) {
        // context-sensitivity - we use Ma in Squid for display
        boolean isAge = ((ExpressionTree) spotSummary.getExpressionTree()).getName().toUpperCase(Locale.ENGLISH)
                .contains("AGE");
        boolean isLambda = ((ExpressionTree) spotSummary.getExpressionTree()).getName().toUpperCase(Locale.ENGLISH)
                .contains("LAMBDA2");
        boolean isConcen = ((ExpressionTree) spotSummary.getExpressionTree()).getName().toUpperCase(Locale.ENGLISH)
                .contains("CONCEN");

        String[][] labels;
        try {
            OperationOrFunctionInterface op = ((ExpressionTree) spotSummary.getExpressionTree()).getOperation();
            if (op instanceof Value) {
                if (((ExpressionTree) spotSummary.getExpressionTree()).getLeftET() instanceof ShrimpSpeciesNode) {
                    labels = new String[][] { { "TotalCPS" } };
                } else if (((ExpressionTree) spotSummary.getExpressionTree()).getLeftET() instanceof ConstantNode) {
                    labels = new String[][] { { "Constant" } };
                } else {
                    labels = new String[][] { { "Value" } };
                }
            } else {
                labels = clone2dArray(op.getLabelsForOutputValues());
            }
        } catch (Exception e) {
            labels = new String[][] { { "Missing Function" } };
        }

        if (isAge) {
            labels[0][0] = "Age (Ma)";
            if (labels[0].length > 1) {
                labels[0][1] += " (Ma)";
            }
            if (labels[0].length > 2) {
                labels[0][2] += " (Ma)";
            }
        }

        if (isLambda) {
            labels[0][0] = ((ExpressionTree) spotSummary.getExpressionTree()).getName() + " (Ma)";
        }

        if (isConcen) {
            labels[0][0] = "ppm";
        }

        StringBuilder sb = new StringBuilder();
        if (concRefMatSwitchCheckBox.isSelected()) {
            sb.append("Concentration Reference Materials Only\n\n");
        }
        for (int i = 0; i < labels[0].length; i++) {
            sb.append("\t");
            if (spotSummary.getValues().length > 0) {
                // show array index in Squid3
                sb.append("[").append(i).append("] ");
                sb.append(String.format("%1$-" + 16 + "s", labels[0][i]));
                sb.append(": ");
                sb.append(squid3RoundedToSize(
                        spotSummary.getValues()[0][i] / (isAge ? 1.0e6 : ((isLambda ? 1.0e-6 : 1.0))), 15));
            } else {
                sb.append("Undefined Expression or Function");
            }
            sb.append("\n");
        }

        // handle special cases
        if (labels.length > 1) {
            sb.append("\t");
            sb.append("    ");
            sb.append(String.format("%1$-" + 16 + "s", labels[1][0]));
            sb.append(": ");
            // print list
            if (spotSummary.getValues()[1].length == 0) {
                sb.append("None");
            } else {
                for (int j = 0; j < spotSummary.getValues()[1].length; j++) {
                    sb.append((int) (spotSummary.getValues()[1][j])).append(" ");
                }
            }
            sb.append("\n");
        }

        if (labels.length > 2) {
            sb.append("\t");
            sb.append("    ");
            sb.append(String.format("%1$-" + 16 + "s", labels[2][0]));
            sb.append(": ");
            // print list
            if (spotSummary.getValues()[2].length == 0) {
                sb.append("None");
            } else {
                for (int j = 0; j < spotSummary.getValues()[2].length; j++) {
                    sb.append((int) (spotSummary.getValues()[2][j])).append(" ");
                }
            }
            sb.append("\n");
        }

        if (spotSummary.isManualRejectionEnabled()) {
            sb.append("\tManually rejected: ");
            boolean rejected = false;
            for (int i = 0; i < spotSummary.getRejectedIndices().length; i++) {
                if (spotSummary.getRejectedIndices()[i]) {
                    sb.append(i).append(" ");
                    rejected = true;
                }
            }
            if (!rejected) {
                sb.append("None");
            }
            sb.append("\n");
        }

        return sb.toString();
    }

    private String peekDetailsPerSpot(List<ShrimpFractionExpressionInterface> spots,
            ExpressionTreeInterface expTree) {
        StringBuilder sb = new StringBuilder();
        int sigDigits = 15;

        // context-sensitivity - we use Ma in Squid for display
        boolean isAge = expTree.getName().toUpperCase(Locale.ENGLISH).contains("AGE");
        String contextAgeFieldName = (isAge ? "Age(Ma)" : "Value");
        String contextAge1SigmaAbsName = (isAge ? "1\u03C3Abs(Ma)" : "1\u03C3Abs");
        // or it may be concentration ppm
        boolean isConcen = expTree.getName().toUpperCase(Locale.ENGLISH).contains("CONCEN");
        contextAgeFieldName = (isConcen ? "ppm" : contextAgeFieldName);

        if (expTree.isSquidSwitchConcentrationReferenceMaterialCalculation()) {
            sb.append("Concentration Reference Materials Only\n\n");
        }
        sb.append(String.format("%1$-" + 15 + "s", "SpotName"));
        String[][] resultLabels;
        if (((ExpressionTree) expTree).getOperation() != null) {
            if ((((ExpressionTree) expTree).getOperation().getName().compareToIgnoreCase("Value") == 0)) {
                if (((ExpressionTree) expTree).getChildrenET().get(0) instanceof VariableNodeForSummary) {
                    String uncertaintyDirective = ((VariableNodeForSummary) ((ExpressionTree) expTree)
                            .getChildrenET().get(0)).getUncertaintyDirective();
                    if (uncertaintyDirective.length() > 0) {
                        if (uncertaintyDirective.compareTo("") == 0) {
                            resultLabels = new String[][] { { contextAge1SigmaAbsName }, {} };
                        } else {
                            resultLabels = new String[][] { { "1\u03C3" + uncertaintyDirective }, {} };
                        }
                    } else {
                        resultLabels = new String[][] { { contextAgeFieldName }, {} };
                    }
                } else if (((ExpressionTree) expTree).getChildrenET().get(0) instanceof ConstantNode) {
                    resultLabels = new String[][] { { "Constant" }, {} };
                } else if (((ExpressionTree) expTree).getChildrenET().get(0) instanceof SpotFieldNode) {
                    resultLabels = new String[][] { { ((ExpressionTree) expTree).getChildrenET().get(0).getName() },
                            {} };
                } else {
                    // ShrimpSpeciesNode
                    resultLabels = new String[][] { { "TotalCPS" }, {} };
                }
            } else if (((ExpressionTree) expTree).getLeftET() instanceof ShrimpSpeciesNode) {
                // Check for functions of species
                if (((ExpressionTree) expTree).getOperation() instanceof ShrimpSpeciesNodeFunction) {
                    resultLabels = new String[][] {
                            { ((ShrimpSpeciesNodeFunction) ((ExpressionTree) expTree).getOperation()).getName() },
                            {} };
                } else {
                    // case of ratio
                    resultLabels = new String[][] { { expTree.getName(), "1\u03C3Abs", "1\u03C3%" }, {} };
                }

            } else if (((ExpressionTree) expTree).hasRatiosOfInterest()) {
                // case of NU switch
                String uncertaintyDirective = ((ExpressionTree) expTree).getUncertaintyDirective();
                if (uncertaintyDirective.length() > 0) {
                    resultLabels = new String[][] { { "1\u03C3" + uncertaintyDirective }, {} };
                } else {
                    resultLabels = new String[][] { { contextAgeFieldName, "1\u03C3Abs", "1\u03C3%" }, {} };
                }
            } else {
                // some smarts
                String[][] resultLabelsFirst = clone2dArray(
                        ((ExpressionTree) expTree).getOperation().getLabelsForOutputValues());
                resultLabels = new String[1][resultLabelsFirst[0].length == 1 ? 1 : 3];
                resultLabels[0][0] = contextAgeFieldName;
                if (resultLabelsFirst[0].length > 1) {
                    resultLabels[0][1] = contextAge1SigmaAbsName;
                    resultLabels[0][2] = "1\u03C3%";
                }
            }

            for (int i = 0; i < resultLabels[0].length; i++) {
                try {
                    sb.append(String.format("%1$-" + (i >= 2 ? 23 : 21) + "s", resultLabels[0][i]));
                } catch (Exception e) {
                }
            }

            sb.append("\n");

            // produce values
            if (((ExpressionTree) expTree).getLeftET() instanceof ShrimpSpeciesNode) {
                // Check for functions of species
                if (((ExpressionTree) expTree).getOperation() instanceof ShrimpSpeciesNodeFunction) {
                    for (ShrimpFractionExpressionInterface spot : spots) {
                        sb.append(String.format("%1$-" + 15 + "s", spot.getFractionID()));
                        try {
                            double[][] results = Arrays
                                    .stream(spot.getTaskExpressionsEvaluationsPerSpot().get(expTree))
                                    .toArray(double[][]::new);
                            for (int i = 0; i < resultLabels[0].length; i++) {
                                try {
                                    sb.append(String.format("%1$-" + 20 + "s",
                                            squid3RoundedToSize(results[0][i], sigDigits)));
                                } catch (Exception e) {
                                }
                            }
                        } catch (Exception e) {
                        }
                        sb.append("\n");
                    }
                } else if (((ExpressionTree) expTree).getOperation() instanceof Value) {
                    // case of isotope
                    for (ShrimpFractionExpressionInterface spot : spots) {
                        sb.append(String.format("%1$-" + 15 + "s", spot.getFractionID()));
                        double[][] results = Arrays.stream(spot.getTaskExpressionsEvaluationsPerSpot().get(expTree))
                                .toArray(double[][]::new);
                        for (int i = 0; i < results[0].length; i++) {
                            try {
                                sb.append(String.format("%1$-23s", squid3RoundedToSize(results[0][i], sigDigits)));
                            } catch (Exception e) {
                            }
                        }
                        sb.append("\n");
                    }
                } else {
                    // case of ratio
                    for (ShrimpFractionExpressionInterface spot : spots) {
                        sb.append(String.format("%1$-" + 15 + "s", spot.getFractionID()));
                        double[][] results = Arrays
                                .stream(spot.getIsotopicRatioValuesByStringName(expTree.getName()))
                                .toArray(double[][]::new);
                        double[] resultsWithPct = new double[3];
                        resultsWithPct[0] = results[0][0];
                        resultsWithPct[1] = results[0][1];
                        resultsWithPct[2] = calcPercentUnct(results[0]);
                        for (int i = 0; i < resultsWithPct.length; i++) {
                            try {
                                sb.append(String.format("%1$-" + (i >= 2 ? 23 : 21) + "s",
                                        squid3RoundedToSize(resultsWithPct[i], sigDigits)));
                            } catch (Exception e) {
                            }
                        }
                        sb.append("\n");
                    }
                }
            } else {
                for (ShrimpFractionExpressionInterface spot : spots) {
                    if (spot.getTaskExpressionsEvaluationsPerSpot().get(expTree) != null) {
                        sb.append(String.format("%1$-" + 15 + "s", spot.getFractionID()));
                        double[][] results = Arrays.stream(spot.getTaskExpressionsEvaluationsPerSpot().get(expTree))
                                .toArray(double[][]::new);

                        double[] resultsWithPct = new double[0];
                        if ((resultLabels[0].length == 1) && (results[0].length >= 1)) {
                            resultsWithPct = new double[1];
                            resultsWithPct[0] = squid3RoundedToSize(results[0][0] / (isAge ? 1.0e6 : 1.0),
                                    sigDigits);
                        } else if (results[0].length > 1) {
                            resultsWithPct = new double[3];
                            resultsWithPct[0] = squid3RoundedToSize(results[0][0] / (isAge ? 1.0e6 : 1.0),
                                    sigDigits);
                            resultsWithPct[1] = squid3RoundedToSize(results[0][1] / (isAge ? 1.0e6 : 1.0),
                                    sigDigits);
                            resultsWithPct[2] = calcPercentUnct(results[0]);
                        }

                        for (int i = 0; i < resultsWithPct.length; i++) {
                            try {
                                sb.append(String.format("%1$-" + (i >= 2 ? 23 : 21) + "s",
                                        squid3RoundedToSize(resultsWithPct[i], sigDigits)));
                            } catch (Exception e) {
                            }
                        }
                        sb.append("\n");
                    }
                }
            }
        } else {
            // null operation ==> SquidSpeciesNode or SpotFieldNode 
            if (expTree instanceof ShrimpSpeciesNode) {
                sb.append(String.format("%1$-23s", "TotalCPS"));
                sb.append("\n");
                for (ShrimpFractionExpressionInterface spot : spots) {
                    sb.append(String.format("%1$-" + 15 + "s", spot.getFractionID()));
                    // make copy
                    ExpressionTreeInterface expTreeSpecies = ShrimpSpeciesNode.buildShrimpSpeciesNode(
                            ((ShrimpSpeciesNode) expTree).getSquidSpeciesModel(), "getTotalCps");
                    ((Task) task).evaluateTaskExpression(expTreeSpecies);
                    try {
                        double[][] results = Arrays
                                .stream(spot.getTaskExpressionsEvaluationsPerSpot().get(expTreeSpecies))
                                .toArray(double[][]::new);
                        for (int i = 0; i < results[0].length; i++) {
                            try {
                                sb.append(String.format("%1$-" + 20 + "s",
                                        squid3RoundedToSize(results[0][i], sigDigits)));
                            } catch (Exception e) {
                            }
                        }
                    } catch (Exception e) {
                    }
                    sb.append("\n");
                }
            } else {
                sb.append(String.format("%1$-23s", expTree.getName()));
                sb.append("\n");
                for (ShrimpFractionExpressionInterface spot : spots) {
                    sb.append(String.format("%1$-" + 15 + "s", spot.getFractionID()));
                    // make copy
                    ExpressionTreeInterface expSpotField = buildSpotNode("get" + expTree.getName());
                    ((Task) task).evaluateTaskExpression(expSpotField);
                    try {
                        double[][] results = Arrays
                                .stream(spot.getTaskExpressionsEvaluationsPerSpot().get(expSpotField))
                                .toArray(double[][]::new);
                        for (int i = 0; i < results[0].length; i++) {
                            try {
                                sb.append(String.format("%1$-" + 20 + "s",
                                        squid3RoundedToSize(results[0][i], sigDigits)));
                            } catch (Exception e) {
                            }
                        }
                    } catch (Exception e) {
                    }
                    sb.append("\n");
                }
            }
        }

        return sb.toString();
    }

    private double calcPercentUnct(double[] valueModel) {
        return Math.abs(valueModel[1] / valueModel[0] * 100.0);
    }

    private ContextMenu createExpressionTextNodeContextMenu(ExpressionTextNode etn) {
        List<MenuItem> itemsForThisNode = new ArrayList<>();

        MenuItem menuItem = new MenuItem("Remove from expression");
        menuItem.setOnAction((evt) -> {
            expressionTextFlow.getChildren().remove(etn);
            updateExpressionTextFlowChildren();
        });
        itemsForThisNode.add(menuItem);

        menuItem = new MenuItem("Move left");
        menuItem.setOnAction((evt) -> {
            etn.setOrdinalIndex(etn.getOrdinalIndex() - 1.5);
            updateExpressionTextFlowChildren();
        });
        itemsForThisNode.add(menuItem);

        menuItem = new MenuItem("Move right");
        menuItem.setOnAction((evt) -> {
            etn.setOrdinalIndex(etn.getOrdinalIndex() + 1.5);
            updateExpressionTextFlowChildren();
        });
        itemsForThisNode.add(menuItem);

        Menu wrap = new Menu("Wrap in");
        itemsForThisNode.add(wrap);

        menuItem = new MenuItem("parentheses");
        menuItem.setOnAction((evt) -> {
            wrapInParentheses(etn.getOrdinalIndex(), etn.getOrdinalIndex());
        });
        wrap.getItems().add(menuItem);

        //For expressions -> allow wrap in brackets and quotes
        if (!(etn instanceof NumberTextNode || etn instanceof OperationTextNode)
                && !etn.getText().trim().matches("^[\\[\\](),]$")
                && !etn.getText().trim().matches("^\\[(?)(%?)\"(.*)\"\\](\\[\\d\\])?$")) {
            menuItem = new MenuItem("brackets and quotes");
            menuItem.setOnAction((evt) -> {
                ExpressionTextNode etn2 = new ExpressionTextNode(" [\"" + etn.getText().trim() + "\"] ");
                etn2.setOrdinalIndex(etn.getOrdinalIndex());
                expressionTextFlow.getChildren().remove(etn);
                expressionTextFlow.getChildren().add(etn2);
                updateExpressionTextFlowChildren();
            });
            wrap.getItems().add(menuItem);
        }

        // provide special modification for uncertainty
        if (!(etn instanceof NumberTextNode || etn instanceof OperationTextNode) && (etn.getText().trim()
                .matches("^\\[(?)(%?)\"(.*)\"\\]( )*(\\[\\d\\]( )*)?$")
                || etn.getText().trim().matches(
                        "^(\\[(?)(%?)\")?[a-zA-Z0-9_]*[a-zA-Z][a-zA-Z0-9]*(\"\\])?( )*(\\[\\d\\]( )*)?$"))) {

            String text = etn.getText().trim().replaceAll("(^(\\[(?)(%?)\\\")?)|((\\\"\\])?( )*(\\[\\d\\]( )*)?)",
                    "");
            Expression ex = task.getExpressionByName(text);
            if (((ex != null) && (!ex.getExpressionTree().isSquidSwitchSCSummaryCalculation())
                    && ((((ExpressionTreeBuilderInterface) ex.getExpressionTree()).getOperation()
                            .getLabelsForOutputValues()[0].length > 1) || ex.isSquidSwitchNU()))
                    || task.getRatioNames().contains(text)) {
                MenuItem menuItem1 = new MenuItem("1 \u03C3 (%)");
                menuItem1.setOnAction((evt) -> {
                    ExpressionTextNode etn2 = new ExpressionTextNode("[%\"" + text + "\"]");//    etn.getText().trim().replaceAll("\\[(?)(%?)\"", "[%\""));
                    etn2.setOrdinalIndex(etn.getOrdinalIndex());
                    expressionTextFlow.getChildren().remove(etn);
                    expressionTextFlow.getChildren().add(etn2);
                    updateExpressionTextFlowChildren();
                });
                MenuItem menuItem2 = new MenuItem("1 \u03C3 abs ()");
                menuItem2.setOnAction((evt) -> {
                    ExpressionTextNode etn2 = new ExpressionTextNode("[\"" + text + "\"]");//    etn.getText().trim().replaceAll("\\[(?)(%?)\"", "[\""));
                    etn2.setOrdinalIndex(etn.getOrdinalIndex());
                    expressionTextFlow.getChildren().remove(etn);
                    expressionTextFlow.getChildren().add(etn2);
                    updateExpressionTextFlowChildren();
                });
                MenuItem menuItem3 = new MenuItem("Use value");
                menuItem3.setOnAction((evt) -> {
                    ExpressionTextNode etn2 = new ExpressionTextNode("[\"" + text + "\"]");//    etn.getText().trim().replaceAll("\\[(?)(%?)\"", "[\""));
                    etn2.setOrdinalIndex(etn.getOrdinalIndex());
                    expressionTextFlow.getChildren().remove(etn);
                    expressionTextFlow.getChildren().add(etn2);
                    updateExpressionTextFlowChildren();
                });
                itemsForThisNode.add(new Menu("Select uncertainty...", null, menuItem1, menuItem2, menuItem3));
            }
        }

        // provide special modification for array access for summary expressions
        if (!(etn instanceof NumberTextNode || etn instanceof OperationTextNode) && (etn.getText().trim()
                .matches("^\\[(?)(%?)\"(.*)\"\\]( )*(\\[\\d\\]( )*)?$")
                || etn.getText().trim().matches(
                        "^(\\[(?)(%?)\")?[a-zA-Z0-9_]*[a-zA-Z][a-zA-Z0-9]*(\"\\])?( )*(\\[\\d\\]( )*)?$"))) {
            String text = etn.getText().trim().replaceAll("(^(\\[(?)(%?)\\\")?)|((\\\"\\])?( )*(\\[\\d\\]( )*)?)",
                    "");
            Expression ex = task.getExpressionByName(text);
            if (((ex != null) && (ex.getExpressionTree().isSquidSwitchSCSummaryCalculation()))) {
                String[] outputLabels = ((ExpressionTreeBuilderInterface) ex.getExpressionTree()).getOperation()
                        .getLabelsForOutputValues()[0];
                MenuItem[] menuItems = new MenuItem[outputLabels.length + 1];
                for (int i = 0; i < outputLabels.length; i++) {
                    final int ii = i;
                    MenuItem menuItemI = new MenuItem("[" + ii + "] = " + outputLabels[ii]);
                    menuItemI.setOnAction((evt) -> {
                        String placeHolder = etn.getText().trim().replaceAll("(\\[\\d\\]( )*)?", "").trim() + "&";
                        ExpressionTextNode etn2 = new ExpressionTextNode(
                                placeHolder.replaceAll("(\\&( )*(\\[\\d\\])?)", "\\[" + ii + "\\]"));
                        etn2.setOrdinalIndex(etn.getOrdinalIndex());
                        expressionTextFlow.getChildren().remove(etn);
                        expressionTextFlow.getChildren().add(etn2);
                        updateExpressionTextFlowChildren();
                    });
                    menuItems[i] = menuItemI;
                }

                MenuItem menuItemN = new MenuItem("None - same as '[0]'");
                menuItemN.setOnAction((evt) -> {
                    String placeHolder = etn.getText().trim().replaceAll("(\\[\\d\\]( )*)?", "").trim() + "&";
                    ExpressionTextNode etn2 = new ExpressionTextNode(placeHolder.replaceAll("\\&", ""));
                    etn2.setOrdinalIndex(etn.getOrdinalIndex());
                    expressionTextFlow.getChildren().remove(etn);
                    expressionTextFlow.getChildren().add(etn2);
                    updateExpressionTextFlowChildren();
                });

                menuItems[outputLabels.length] = menuItemN;
                itemsForThisNode.add(new Menu("Select summary statistic by index ...", null, menuItems));
            }
        }

        //for true/false : allow to invert value
        if (!(etn instanceof NumberTextNode || etn instanceof OperationTextNode)
                && (etn.getText().trim().equalsIgnoreCase("true")
                        || etn.getText().trim().equalsIgnoreCase("false"))) {
            menuItem = new MenuItem("Invert value");
            menuItem.setOnAction((evt) -> {
                String text;
                if (etn.getText().trim().equalsIgnoreCase("true")) {
                    text = " FALSE ";
                } else {
                    text = " TRUE ";
                }
                ExpressionTextNode etn2 = new ExpressionTextNode(text);
                etn2.setOrdinalIndex(etn.getOrdinalIndex());
                expressionTextFlow.getChildren().remove(etn);
                expressionTextFlow.getChildren().add(etn2);
                updateExpressionTextFlowChildren();
            });
            itemsForThisNode.add(menuItem);
        }

        // For numbers -> make an editable node
        if (etn instanceof NumberTextNode) {
            TextField editText = new TextField(etn.getText().trim());
            editText.setPrefWidth((editText.getText().trim().length() + 2) * editText.getFont().getSize());
            editText.textProperty().addListener(
                    (ObservableValue<? extends Object> observable, Object oldValue, Object newValue) -> {
                        editText.setPrefWidth(
                                (editText.getText().trim().length() + 2) * editText.getFont().getSize());
                    });
            menuItem = new MenuItem("<< Edit value then click here to save", editText);
            menuItem.setOnAction((evt) -> {
                //etn.setText(editText.getText());
                // this allows for redo of content editing
                NumberTextNode etn2 = new NumberTextNode(editText.getText());
                etn2.setOrdinalIndex(etn.getOrdinalIndex());
                expressionTextFlow.getChildren().remove(etn);
                expressionTextFlow.getChildren().add(etn2);
                updateExpressionTextFlowChildren();
            });
            itemsForThisNode.add(menuItem);
        }

        List<MenuItem> itemsForSelection = new ArrayList<>();

        //Menu items for multi-selection nodes
        if (selectedNodes.size() > 1) {
            menuItem = new MenuItem("Remove from expression");
            menuItem.setOnAction((evt) -> {
                for (ExpressionTextNode selected : selectedNodes) {
                    expressionTextFlow.getChildren().remove(selected);
                }
                updateExpressionTextFlowChildren();
            });
            itemsForSelection.add(menuItem);

            wrap = new Menu("Wrap in");
            itemsForSelection.add(wrap);

            menuItem = new MenuItem("parentheses");
            menuItem.setOnAction((evt) -> {
                List<ExpressionTextNode> nodesToAdd = new ArrayList<>();
                double begin = -1.0;
                double lastIndex = -1.0;
                for (Node node : expressionTextFlow.getChildren()) {
                    if (node instanceof ExpressionTextNode) {
                        if (selectedNodes.contains((ExpressionTextNode) node)) {
                            if (begin == -1.0) {
                                begin = ((ExpressionTextNode) node).getOrdinalIndex();
                            }
                            lastIndex = ((ExpressionTextNode) node).getOrdinalIndex();
                        } else {
                            if (begin != -1.0 && lastIndex != -1.0) {
                                ExpressionTextNode left = new ExpressionTextNode(" ( ");
                                left.setOrdinalIndex(begin - 0.5);
                                ExpressionTextNode right = new ExpressionTextNode(" ) ");
                                right.setOrdinalIndex(lastIndex + 0.5);
                                nodesToAdd.add(left);
                                nodesToAdd.add(right);
                                begin = -1.0;
                                lastIndex = -1.0;
                            }
                        }
                    }
                }
                if (begin != -1.0 && lastIndex != -1.0) {
                    ExpressionTextNode left = new ExpressionTextNode(" ( ");
                    left.setOrdinalIndex(begin - 0.5);
                    ExpressionTextNode right = new ExpressionTextNode(" ) ");
                    right.setOrdinalIndex(lastIndex + 0.5);
                    nodesToAdd.add(left);
                    nodesToAdd.add(right);
                }
                expressionTextFlow.getChildren().addAll(nodesToAdd);
                updateExpressionTextFlowChildren();
            });
            wrap.getItems().add(menuItem);

            menuItem = new MenuItem("brackets");
            menuItem.setOnAction((evt) -> {
                List<ExpressionTextNode> nodesToAdd = new ArrayList<>();
                double begin = -1.0;
                double lastIndex = -1.0;
                for (Node node : expressionTextFlow.getChildren()) {
                    if (node instanceof ExpressionTextNode) {
                        if (selectedNodes.contains((ExpressionTextNode) node)) {
                            if (begin == -1.0) {
                                begin = ((ExpressionTextNode) node).getOrdinalIndex();
                            }
                            lastIndex = ((ExpressionTextNode) node).getOrdinalIndex();
                        } else {
                            if (begin != -1.0 && lastIndex != -1.0) {
                                ExpressionTextNode left = new ExpressionTextNode(" [ ");
                                left.setOrdinalIndex(begin - 0.5);
                                ExpressionTextNode right = new ExpressionTextNode(" ] ");
                                right.setOrdinalIndex(lastIndex + 0.5);
                                nodesToAdd.add(left);
                                nodesToAdd.add(right);
                                begin = -1.0;
                                lastIndex = -1.0;
                            }
                        }
                    }
                }
                if (begin != -1.0 && lastIndex != -1.0) {
                    ExpressionTextNode left = new ExpressionTextNode(" [ ");
                    left.setOrdinalIndex(begin - 0.5);
                    ExpressionTextNode right = new ExpressionTextNode(" ] ");
                    right.setOrdinalIndex(lastIndex + 0.5);
                    nodesToAdd.add(left);
                    nodesToAdd.add(right);
                }
                expressionTextFlow.getChildren().addAll(nodesToAdd);
                updateExpressionTextFlowChildren();
            });
            wrap.getItems().add(menuItem);
        }

        ContextMenu contextMenu = new ContextMenu();

        if (!itemsForSelection.isEmpty()) {
            Menu menuNode = new Menu("Entity \"" + etn.getText().trim() + "\"");
            for (MenuItem mi : itemsForThisNode) {
                menuNode.getItems().add(mi);
            }
            contextMenu.getItems().add(menuNode);

            Menu menuSelection = new Menu("Selection");
            for (MenuItem mi : itemsForSelection) {
                menuSelection.getItems().add(mi);
            }
            contextMenu.getItems().add(menuSelection);
        } else {
            contextMenu.getItems().setAll(itemsForThisNode);
        }

        contextMenu.setOnHiding((WindowEvent event) -> {
            etn.popupShowing = false;
        });

        return contextMenu;
    }

    private String createPeekForTooltip(Expression ex) {
        String peek = "";
        if (ex.getExpressionTree().isSquidSwitchSCSummaryCalculation()) {
            if (ex.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()
                    || ex.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()) {
                peek += "Reference material :\n" + createPeekRM(ex) + "\n";
            }
            if (ex.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                peek += "Unknowns :\n" + createPeekUN(ex);
            }
        } else {
            if (ex.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()
                    || ex.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()) {
                String peekString = createPeekRM(ex);
                int lineNumber = 0;
                for (int n = 0; n < peekString.length(); n++) {
                    if (peekString.charAt(n) == '\n') {
                        lineNumber++;
                        if (lineNumber == 10) {
                            peekString = peekString.substring(0, n);
                            peekString += "\n...";
                            break;
                        }
                    }
                }
                peek += "Reference material :\n" + peekString + "\n";
            }
            if (ex.getExpressionTree().isSquidSwitchSAUnknownCalculation()) {
                if (ex.getExpressionTree().isSquidSwitchConcentrationReferenceMaterialCalculation()
                        || ex.getExpressionTree().isSquidSwitchSTReferenceMaterialCalculation()) {
                    peek += "\n";
                }
                String peekString = createPeekUN(ex);
                int lineNumber = 0;
                for (int n = 0; n < peekString.length(); n++) {
                    if (peekString.charAt(n) == '\n') {
                        lineNumber++;
                        if (lineNumber == 10) {
                            peekString = peekString.substring(0, n);
                            peekString += "\n...";
                            break;
                        }
                    }
                }
                peek += "Unknowns :\n" + peekString;
            }
        }
        if (!peek.equals("")) {
            peek = "Peek:\n\t" + peek.replaceAll("\n", "\n\t").trim();
        }
        return peek;
    }

    private Tooltip createFloatingTooltip(String nodeText) {
        Tooltip tooltip = tooltipsMap.get(nodeText);
        if (nodeText != null && tooltip == null) {
            String text = nodeText.replace(INVISIBLENEWLINEPLACEHOLDER, "\n");
            text = text.replace(VISIBLENEWLINEPLACEHOLDER, "\n");
            text = text.replace(INVISIBLETABPLACEHOLDER, "\t");
            text = text.replace(VISIBLETABPLACEHOLDER, "\t");
            text = text.replace(INVISIBLEWHITESPACEPLACEHOLDER, " ");
            text = text.replace(VISIBLEWHITESPACEPLACEHOLDER, " ");

            if (!text.matches("^[ \t\n\r]$")) {
                text = nodeText.replace(SUPERSCRIPT_R_FOR_REFMAT + SUPERSCRIPT_SPACE + " ", "");
                text = text.replace(SUPERSCRIPT_C_FOR_CONCREFMAT + SUPERSCRIPT_SPACE + " ", "");
                text = text.replace(SUPERSCRIPT_R_FOR_REFMAT + SUPERSCRIPT_U_FOR_UNKNOWN + " ", "");
                text = text.replace(SUPERSCRIPT_SPACE + SUPERSCRIPT_U_FOR_UNKNOWN + " ", "");
                text = text.trim();
            }

            ImageView imageView = new ImageView(UNHEALTHY);
            imageView.setFitHeight(12);
            imageView.setFitWidth(12);

            TokenTypes type = ShuntingYard.TokenTypes.getType(text);

            switch (type) {
            case OPERATOR_A:
            case OPERATOR_M:
            case OPERATOR_E:

                tooltip = new Tooltip("Operation: " + text + " (" + OPERATIONS_MAP.get(text) + ")");
                break;

            case LEFT_PAREN:
            case RIGHT_PAREN:

                tooltip = new Tooltip("Parenthesis: " + text);
                break;

            case NUMBER:

                tooltip = new Tooltip("Number: " + text);
                break;

            case NAMED_CONSTANT:

                ConstantNode constant;
                constant = (ConstantNode) task.getNamedConstantsMap().get(text);
                if (constant == null) {
                    constant = (ConstantNode) task.getNamedParametersMap().get(text);
                }
                if (constant != null) {
                    tooltip = new Tooltip(
                            "Named constant: " + constant.getName() + "\n\nValue: " + constant.getValue());
                }
                break;

            case FUNCTION:

                String str = FUNCTIONS_MAP.get(text);
                if (str != null) {
                    OperationOrFunctionInterface fn = Function.operationFactory(str);
                    if (fn != null) {
                        tooltip = new Tooltip("Function: " + fn.getName() + "\n\n" + fn.getArgumentCount()
                                + " argument(s): " + fn.printInputValues().trim() + "\nOutputs: "
                                + fn.printOutputValues().trim() + "\nDefinition: " + fn.getDefinition().trim());
                    }
                }
                break;

            case COMMA:

                tooltip = new Tooltip("Comma: " + text);
                break;

            case FORMATTER:

                String tooltipText = "Presentation node: ";
                switch (text) {
                case "\t":
                    tooltipText += "tab";
                    break;
                case "\n":
                    tooltipText += "new line";
                    break;
                case " ":
                    tooltipText += "space";
                    break;
                default:
                    tooltipText += "unknown";
                }
                tooltip = new Tooltip(tooltipText);
                break;

            case NAMED_EXPRESSION_INDEXED:

                text = text.replaceAll("\\[\\d\\]( )*$", "");

            case NAMED_EXPRESSION:
                String exname = text;
                String uncertainty = "";
                if (text.matches("^\\[(?)(%?)\"(.*?)\"\\]( )*$")) {
                    exname = text.replaceAll("(^\\[(%)?()?\")|(\"\\]( )*$)", "");
                    if (text.contains("[%\"")) {
                        uncertainty = "1 \u03C3 % uncertainty\n\n";
                    } else if (text.contains("[\"")) {
                        uncertainty = "1 \u03C3  uncertainty\n\n";
                    }
                }
                // let's see if we have an array reference in the form of SUMMARY named_expression00
                // this would be hard to catch with regex since ratios fit the pattern too
                if (exname.length() > 2) {
                    String lastTwo = exname.substring(exname.length() - 2);
                    if (ShuntingYard.isNumber(lastTwo)) {
                        // index = first digit minus 1 (converting from vertical 1-based excel to horiz 0-based java
                        int index = Integer.parseInt(lastTwo.substring(0, 1)) - 1;
                        String baseExpressionName = exname.substring(0, exname.length() - 2);
                        if (index >= 0) {
                            ExpressionTreeInterface retExpTreeKnown = task.getNamedExpressionsMap()
                                    .get(baseExpressionName);
                            if (retExpTreeKnown != null) {
                                exname = baseExpressionName;
                            }
                        }
                    }
                }

                Expression ex = task.getExpressionByName(exname);
                if (ex == null && text.matches("^\\.*\\d\\d$")) {
                    exname = text.replaceAll("\\d\\d$", "");
                    ex = task.getExpressionByName(exname);
                }
                //case expression
                if (ex != null) {
                    boolean isCustom = ex.isCustom();
                    ExpressionTreeInterface expTree = ex.getExpressionTree();
                    tooltip = new Tooltip((isCustom ? "Custom expression: " : "Expression: ") + "  " + ex.getName()
                            + "\n  Targets: "
                            + (expTree.isSquidSwitchConcentrationReferenceMaterialCalculation() ? "C" : "")
                            + (expTree.isSquidSwitchSTReferenceMaterialCalculation() ? "R" : "")
                            + (expTree.isSquidSwitchSAUnknownCalculation() ? "U" : "") + "    Type: "
                            + (expTree.isSquidSwitchSCSummaryCalculation() ? "Summary " : "")
                            + (ex.isSquidSwitchNU() ? "NU-switched " : "")
                            + (expTree.isSquidSpecialUPbThExpression() ? "Built-In" : "")
                            + "\n\nExpression string: " + ex.getExcelExpressionString() + "\n" + uncertainty
                            + (ex.amHealthy() ? createPeekForTooltip(ex) : customizeExpressionTreeAudit(ex).trim())
                            + "\nNotes:\n" + (ex.getNotes().equals("") ? "none" : ex.getNotes()));
                    if (!ex.amHealthy()) {
                        tooltip.setGraphic(imageView);
                    }
                }
                //case ratio
                if (tooltip == null) {
                    for (SquidRatiosModel r : task.getSquidRatiosModelList()) {
                        if (exname.equalsIgnoreCase(r.getRatioName())) {
                            Expression exp = new Expression(task.getNamedExpressionsMap().get(r.getRatioName()),
                                    "[\"" + r.getRatioName() + "\"]", false, false, false);
                            exp.getExpressionTree().setSquidSpecialUPbThExpression(true);
                            exp.getExpressionTree().setSquidSwitchSTReferenceMaterialCalculation(true);
                            exp.getExpressionTree().setSquidSwitchSAUnknownCalculation(true);
                            tooltip = new Tooltip(("Ratio: " + r.getRatioName() + "\n\n" + uncertainty)
                                    + createPeekForTooltip(exp));
                            break;
                        }
                    }
                }
                //case constant
                if (tooltip == null) {
                    constant = (ConstantNode) task.getNamedConstantsMap().get(text);
                    if (constant == null) {
                        constant = (ConstantNode) task.getNamedParametersMap().get(text);
                    }
                    if (constant != null) {
                        tooltip = new Tooltip(
                                "Named constant: " + constant.getName() + "\n\nValue: " + constant.getValue());
                    }
                }

                //case SpotLookupField
                if (tooltip == null) {
                    ExpressionTreeInterface spotLookupField = task.getNamedSpotLookupFieldsMap().get(text);
                    if (spotLookupField != null) {
                        tooltip = new Tooltip("Available lookup field for spots: " + spotLookupField.getName());
                    }
                }

                if (tooltip == null && text.equals(NUMBERSTRING)) {
                    tooltip = new Tooltip("Placeholder for number: " + NUMBERSTRING);
                }

                // case of mass labels or isotopes
                if (tooltip == null) {
                    if (task.getNominalMasses().contains(exname)) {
                        tooltip = new Tooltip("Mass label: " + exname);
                    }
                }

                if (tooltip == null) {
                    tooltip = new Tooltip("Missing expression: " + exname);
                    tooltip.setGraphic(imageView);
                }

                break;

            }
            if (tooltip == null) {
                tooltip = new Tooltip("Unrecognized node: " + text);
                tooltip.setGraphic(imageView);
            }
            tooltip.setStyle(EXPRESSION_TOOLTIP_CSS_STYLE_SPECS);
            tooltipsMap.put(text, tooltip);
        }
        return tooltip;
    }

    private void updateExpressionTextFlowChildren() {
        // extract and sort
        List<Node> children = new ArrayList<>();
        for (Node etn : expressionTextFlow.getChildren()) {
            children.add((ExpressionTextNode) etn);
        }
        // sort
        children.sort((Node o1, Node o2) -> {
            int retVal = 0;
            if (o1 instanceof ExpressionTextNode && o2 instanceof ExpressionTextNode) {
                retVal = Double.compare(((ExpressionTextNode) o1).getOrdinalIndex(),
                        ((ExpressionTextNode) o2).getOrdinalIndex());
            }
            return retVal;
        });

        // reset ordinals to integer values
        double ordIndex = 0;
        for (Node etn : children) {
            ((ExpressionTextNode) etn).setOrdinalIndex(ordIndex);
            ordIndex++;
        }

        expressionTextFlow.getChildren().setAll(children);

        expressionString.set(makeStringFromTextFlow());

    }

    private String customizeExpressionTreeAudit(Expression exp) {
        String audit = exp.produceExpressionTreeAudit().trim();
        // add in secton reviewing health of dependent expressions
        StringBuilder depAudit = new StringBuilder().append("Dependency Audit:\n");
        List<String> dependentExpressionNames = ((Task) task).getRequiresExpressionsGraph().get(exp.getName());

        if (dependentExpressionNames == null) {
            // we may be on a new expression
            dependentExpressionNames = new ArrayList<>();
            List<ExpressionTreeInterface> children = ((ExpressionTreeBuilderInterface) exp.getExpressionTree())
                    .getChildrenET();
            for (ExpressionTreeInterface child : children) {
                String calledName = child.getName();
                if (task.getNamedExpressionsMap().containsKey(calledName) && !(child instanceof ShrimpSpeciesNode)
                        && (child.getName().compareTo("FALSE") != 0) && (child.getName().compareTo("TRUE") != 0)) {
                    if (!dependentExpressionNames.contains(calledName)) {
                        dependentExpressionNames.add(calledName);
                    }
                }
            }
        }

        if (!dependentExpressionNames.isEmpty()) {
            for (String expressionName : dependentExpressionNames) {

                depAudit.append("\t").append(expressionName).append(" : ");
                if (task.getExpressionByName(expressionName) != null) {
                    depAudit.append(task.getExpressionByName(expressionName).amHealthy() ? "" : "UN")
                            .append("HEALTHY");
                } else {
                    // constant or ratio etc
                    depAudit.append("HEALTHY");
                }
                depAudit.append("\n");

            }
        } else {
            depAudit.append("\t").append("No dependent expressions.").append("\n");
        }

        depAudit.append("\n");

        return (depAudit.toString() + audit);
    }

    public void updateEditor() {

        if (selectedExpression.isNotNull().get()) {
            Expression exp;
            if (selectedExpressionIsEditable.get()) {
                exp = makeExpression(expressionNameTextField.getText(), expressionString.get());
            } else {
                exp = selectedExpression.get();
            }

            ((Task) task).evaluateTaskExpression(exp.getExpressionTree());
            boolean localAmHealthy = exp.amHealthy() && (exp.getName().length() > 0);
            auditTextArea.setText(customizeExpressionTreeAudit(exp));
            auditPane.setTextFill(localAmHealthy ? Paint.valueOf("black") : Paint.valueOf("red"));
            auditPane.setText(localAmHealthy ? "Audit"
                    : "Audit - ALERT !!!" + ((exp.getName().length() == 0) ? "  Expression needs name" : ""));
            auditPane.setGraphic(localAmHealthy ? new ImageView(HEALTHY) : new ImageView(UNHEALTHY));
            ((ImageView) auditPane.getGraphic()).setFitHeight(16);
            ((ImageView) auditPane.getGraphic()).setFitWidth(16);
            graphExpressionTree(exp.getExpressionTree());
            populatePeeks(exp);

        } else {

            graphExpressionTree(null);
            auditTextArea.setText("");
            rmPeekTextArea.setText("");
            unPeekTextArea.setText("");
        }
    }

    /**
     * Creates a new expression from the modifications.
     *
     * @param expressionName the value of expressionName
     * @param expressionString the value of expressionString
     */
    private Expression makeExpression(String expressionName, final String expressionString) {

        Expression exp = task.generateExpressionFromRawExcelStyleText(expressionName, expressionString,
                NUSwitchCheckBox.isSelected(), selectedExpression.get().isReferenceMaterialValue(),
                selectedExpression.get().isParameterValue());

        exp.setNotes(notesTextArea.getText());

        ExpressionTreeInterface expTree = exp.getExpressionTree();

        expTree.setSquidSwitchSTReferenceMaterialCalculation(refMatSwitchCheckBox.isSelected());
        expTree.setSquidSwitchSAUnknownCalculation(unknownsSwitchCheckBox.isSelected());
        expTree.setSquidSwitchConcentrationReferenceMaterialCalculation(concRefMatSwitchCheckBox.isSelected());
        expTree.setSquidSwitchSCSummaryCalculation(summaryCalculationSwitchCheckBox.isSelected());
        expTree.setSquidSpecialUPbThExpression(specialUPbThSwitchCheckBox.isSelected());
        expTree.setUnknownsGroupSampleName(unknownGroupsComboBox.getValue());

        // to detect ratios of interest
        if (expTree instanceof BuiltInExpressionInterface) {
            ((BuiltInExpressionInterface) expTree).buildExpression();
        }

        return exp;
    }

    private Expression copySelectedExpression() {

        Expression exp = task.generateExpressionFromRawExcelStyleText(selectedExpression.get().getName(),
                selectedExpression.get().getExcelExpressionString(), selectedExpression.get().isSquidSwitchNU(),
                selectedExpression.get().isReferenceMaterialValue(), selectedExpression.get().isParameterValue());

        ExpressionTree expTreeCopy = (ExpressionTree) exp.getExpressionTree();
        ExpressionTree expTree = (ExpressionTree) selectedExpression.get().getExpressionTree();

        exp.setSquidSwitchNU(selectedExpression.get().isSquidSwitchNU());
        exp.setReferenceMaterialValue(selectedExpression.get().isReferenceMaterialValue());
        copyTreeTags(expTree, expTreeCopy);

        return exp;
    }

    private void copyTreeTags(ExpressionTreeInterface source, ExpressionTreeInterface dest) {
        dest.setSquidSwitchConcentrationReferenceMaterialCalculation(
                source.isSquidSwitchConcentrationReferenceMaterialCalculation());
        dest.setSquidSwitchSAUnknownCalculation(source.isSquidSwitchSAUnknownCalculation());
        dest.setSquidSwitchSCSummaryCalculation(source.isSquidSwitchSCSummaryCalculation());
        dest.setSquidSwitchSTReferenceMaterialCalculation(source.isSquidSwitchSTReferenceMaterialCalculation());
        dest.setSquidSpecialUPbThExpression(source.isSquidSpecialUPbThExpression());
        dest.setUnknownsGroupSampleName(source.getUnknownsGroupSampleName());

    }

    private void save() {
        //Saves the newly built expression
        // remove spurious spaces from [expr]__[n] and expr  [n] ==> [][]
        String expression = expressionString.get();
        expression = expression.replaceAll("( )*\\[", "[");
        Expression exp = makeExpression(expressionNameTextField.getText(), expression);
        // March 2019 change logic
        // if the replacement expression does not change its name or
        // if this is a new expression, we need the low-impact route
        // of just recreating and re-evaluating this expression
        // otherwise with a new name we need the full expressions list re-evaluated
        // first detect if user should have used SummaryCalculation choice
        OperationOrFunctionInterface operation = ((ExpressionTreeBuilderInterface) exp.getExpressionTree())
                .getOperation();
        if ((operation != null) && (operation.isSummaryCalc() || (operation instanceof Value))
                && !summaryCalculationSwitchCheckBox.isSelected()
                && !(((ExpressionTree) exp.getExpressionTree()).getLeftET() instanceof ShrimpSpeciesNode)
                && !(((ExpressionTree) exp.getExpressionTree())
                        .getLeftET() instanceof VariableNodeForIsotopicRatios)
                && !(((ExpressionTree) exp.getExpressionTree()).getLeftET() instanceof SpotFieldNode)) {
            Alert alert = new Alert(Alert.AlertType.WARNING,
                    "Squid recommends choosing the Summary Calculation switch ... make it so?", ButtonType.YES,
                    ButtonType.NO);
            alert.setX(SquidUI.primaryStageWindow.getX() + (SquidUI.primaryStageWindow.getWidth() - 200) / 2);
            alert.setY(SquidUI.primaryStageWindow.getY() + (SquidUI.primaryStageWindow.getHeight() - 150) / 2);
            alert.showAndWait().ifPresent((t) -> {
                if (t.equals(ButtonType.YES)) {
                    exp.getExpressionTree().setSquidSwitchSCSummaryCalculation(true);
                }
            });
        }
        task.removeExpression(exp, false);
        //Removes the old expression if the name has been changed
        if (currentMode.get().equals(Mode.EDIT)
                && !exp.getName().equalsIgnoreCase(selectedExpression.get().getName())) {
            task.removeExpression(selectedExpression.get(), false);
            task.addExpression(exp, true);
        } else if (task.getMissingExpressionsByName().contains(exp.getName().toUpperCase(Locale.ENGLISH))) {
            task.addExpression(exp, true);
        } else {
            task.addExpression(exp, false);
        }

        //        //Remove if an expression already exists with the same name
        //        task.removeExpression(exp, true);
        //        //Removes the old expression if the name has been changed
        //        if (currentMode.get().equals(Mode.EDIT) && !exp.getName().equalsIgnoreCase(selectedExpression.get().getName())) {
        //            task.removeExpression(selectedExpression.get(), true);
        //        }
        //        task.addExpression(exp, false);
        //
        //update lists
        populateExpressionListViews();
        //set the new expression as edited expression
        currentMode.set(Mode.VIEW);
        selectedExpression.set(null);
        selectedExpression.set(exp);
        refreshSaved();
        //Calculate peeks
        populatePeeks(exp);

        selectInAllPanes(exp, true);

        selectedBeforeCreateOrCopy = null;
    }

    public void refreshSaved() {
        boolean saved = true;
        if (currentMode.get().equals(Mode.EDIT) && selectedExpression.isNotNull().get()) {
            if (!selectedExpression.get().getName().equals(expressionNameTextField.getText())) {
                saved = false;
            }
            if (!selectedExpression.get().getExcelExpressionString().equals(expressionString.get())) {
                saved = false;
            }
            if (selectedExpression.get().isSquidSwitchNU() != NUSwitchCheckBox.isSelected()) {
                saved = false;
            }
            if (selectedExpression.get().getExpressionTree()
                    .isSquidSwitchSTReferenceMaterialCalculation() != refMatSwitchCheckBox.isSelected()) {
                saved = false;
            }
            if (selectedExpression.get().getExpressionTree()
                    .isSquidSwitchSAUnknownCalculation() != unknownsSwitchCheckBox.isSelected()) {
                saved = false;
            }
            if (selectedExpression.get().getExpressionTree()
                    .isSquidSwitchConcentrationReferenceMaterialCalculation() != concRefMatSwitchCheckBox
                            .isSelected()) {
                saved = false;
            }
            if (selectedExpression.get().getExpressionTree()
                    .isSquidSwitchSCSummaryCalculation() != summaryCalculationSwitchCheckBox.isSelected()) {
                saved = false;
            }
            if (selectedExpression.get().getExpressionTree()
                    .isSquidSpecialUPbThExpression() != specialUPbThSwitchCheckBox.isSelected()) {
                saved = false;
            }
            if (!selectedExpression.get().getNotes().equals(notesTextArea.getText())) {
                saved = false;
            }
            if (selectedExpression.get().getExpressionTree().getUnknownsGroupSampleName()
                    .compareToIgnoreCase(unknownGroupsComboBox.getValue()) != 0) {
                saved = false;
            }
        } else if (currentMode.get().equals(Mode.CREATE)) {
            saved = false;
        }
        expressionIsSaved.set(saved);
    }

    private String makeStringFromTextFlow() {
        return makeStringFromExpressionTextNodeList(expressionTextFlow.getChildren());
    }

    private String makeStringFromExpressionTextNodeList(List<Node> list) {
        StringBuilder sb = new StringBuilder();
        for (Node node : list) {
            if (node instanceof ExpressionTextNode) {
                switch (((ExpressionTextNode) node).getText()) {
                case VISIBLENEWLINEPLACEHOLDER:
                case INVISIBLENEWLINEPLACEHOLDER:
                    sb.append("\n");
                    break;
                case VISIBLETABPLACEHOLDER:
                case INVISIBLETABPLACEHOLDER:
                    sb.append("\t");
                    break;
                case INVISIBLEWHITESPACEPLACEHOLDER:
                case VISIBLEWHITESPACEPLACEHOLDER:
                    sb.append(" ");
                    break;
                default:
                    String txt = ((ExpressionTextNode) node).getText().trim();
                    String nonLetter = "\t\n\r [](),+-*/<>=^\"";
                    if (sb.length() == 0 || nonLetter.indexOf(sb.charAt(sb.length() - 1)) != -1
                            || nonLetter.indexOf(txt.charAt(0)) != -1) {
                        sb.append(txt);
                    } else {
                        sb.append(" ").append(txt);
                    }
                    break;
                }
            }
        }
        return sb.toString();
    }

    private void makeTextFlowFromString(String string) {

        List<Node> children = new ArrayList<>();

        //The lexer separates the expression into tokens
        // updated to fix deprecations July 2018
        try {
            InputStream stream = new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8));
            ExpressionsForSquid2Lexer lexer = new ExpressionsForSquid2Lexer(
                    CharStreams.fromStream(stream, StandardCharsets.UTF_8));
            List<? extends Token> tokens = lexer.getAllTokens();

            //Creates the notes from tokens
            for (int i = 0; i < tokens.size(); i++) {
                Token token = tokens.get(i);
                String nodeText = token.getText();

                ExpressionTextNode etn;

                // Make a node of the corresponding type
                if (ShuntingYard.isNumber(nodeText) || NUMBERSTRING.equals(nodeText)) {
                    etn = new NumberTextNode(' ' + nodeText + ' ');
                } else if (listOperators.contains(nodeText)) {
                    etn = new OperationTextNode(' ' + nodeText + ' ');
                } else if (nodeText.equals("\n") || nodeText.equals("\r")) {
                    if (whiteSpaceVisible.get()) {
                        etn = new PresentationTextNode(VISIBLENEWLINEPLACEHOLDER);
                    } else {
                        etn = new PresentationTextNode(INVISIBLENEWLINEPLACEHOLDER);
                    }
                } else if (nodeText.equals("\t")) {
                    if (whiteSpaceVisible.get()) {
                        etn = new PresentationTextNode(VISIBLETABPLACEHOLDER);
                    } else {
                        etn = new PresentationTextNode(INVISIBLETABPLACEHOLDER);
                    }
                } else if (nodeText.equals(" ")) {
                    if (whiteSpaceVisible.get()) {
                        etn = new PresentationTextNode(VISIBLEWHITESPACEPLACEHOLDER);
                    } else {
                        etn = new PresentationTextNode(INVISIBLEWHITESPACEPLACEHOLDER);
                    }
                } else {
                    etn = new ExpressionTextNode(' ' + nodeText + ' ');
                }

                etn.setOrdinalIndex(i);
                children.add(etn);
            }
        } catch (IOException iOException) {
        }
        expressionTextFlow.getChildren().setAll(children);
    }

    private void insertFunctionIntoExpressionTextFlow(String content, double ordinalIndex) {
        //Add spaces in order to split
        content = content.replaceAll("([(,)])", " $1 ");
        String[] funcCall = content.split(" ");

        for (int i = 0; i < funcCall.length; i++) {
            if (funcCall[i].compareTo("") != 0) {
                //Add spaces
                funcCall[i] = funcCall[i].replaceAll("([(,)])", " $1 ");
                ExpressionTextNode expressionTextNode = new ExpressionTextNode(funcCall[i]);
                expressionTextNode.setOrdinalIndex(ordinalIndex - (9 - i) * 0.01);
                expressionTextFlow.getChildren().add(expressionTextNode);
            }
        }
        updateExpressionTextFlowChildren();
    }

    private void insertOperationIntoExpressionTextFlow(String content, double ordinalIndex) {
        //Add spaces
        ExpressionTextNode exp = new OperationTextNode(' ' + content.trim() + ' ');
        exp.setOrdinalIndex(ordinalIndex);
        expressionTextFlow.getChildren().add(exp);
        updateExpressionTextFlowChildren();
    }

    private void insertNumberIntoExpressionTextFlow(String content, double ordinalIndex) {
        //Add spaces
        ExpressionTextNode exp = new NumberTextNode(' ' + content.trim() + ' ');
        exp.setOrdinalIndex(ordinalIndex);
        expressionTextFlow.getChildren().add(exp);
        updateExpressionTextFlowChildren();
    }

    private void insertExpressionIntoExpressionTextFlow(String content, double ordinalIndex) {
        //Add spaces
        ExpressionTextNode exp = new ExpressionTextNode(' ' + content.trim() + ' ');
        exp.setOrdinalIndex(ordinalIndex);
        expressionTextFlow.getChildren().add(exp);
        updateExpressionTextFlowChildren();
    }

    private void insertPresentationIntoExpressionTextFlow(String content, double ordinalIndex) {
        ExpressionTextNode exp = new PresentationTextNode(content);
        exp.setOrdinalIndex(ordinalIndex);
        expressionTextFlow.getChildren().add(exp);
        updateExpressionTextFlowChildren();
    }

    private void wrapInParentheses(double ordLeft, double ordRight) {
        wrap(ordLeft, ordRight, "(", ")");
    }

    private void wrapInBracketsAndQuotes(double ordLeft, double ordRight) {
        wrap(ordLeft, ordRight, "[\"", "\"]");
    }

    private void wrap(double ordLeft, double ordRight, String stringLeft, String stringRight) {
        //Insert strings before ordLeft and after ordRight
        ExpressionTextNode left = new ExpressionTextNode(" " + stringLeft + " ");
        left.setOrdinalIndex(ordLeft - 0.5);
        ExpressionTextNode right = new ExpressionTextNode(" " + stringRight + " ");
        right.setOrdinalIndex(ordRight + 0.5);
        expressionTextFlow.getChildren().addAll(left, right);
        updateExpressionTextFlowChildren();
    }

    private void saveUndo(String v) {
        undoListForExpression.add(v);
        redoListForExpression.clear();
    }

    private void resetDragSources() {
        dragOperationOrFunctionSource.set(null);
        dragNumberSource.set(null);
        dragPresentationSource.set(null);
    }

    private void graphExpressionTree(ExpressionTreeInterface expTree) {

        String contentLocalGraphingOff = "<html>\n" + "<h3> &nbsp; </h3>\n"
                + "<h3 style=\"text-align:center;\">Local graphing is off.</h3>\n" + "</html>";

        String contentNoExpression = "";

        String graphContents;
        //Decides the content to show
        if (expTree != null) {
            graphContents = ExpressionTreeWriterMathML.toStringBuilderMathML(expTree).toString();
        } else {
            graphContents = contentNoExpression;
        }

        //Show in the software?
        if (showGraphCheckBox.isSelected()) {
            graphView.getEngine().loadContent(graphContents);
        } else {
            graphView.getEngine().loadContent(contentLocalGraphingOff);
        }

        //Show in the browser?
        if (graphBrowserCheckBox.isSelected()) {
            try {
                Files.write(Paths.get("EXPRESSION.HTML"), graphContents.getBytes());
                BrowserControl.showURI("EXPRESSION.HTML");

            } catch (IOException iOException) {
            }
        }

    }

    private class ExpressionCellFactory implements Callback<ListView<Expression>, ListCell<Expression>> {

        private final boolean showImage;

        public ExpressionCellFactory() {
            showImage = false;
        }

        public ExpressionCellFactory(boolean showImage) {
            this.showImage = showImage;
        }

        @Override
        public ListCell<Expression> call(ListView<Expression> param) {
            ListCell<Expression> cell = new ListCell<Expression>() {

                @Override
                public void updateItem(Expression expression, boolean empty) {
                    super.updateItem(expression, empty);
                    if (empty) {
                        setText(null);
                        setGraphic(null);
                    } else {
                        String mainText = expression.buildShortSignatureString();
                        String postPend = (expression.isParameterValue() || (expression.isReferenceMaterialValue()))
                                ? " (see Notes)"
                                : "";
                        setText(mainText + postPend);
                        if (showImage) {
                            ImageView imageView;
                            if (expression.amHealthy()) {
                                imageView = new ImageView(HEALTHY);

                            } else {
                                imageView = new ImageView(UNHEALTHY);
                            }
                            imageView.setFitHeight(12);
                            imageView.setFitWidth(12);
                            setGraphic(imageView);
                        }
                        Tooltip toolTip = createFloatingTooltip("[\"" + mainText + "\"]");
                        setOnMouseEntered((event) -> {
                            showToolTip(event, this, toolTip);
                        });
                        setOnMouseExited((event) -> {
                            hideToolTip(toolTip, this);
                        });
                        setOnMouseMoved((event) -> {
                            showToolTip(event, this, toolTip);
                        });
                    }
                }
            };

            updateCellMode(currentMode.get(), cell);

            currentMode.addListener((observable, oldValue, newValue) -> {
                updateCellMode(newValue, cell);
            });

            expressionsAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
                //Reupdates the cell mode when changing pane because sometimes they are not in the right mode
                updateCellMode(currentMode.get(), cell);
            });

            return cell;
        }

        private void showToolTip(MouseEvent event, ListCell<Expression> cell, Tooltip toolTip) {
            if (toolTip != null) {
                if (keyMap.get(KeyCode.T)) {
                    toolTip.show(cell, event.getScreenX() + 10, event.getScreenY() + 10);
                } else {
                    hideToolTip(toolTip, cell);
                }
            }
        }

        private void hideToolTip(Tooltip t, ListCell<Expression> cell) {
            if (t != null) {
                t.hide();
            }
            switch (currentMode.get()) {
            case VIEW:
                cell.setCursor(Cursor.HAND);
                break;
            case CREATE:
            case EDIT:
                cell.setCursor(Cursor.OPEN_HAND);
            }
        }

        private void updateCellMode(Mode mode, ListCell<Expression> cell) {
            switch (mode) {
            case VIEW:
                setCellModeView(cell);
                break;
            case CREATE:
            case EDIT:
                setCellModeEditCreate(cell);
            }
        }

        private void setCellModeView(ListCell<Expression> cell) {
            cell.setOnDragDetected((event) -> {
                //Nothing
            });
            cell.setOnDragDone((event) -> {
                //Nothing
            });
            cell.setOnMousePressed((event) -> {
                //Nothing
            });
            cell.setOnMouseReleased((event) -> {
                //Nothing
            });
            cell.setCursor(Cursor.HAND);

            ContextMenu cm = new ContextMenu();

            cell.setOnMouseClicked((event) -> {
                if (event.getButton().equals(MouseButton.SECONDARY)) {
                    cm.getItems().clear();

                    MenuItem remove = new MenuItem("Remove expression");
                    remove.setOnAction((t) -> {
                        int index = cell.getIndex();
                        ListView parent = cell.getListView();
                        removedExpressions.add(cell.getItem());
                        task.removeExpression(cell.getItem(), true);
                        selectedExpression.set(null);
                        populateExpressionListViews();

                        //Determines the new expression to select
                        int size = parent.getItems().size();
                        if (size <= index) {
                            index = size - 1;
                        }
                        if (index >= 0) {
                            selectInAllPanes((Expression) parent.getItems().get(index), false);
                        } else {
                            if (namedExpressions.size() > 0) {
                                selectInAllPanes(namedExpressions.get(0), true);
                            }
                        }
                    });
                    cm.getItems().add(remove);

                    MenuItem restore = new MenuItem("Restore removed expressions");
                    restore.setOnAction((t) -> {
                        for (Expression removedExp : removedExpressions) {
                            boolean nameExist;
                            do {
                                nameExist = false;
                                for (Expression e : namedExpressions) {
                                    if (e.getName().equalsIgnoreCase(removedExp.getName())) {
                                        removedExp.setName(removedExp.getName() + " [restored]");
                                        nameExist = true;
                                    }
                                }
                            } while (nameExist);

                            task.addExpression(removedExp, true);
                        }
                        removedExpressions.clear();
                        populateExpressionListViews();
                    });
                    restore.setDisable(removedExpressions.isEmpty());
                    cm.getItems().add(restore);

                    cm.getItems().add(new SeparatorMenuItem());

                    Menu export = new Menu("Export as");

                    MenuItem exportXML = new MenuItem("XML document");
                    exportXML.setOnAction((t) -> {
                        try {
                            FileHandler.saveExpressionFileXML(cell.getItem(), SquidUI.primaryStageWindow);
                        } catch (IOException ex) {
                        }
                    });
                    export.getItems().add(exportXML);

                    MenuItem exportHTML = new MenuItem("HTML document");
                    exportHTML.setOnAction((t) -> {
                        try {
                            FileHandler.saveExpressionGraphHTML(cell.getItem(), SquidUI.primaryStageWindow);
                        } catch (IOException ex) {
                        }
                    });
                    export.getItems().add(exportHTML);

                    cm.getItems().add(export);

                    cm.show(cell, event.getScreenX(), event.getScreenY());
                }
            });

        }

        private void setCellModeEditCreate(ListCell<Expression> cell) {
            cell.setOnDragDetected(event -> {
                if (!cell.isEmpty()) {
                    Dragboard db = cell.startDragAndDrop(TransferMode.COPY);
                    db.setDragView(new Image(SQUID_LOGO_SANS_TEXT_URL, 32, 32, true, true));
                    ClipboardContent cc = new ClipboardContent();
                    // detect if brackets and quotes needed
                    if (cell.getItem().getName().matches("^[a-zA-Z0-9_$]*$")) {
                        cc.putString(cell.getItem().getName());
                    } else {
                        cc.putString("[\"" + cell.getItem().getName() + "\"]");
                    }
                    db.setContent(cc);
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnDragDone((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setOnMousePressed((event) -> {
                if (!cell.isEmpty()) {
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnMouseReleased((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setCursor(Cursor.OPEN_HAND);

            cell.setOnMouseClicked((event) -> {
                //Nothing
            });
        }

    }

    private class SquidRatiosModelCellFactory
            implements Callback<ListView<SquidRatiosModel>, ListCell<SquidRatiosModel>> {

        @Override
        public ListCell<SquidRatiosModel> call(ListView<SquidRatiosModel> param) {
            ListCell<SquidRatiosModel> cell = new ListCell<SquidRatiosModel>() {
                @Override
                public void updateItem(SquidRatiosModel expression, boolean empty) {

                    super.updateItem(expression, empty);
                    if (empty) {
                        setText(null);
                        setGraphic(null);
                    } else {
                        setText(expression.getRatioName());
                        Tooltip t = createFloatingTooltip("[\"" + getText() + "\"]");
                        setOnMouseEntered((event) -> {
                            showToolTip(event, this, t);
                        });
                        setOnMouseExited((event) -> {
                            hideToolTip(t, this);
                        });
                        setOnMouseMoved((event) -> {
                            showToolTip(event, this, t);
                        });
                    }
                }
            };

            updateCellMode(currentMode.get(), cell);

            currentMode.addListener((observable, oldValue, newValue) -> {
                updateCellMode(newValue, cell);
            });

            expressionsAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
                updateCellMode(currentMode.get(), cell);
            });

            return cell;
        }

        private void showToolTip(MouseEvent event, ListCell<SquidRatiosModel> cell, Tooltip t) {
            if (t != null) {
                if (keyMap.get(KeyCode.T)) {
                    t.show(cell, event.getScreenX() + 10, event.getScreenY() + 10);
                } else {
                    hideToolTip(t, cell);
                }
            }
        }

        private void hideToolTip(Tooltip t, ListCell<SquidRatiosModel> cell) {
            if (t != null) {
                t.hide();
            }
            switch (currentMode.get()) {
            case VIEW:
                cell.setCursor(Cursor.HAND);
                break;
            case CREATE:
            case EDIT:
                cell.setCursor(Cursor.OPEN_HAND);
            }
        }

        private void updateCellMode(Mode mode, ListCell<SquidRatiosModel> cell) {
            switch (mode) {
            case VIEW:
                setCellModeView(cell);
                break;
            case CREATE:
            case EDIT:
                setCellModeEditCreate(cell);
            }
        }

        private void setCellModeView(ListCell<SquidRatiosModel> cell) {
            cell.setOnDragDetected(event -> {
                //Nothing
            });

            cell.setOnDragDone((event) -> {
                //Nothing
            });

            cell.setOnMousePressed((event) -> {
                //Nothing
            });

            cell.setOnMouseReleased((event) -> {
                //Nothing
            });

            cell.setCursor(Cursor.DEFAULT);
        }

        private void setCellModeEditCreate(ListCell<SquidRatiosModel> cell) {
            cell.setOnDragDetected(event -> {
                if (!cell.isEmpty()) {
                    Dragboard db = cell.startDragAndDrop(TransferMode.COPY);
                    db.setDragView(new Image(SQUID_LOGO_SANS_TEXT_URL, 32, 32, true, true));
                    ClipboardContent cc = new ClipboardContent();
                    cc.putString("[\"" + cell.getItem().getRatioName() + "\"]");
                    db.setContent(cc);
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnDragDone((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setOnMousePressed((event) -> {
                if (!cell.isEmpty()) {
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnMouseReleased((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setCursor(Cursor.OPEN_HAND);
        }

    }

    private class SquidSpeciesModelCellFactory
            implements Callback<ListView<SquidSpeciesModel>, ListCell<SquidSpeciesModel>> {

        @Override
        public ListCell<SquidSpeciesModel> call(ListView<SquidSpeciesModel> param) {
            ListCell<SquidSpeciesModel> cell = new ListCell<SquidSpeciesModel>() {
                @Override
                public void updateItem(SquidSpeciesModel expression, boolean empty) {

                    super.updateItem(expression, empty);
                    if (empty) {
                        setText(null);
                        setGraphic(null);
                    } else {
                        setText(expression.getIsotopeName());
                        Tooltip t = createFloatingTooltip("[\"" + getText() + "\"]");
                        setOnMouseEntered((event) -> {
                            showToolTip(event, this, t);
                        });
                        setOnMouseExited((event) -> {
                            hideToolTip(t, this);
                        });
                        setOnMouseMoved((event) -> {
                            showToolTip(event, this, t);
                        });
                    }
                }
            };

            updateCellMode(currentMode.get(), cell);

            currentMode.addListener((observable, oldValue, newValue) -> {
                updateCellMode(newValue, cell);
            });

            expressionsAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
                updateCellMode(currentMode.get(), cell);
            });

            return cell;
        }

        private void showToolTip(MouseEvent event, ListCell<SquidSpeciesModel> cell, Tooltip t) {
            if (t != null) {
                if (keyMap.get(KeyCode.T)) {
                    t.show(cell, event.getScreenX() + 10, event.getScreenY() + 10);
                } else {
                    hideToolTip(t, cell);
                }
            }
        }

        private void hideToolTip(Tooltip t, ListCell<SquidSpeciesModel> cell) {
            if (t != null) {
                t.hide();
            }
            switch (currentMode.get()) {
            case VIEW:
                cell.setCursor(Cursor.HAND);
                break;
            case CREATE:
            case EDIT:
                cell.setCursor(Cursor.OPEN_HAND);
            }
        }

        private void updateCellMode(Mode mode, ListCell<SquidSpeciesModel> cell) {
            switch (mode) {
            case VIEW:
                setCellModeView(cell);
                break;
            case CREATE:
            case EDIT:
                setCellModeEditCreate(cell);
            }
        }

        private void setCellModeView(ListCell<SquidSpeciesModel> cell) {
            cell.setOnDragDetected(event -> {
                //Nothing
            });

            cell.setOnDragDone((event) -> {
                //Nothing
            });

            cell.setOnMousePressed((event) -> {
                //Nothing
            });

            cell.setOnMouseReleased((event) -> {
                //Nothing
            });

            cell.setCursor(Cursor.DEFAULT);
        }

        private void setCellModeEditCreate(ListCell<SquidSpeciesModel> cell) {
            cell.setOnDragDetected(event -> {
                if (!cell.isEmpty()) {
                    Dragboard db = cell.startDragAndDrop(TransferMode.COPY);
                    db.setDragView(new Image(SQUID_LOGO_SANS_TEXT_URL, 32, 32, true, true));
                    ClipboardContent cc = new ClipboardContent();
                    cc.putString("TotalCPS([\"" + cell.getItem().getIsotopeName() + "\"])");
                    db.setContent(cc);
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnDragDone((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setOnMousePressed((event) -> {
                if (!cell.isEmpty()) {
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnMouseReleased((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setCursor(Cursor.OPEN_HAND);
        }

    } // end SquidSpeciesModelCellFactory

    private class ExpressionTreeCellFactory
            implements Callback<ListView<ExpressionTreeInterface>, ListCell<ExpressionTreeInterface>> {

        @Override
        public ListCell<ExpressionTreeInterface> call(ListView<ExpressionTreeInterface> param) {
            ListCell<ExpressionTreeInterface> cell = new ListCell<ExpressionTreeInterface>() {
                @Override
                public void updateItem(ExpressionTreeInterface expression, boolean empty) {

                    super.updateItem(expression, empty);
                    if (empty) {
                        setText(null);
                        setGraphic(null);
                    } else {
                        setText(expression.getName());
                        Tooltip t = createFloatingTooltip(getText());
                        setOnMouseEntered((event) -> {
                            showToolTip(event, this, t);
                        });
                        setOnMouseExited((event) -> {
                            hideToolTip(t, this);
                        });
                        setOnMouseMoved((event) -> {
                            showToolTip(event, this, t);
                        });
                    }
                }
            };

            updateCellMode(currentMode.get(), cell);

            currentMode.addListener((observable, oldValue, newValue) -> {
                updateCellMode(newValue, cell);
            });

            expressionsAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
                updateCellMode(currentMode.get(), cell);
            });

            return cell;
        }

        private void showToolTip(MouseEvent event, ListCell<ExpressionTreeInterface> cell, Tooltip t) {
            if (t != null) {
                if (keyMap.get(KeyCode.T)) {
                    t.show(cell, event.getScreenX() + 10, event.getScreenY() + 10);
                } else {
                    hideToolTip(t, cell);
                }
            }
        }

        private void hideToolTip(Tooltip t, ListCell<ExpressionTreeInterface> cell) {
            if (t != null) {
                t.hide();
            }
            switch (currentMode.get()) {
            case VIEW:
                cell.setCursor(Cursor.HAND);
                break;
            case CREATE:
            case EDIT:
                cell.setCursor(Cursor.OPEN_HAND);
            }
        }

        private void updateCellMode(Mode mode, ListCell<ExpressionTreeInterface> cell) {
            switch (mode) {
            case VIEW:
                setCellModeView(cell);
                break;
            case CREATE:
            case EDIT:
                setCellModeEditCreate(cell);
            }
        }

        private void setCellModeView(ListCell<ExpressionTreeInterface> cell) {
            cell.setOnDragDetected(event -> {
                //Nothing
            });

            cell.setOnDragDone((event) -> {
                //Nothing
            });

            cell.setOnMousePressed((event) -> {
                //Nothing
            });

            cell.setOnMouseReleased((event) -> {
                //Nothing
            });

            cell.setCursor(Cursor.DEFAULT);
        }

        private void setCellModeEditCreate(ListCell<ExpressionTreeInterface> cell) {
            cell.setOnDragDetected(event -> {
                if (!cell.isEmpty()) {
                    Dragboard db = cell.startDragAndDrop(TransferMode.COPY);
                    db.setDragView(new Image(SQUID_LOGO_SANS_TEXT_URL, 32, 32, true, true));
                    ClipboardContent cc = new ClipboardContent();
                    cc.putString(cell.getItem().getName());
                    db.setContent(cc);
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnDragDone((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setOnMousePressed((event) -> {
                if (!cell.isEmpty()) {
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnMouseReleased((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setCursor(Cursor.OPEN_HAND);
        }

    } // end SquidSpeciesModelCellFactory

    private class StringCellFactory implements Callback<ListView<String>, ListCell<String>> {

        private final ObjectProperty<String> dragSource;

        public StringCellFactory(ObjectProperty<String> dragSource) {
            this.dragSource = dragSource;
        }

        @Override
        public ListCell<String> call(ListView<String> param) {
            ListCell<String> cell = new ListCell<String>() {

                @Override
                public void updateItem(String operationOrFunction, boolean empty) {
                    super.updateItem(operationOrFunction, empty);
                    if (empty) {
                        setText(null);
                        setGraphic(null);
                    } else {
                        setText(operationOrFunction);
                        Tooltip t = createFloatingTooltip(getText().replaceAll("(:.*|\\(.*\\))$", "").trim()
                                .replaceAll("Tab", VISIBLETABPLACEHOLDER)
                                .replaceAll("New line", VISIBLENEWLINEPLACEHOLDER)
                                .replaceAll("White space", VISIBLEWHITESPACEPLACEHOLDER));
                        setOnMouseEntered((event) -> {
                            showToolTip(event, this, t);
                        });
                        setOnMouseExited((event) -> {
                            hideToolTip(t, this);
                        });
                        setOnMouseMoved((event) -> {
                            showToolTip(event, this, t);
                        });
                    }
                }
            };

            updateCellMode(currentMode.get(), cell);

            currentMode.addListener((observable, oldValue, newValue) -> {
                updateCellMode(newValue, cell);
            });

            expressionsAccordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
                updateCellMode(currentMode.get(), cell);
            });

            return cell;
        }

        private void showToolTip(MouseEvent event, ListCell<String> cell, Tooltip t) {
            if (t != null) {
                if (keyMap.get(KeyCode.T)) {
                    t.show(cell, event.getScreenX() + 10, event.getScreenY() + 10);
                } else {
                    hideToolTip(t, cell);
                }
            }
        }

        private void hideToolTip(Tooltip t, ListCell<String> cell) {
            if (t != null) {
                t.hide();
            }
            switch (currentMode.get()) {
            case VIEW:
                cell.setCursor(Cursor.HAND);
                break;
            case CREATE:
            case EDIT:
                cell.setCursor(Cursor.OPEN_HAND);
            }
        }

        private void updateCellMode(Mode mode, ListCell<String> cell) {
            switch (mode) {
            case VIEW:
                setCellModeView(cell);
                break;
            case CREATE:
            case EDIT:
                setCellModeEditCreate(cell);
            }
        }

        private void setCellModeView(ListCell<String> cell) {
            cell.setOnDragDetected(event -> {
                //Nothing
            });

            cell.setOnDragDone((event) -> {
                //Nothing
            });

            cell.setOnMousePressed((event) -> {
                //Nothing
            });

            cell.setOnMouseReleased((event) -> {
                //Nothing
            });

            cell.setCursor(Cursor.DEFAULT);
        }

        private void setCellModeEditCreate(ListCell<String> cell) {
            cell.setOnDragDetected(event -> {
                if (!cell.isEmpty()) {
                    Dragboard db = cell.startDragAndDrop(TransferMode.COPY);
                    db.setDragView(new Image(SQUID_LOGO_SANS_TEXT_URL, 32, 32, true, true));
                    ClipboardContent cc = new ClipboardContent();
                    cc.putString(cell.getItem());
                    db.setContent(cc);
                    dragSource.set(cell.getItem());
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnDragDone((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
                resetDragSources();
            });

            cell.setOnMousePressed((event) -> {
                if (!cell.isEmpty()) {
                    cell.setCursor(Cursor.CLOSED_HAND);
                }
            });

            cell.setOnMouseReleased((event) -> {
                cell.setCursor(Cursor.OPEN_HAND);
            });

            cell.setCursor(Cursor.OPEN_HAND);
        }
    }

    private class PresentationTextNode extends ExpressionTextNode {

        public PresentationTextNode(String text) {
            super(text);
            this.fontSize = EXPRESSION_BUILDER_DEFAULT_FONTSIZE + 3;
            updateFontSize();
            if (text.equals(INVISIBLEWHITESPACEPLACEHOLDER) || text.equals(VISIBLEWHITESPACEPLACEHOLDER)) {
                this.isWhiteSpace = true;
                whiteSpaceVisible.addListener((observable, oldValue, newValue) -> {
                    if (newValue != null) {
                        if (newValue) {
                            setText(VISIBLEWHITESPACEPLACEHOLDER);
                        } else {
                            setText(INVISIBLEWHITESPACEPLACEHOLDER);
                        }
                        updateMode(currentMode.get());
                    }
                });
            }
            if (text.equals(INVISIBLENEWLINEPLACEHOLDER) || text.equals(VISIBLENEWLINEPLACEHOLDER)) {
                this.isWhiteSpace = true;
                whiteSpaceVisible.addListener((observable, oldValue, newValue) -> {
                    if (newValue != null) {
                        if (newValue) {
                            setText(VISIBLENEWLINEPLACEHOLDER);
                        } else {
                            setText(INVISIBLENEWLINEPLACEHOLDER);
                        }
                        updateMode(currentMode.get());
                    }
                });
            }
            if (text.equals(INVISIBLETABPLACEHOLDER) || text.equals(VISIBLETABPLACEHOLDER)) {
                this.isWhiteSpace = true;
                whiteSpaceVisible.addListener((observable, oldValue, newValue) -> {
                    if (newValue != null) {
                        if (newValue) {
                            setText(VISIBLETABPLACEHOLDER);
                        } else {
                            setText(INVISIBLETABPLACEHOLDER);
                        }
                        updateMode(currentMode.get());
                    }
                });
            }
            updateMode(currentMode.get());
        }
    }

    private class OperationTextNode extends ExpressionTextNode {

        public OperationTextNode(String text) {
            super(text);
            this.regularColor = Color.GREEN;
            setFill(regularColor);
            this.fontSize = EXPRESSION_BUILDER_DEFAULT_FONTSIZE + 3;
            updateFontSize();
        }
    }

    // this node signals user can edit in context menu
    private class NumberTextNode extends ExpressionTextNode {

        public NumberTextNode(String text) {
            super(text);
        }

    }

    private void initNodeSelection() {
        selectedNodes.addListener((Change<? extends ExpressionTextNode> c) -> {
            while (c.next()) {
                for (ExpressionTextNode ex : c.getAddedSubList()) {
                    ex.setSelected(true);
                }
                for (ExpressionTextNode ex : c.getRemoved()) {
                    ex.setSelected(false);
                }
            }
        });

    }

    private class ExpressionTextNode extends Text {

        private boolean selected = false;
        protected boolean isWhiteSpace;

        private final String text;
        private double ordinalIndex;
        private boolean popupShowing;

        protected Color regularColor;
        protected Color selectedColor;
        protected Color opositeColor;

        protected int fontSize;

        //Saves where was the indicator before moving it in order to be able to restore it when drag is exiting this node
        int previousIndicatorIndex = -1;

        private Tooltip tooltip;

        ExpressionTextNode oppositeParenthese = null;

        public ExpressionTextNode(String text) {
            super(text);

            setFontSmoothingType(FontSmoothingType.LCD);
            setFont(Font.font("SansSerif"));

            this.selectedColor = Color.RED;
            this.regularColor = Color.BLACK;
            this.opositeColor = Color.LIME;

            setFill(regularColor);

            this.text = text;
            this.popupShowing = false;

            this.fontSize = EXPRESSION_BUILDER_DEFAULT_FONTSIZE;
            updateFontSize();

            updateMode(currentMode.get());

            currentMode.addListener((observable, oldValue, newValue) -> {
                updateMode(newValue);
            });

            setTooltip(createFloatingTooltip(text));
        }

        private void setSelected(boolean selected) {
            if (this.selected != selected) {
                this.selected = selected;
                if (selected) {
                    if (!keyMap.get(KeyCode.CONTROL)) {
                        selectedNodes.clear();
                    } else if (keyMap.get(KeyCode.SHIFT)) {
                        boolean hasSelectedNodeBefore = false;
                        ExpressionTextNode precNode = null;
                        for (int i = 0; i < expressionTextFlow.getChildren().size(); i++) {
                            if (expressionTextFlow.getChildren().get(i) instanceof ExpressionTextNode) {
                                ExpressionTextNode nodeLoop = (ExpressionTextNode) expressionTextFlow.getChildren()
                                        .get(i);
                                if (selectedNodes.contains(nodeLoop)) {
                                    hasSelectedNodeBefore = true;
                                    precNode = null;
                                } else if (nodeLoop.equals(this)) {
                                    break;
                                } else {
                                    precNode = nodeLoop;
                                }
                            }
                        }
                        if (hasSelectedNodeBefore && precNode != null) {
                            precNode.setSelected(true);
                        }
                    }
                    if (!selectedNodes.contains(this)) {
                        selectedNodes.add(this);
                    }
                    setFill(selectedColor);
                } else {
                    if (selectedNodes.contains(this)) {
                        selectedNodes.remove(this);
                    }
                    setFill(regularColor);
                }
            }
        }

        public final void updateFontSize() {
            setFont(Font.font("SansSerif", FontWeight.SEMI_BOLD, fontSize + fontSizeModifier));
        }

        private void selectOppositeParenthese() {
            if (text.trim().matches("^[()\\[\\]]$")) {
                String current = text.trim();
                String opposite = "";
                Predicate<Double> predicate = (Double t) -> {
                    return false;
                };
                List<Node> nodes = expressionTextFlow.getChildren();
                switch (current) {
                case "[":
                    opposite = "]";
                    predicate = (Double t) -> {
                        return t > ordinalIndex;
                    };
                    break;
                case "]":
                    opposite = "[";
                    predicate = (Double t) -> {
                        return t < ordinalIndex;
                    };
                    nodes = Lists.reverse(nodes);
                    break;
                case "(":
                    opposite = ")";
                    predicate = (Double t) -> {
                        return t > ordinalIndex;
                    };
                    break;
                case ")":
                    opposite = "(";
                    predicate = (Double t) -> {
                        return t < ordinalIndex;
                    };
                    nodes = Lists.reverse(nodes);
                    break;
                }
                int cpt = 0;
                for (Node node : nodes) {
                    if (node instanceof ExpressionTextNode) {
                        ExpressionTextNode etn = (ExpressionTextNode) node;
                        if (predicate.test(etn.getOrdinalIndex())) {
                            if (etn.getText().trim().equals(opposite)) {
                                if (cpt == 0) {
                                    oppositeParenthese = etn;
                                    etn.setFill(etn.opositeColor);
                                    break;
                                } else {
                                    cpt--;
                                }
                            } else if (etn.getText().trim().equals(current)) {
                                cpt++;
                            }
                        }
                    }
                }
            }
        }

        public void resetColor() {
            if (selected) {
                setFill(selectedColor);
            } else {
                setFill(regularColor);
            }
        }

        private void unselectOppositeParenthese() {
            if (oppositeParenthese != null) {
                oppositeParenthese.resetColor();
                oppositeParenthese = null;
            }
        }

        private void setTooltip(Tooltip t) {
            tooltip = t;
            setOnMouseEntered((event) -> {
                showToolTip(event);
            });
            setOnMouseExited((event) -> {
                hideToolTip();
            });
            setOnMouseMoved((event) -> {
                showToolTip(event);
            });
        }

        private void showToolTip(MouseEvent event) {
            if (tooltip != null) {
                if (keyMap.get(KeyCode.T)) {
                    tooltip.show(this, event.getScreenX() + 10, event.getScreenY() + 10);
                } else {
                    hideToolTip();
                }
            }
        }

        private void hideToolTip() {
            if (tooltip != null) {
                tooltip.hide();
            }
        }

        protected final void updateMode(Mode mode) {
            if (isWhiteSpace && !whiteSpaceVisible.get()) {
                setNodeModeView();
            } else {
                switch (mode) {
                case VIEW:
                    setNodeModeView();
                    break;
                case CREATE:
                case EDIT:
                    setNodeModeEditCreate();
                }
            }
        }

        /**
         * @return the ordinalIndex
         */
        public double getOrdinalIndex() {
            return ordinalIndex;
        }

        /**
         * @param ordinalIndex the ordinalIndex to set
         */
        public void setOrdinalIndex(double ordinalIndex) {
            this.ordinalIndex = ordinalIndex;
        }

        private void setNodeModeEditCreate() {
            setCursor(Cursor.OPEN_HAND);

            setOnMouseClicked((MouseEvent event) -> {
                if (event.getButton() == MouseButton.SECONDARY && event.getEventType() == MouseEvent.MOUSE_CLICKED
                        && !popupShowing) {
                    if (!selected) {
                        setSelected(true);
                    }
                    createExpressionTextNodeContextMenu((ExpressionTextNode) event.getSource())
                            .show((ExpressionTextNode) event.getSource(), event.getScreenX(), event.getScreenY());
                    popupShowing = true;
                } else if (event.getButton() == MouseButton.PRIMARY
                        && event.getEventType() == MouseEvent.MOUSE_CLICKED) {
                    setSelected(!selected);
                }
            });

            setOnMousePressed((MouseEvent event) -> {
                hideToolTip();
                selectOppositeParenthese();
                setCursor(Cursor.CLOSED_HAND);
                expressionTextFlow.requestLayout();//fixes a javafx bug where the etn are sometimes not updated
            });

            setOnMouseReleased((MouseEvent event) -> {
                showToolTip(event);
                unselectOppositeParenthese();
                setCursor(Cursor.OPEN_HAND);
            });

            setOnDragDetected((MouseEvent event) -> {
                unselectOppositeParenthese();
                setCursor(Cursor.CLOSED_HAND);
                Dragboard db = startDragAndDrop(TransferMode.MOVE);
                db.setDragView(new Image(SQUID_LOGO_SANS_TEXT_URL, 32, 32, true, true));
                ClipboardContent cc = new ClipboardContent();
                cc.putString(text);
                db.setContent(cc);
            });

            setOnDragDone((event) -> {
                setCursor(Cursor.OPEN_HAND);
                expressionTextFlow.getChildren().remove(insertIndicator);
                previousIndicatorIndex = -1;
            });

            setOnDragOver((DragEvent event) -> {
                if (event.getGestureSource() != (ExpressionTextNode) event.getSource()) {
                    event.acceptTransferModes(TransferMode.COPY, TransferMode.MOVE);
                }
                event.consume();
            });

            setOnDragEntered((event) -> {
                if (expressionTextFlow.getChildren().contains(insertIndicator)) {
                    //Save previous position and remove
                    previousIndicatorIndex = expressionTextFlow.getChildren().indexOf(insertIndicator);
                    expressionTextFlow.getChildren().remove(insertIndicator);
                } else {
                    //No previous position
                    previousIndicatorIndex = -1;
                }
                if (dragndropReplaceRadio.isSelected()) {
                    setFill(selectedColor);
                } else {
                    //determines index
                    int index = expressionTextFlow.getChildren().indexOf(this);
                    if (dragndropRightRadio.isSelected()) {
                        index++;
                    }
                    //And show indicator
                    expressionTextFlow.getChildren().add(index, insertIndicator);
                }
            });

            setOnDragExited((event) -> {
                //Remove indicator
                expressionTextFlow.getChildren().remove(insertIndicator);
                if (dragndropReplaceRadio.isSelected()) {
                    setFill(regularColor);
                }
                //And restore it if there is a previous position
                if (previousIndicatorIndex != -1) {
                    expressionTextFlow.getChildren().add(previousIndicatorIndex, insertIndicator);
                }
            });

            setOnDragDropped((DragEvent event) -> {
                //reset color and indicator
                if (dragndropReplaceRadio.isSelected()) {
                    setFill(regularColor);
                }
                expressionTextFlow.getChildren().remove(insertIndicator);
                previousIndicatorIndex = -1;

                Dragboard db = event.getDragboard();
                boolean success = false;
                double ord = 0.0;

                //Chosing where to put the new node
                if (toggleGroup.getSelectedToggle() == dragndropLeftRadio) {
                    ord = ordinalIndex - 0.5;
                } else if (toggleGroup.getSelectedToggle() == dragndropReplaceRadio) {
                    ord = ordinalIndex;
                    expressionTextFlow.getChildren().remove(this);
                } else if (toggleGroup.getSelectedToggle() == dragndropRightRadio) {
                    ord = ordinalIndex + 0.5;
                }

                //If moving an existing node
                if (event.getGestureSource() instanceof ExpressionTextNode) {
                    if (selectedNodes.size() > 1) {
                        //If multiple nodes update all indexes
                        for (ExpressionTextNode etn : selectedNodes) {
                            etn.setOrdinalIndex(ord + 0.001 * etn.getOrdinalIndex());
                        }
                    } else {
                        //Else just update the index
                        ((ExpressionTextNode) event.getGestureSource()).setOrdinalIndex(ord);
                    }
                    updateExpressionTextFlowChildren();
                    success = true;
                } //If copying a node from the lists
                else if (db.hasString()) {
                    String content = db.getString().split(OPERATION_FLAG_DELIMITER)[0];
                    //Insert the right type of node
                    if ((dragOperationOrFunctionSource.get() != null)
                            && (!db.getString().contains(OPERATION_FLAG_DELIMITER))) {
                        // case of function, make a series of objects
                        insertFunctionIntoExpressionTextFlow(content, ord);
                    } else if ((dragOperationOrFunctionSource.get() != null)
                            && (db.getString().contains(OPERATION_FLAG_DELIMITER))) {
                        // case of operation
                        insertOperationIntoExpressionTextFlow(content, ord);
                    } else if ((dragNumberSource.get() != null) && content.contains(NUMBERSTRING)) {
                        // case of "NUMBER"
                        insertNumberIntoExpressionTextFlow(NUMBERSTRING, ord);
                    } else if (dragPresentationSource.get() != null
                            && presentationMap.containsKey(dragPresentationSource.get())) {
                        // case of presentation (new line, tab)
                        insertPresentationIntoExpressionTextFlow(presentationMap.get(dragPresentationSource.get()),
                                ord);
                    } else {
                        // case of expression
                        insertExpressionIntoExpressionTextFlow(content, ord);
                    }

                    success = true;
                }

                event.setDropCompleted(success);

                event.consume();
                resetDragSources();
            });
        }

        private void setNodeModeView() {
            setCursor(Cursor.DEFAULT);

            setOnMouseClicked((MouseEvent event) -> {
                //Nothing
            });

            setOnMousePressed((MouseEvent event) -> {
                hideToolTip();
                selectOppositeParenthese();
                setFill(selectedColor);
                expressionTextFlow.requestLayout();//fixes a javafx bug where the etn are sometimes not updated
            });

            setOnMouseReleased((MouseEvent event) -> {
                showToolTip(event);
                unselectOppositeParenthese();
                setFill(regularColor);
            });

            setOnDragDetected((MouseEvent event) -> {
                //Nothing
            });

            setOnDragDone((event) -> {
                //Nothing
            });

            setOnDragOver((DragEvent event) -> {
                //Nothing
            });

            setOnDragDropped((DragEvent event) -> {
                //Nothing
            });
        }
    }

}