gov.va.isaac.gui.ConceptNode.java Source code

Java tutorial

Introduction

Here is the source code for gov.va.isaac.gui.ConceptNode.java

Source

/**
 * Copyright Notice
 * 
 * This is a work of the U.S. Government and is not subject to copyright
 * protection in the United States. Foreign copyrights may apply.
 * 
 * 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 gov.va.isaac.gui;

import gov.va.isaac.AppContext;
import gov.va.isaac.gui.dragAndDrop.DragRegistry;
import gov.va.isaac.gui.dragAndDrop.SingleConceptIdProvider;
import gov.va.isaac.gui.util.CustomClipboard;
import gov.va.isaac.gui.util.Images;
import gov.va.isaac.util.CommonMenuBuilderI;
import gov.va.isaac.util.CommonMenus;
import gov.va.isaac.util.CommonMenusNIdProvider;
import gov.va.isaac.util.CommonlyUsedConcepts;
import gov.va.isaac.util.ConceptLookupCallback;
import gov.va.isaac.util.SimpleValidBooleanProperty;
import gov.va.isaac.util.OTFUtility;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.StringConverter;
import org.apache.commons.lang3.StringUtils;
import org.ihtsdo.otf.tcc.api.concept.ConceptVersionBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link ConceptNode}
 * 
 *  This class handles the GUI display of concepts with many other useful tidbits, 
 *  such as allowing users to enter UUIDs, SCTIDs, NIDS, or do type ahead searches.
 *  
 *  Validation lookups are background threaded.
 *  
 * @author <a href="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</a> 
 */
public class ConceptNode implements ConceptLookupCallback {
    private static Logger logger = LoggerFactory.getLogger(ConceptNode.class);

    private HBox hbox_;
    private ComboBox<SimpleDisplayConcept> cb_;
    private ProgressIndicator pi_;
    private ImageView lookupFailImage_;
    private ConceptVersionBI c_;
    private ObjectBinding<ConceptVersionBI> conceptBinding_;
    private SimpleDisplayConcept codeSetComboBoxConcept_ = null;
    private SimpleValidBooleanProperty isValid = new SimpleValidBooleanProperty(true, null);
    private boolean flagAsInvalidWhenBlank_ = true;
    private volatile long lookupUpdateTime_ = 0;
    private AtomicInteger lookupsCurrentlyInProgress_ = new AtomicInteger();
    private BooleanBinding isLookupInProgress_ = new BooleanBinding() {
        @Override
        protected boolean computeValue() {
            return lookupsCurrentlyInProgress_.get() > 0;
        }
    };

    private ListChangeListener<SimpleDisplayConcept> listChangeListener_;
    private volatile boolean disableChangeListener_ = false;
    private Function<ConceptVersionBI, String> descriptionReader_;
    private ObservableList<SimpleDisplayConcept> dropDownOptions_;
    private ContextMenu cm_;

    public ConceptNode(ConceptVersionBI initialConcept, boolean flagAsInvalidWhenBlank) {
        this(initialConcept, flagAsInvalidWhenBlank, null, null);
    }

    /**
     * descriptionReader is optional
     */
    public ConceptNode(ConceptVersionBI initialConcept, boolean flagAsInvalidWhenBlank,
            ObservableList<SimpleDisplayConcept> dropDownOptions,
            Function<ConceptVersionBI, String> descriptionReader) {
        c_ = initialConcept;
        //We can't simply use the ObservableList from the CommonlyUsedConcepts, because it infinite loops - there doesn't seem to be a way 
        //to change the items in the drop down without changing the selection.  So, we have this hack instead.
        listChangeListener_ = new ListChangeListener<SimpleDisplayConcept>() {
            @Override
            public void onChanged(Change<? extends SimpleDisplayConcept> c) {
                //TODO I still have an infinite loop here.  Find and fix.
                logger.debug("updating concept dropdown");
                disableChangeListener_ = true;
                SimpleDisplayConcept temp = cb_.getValue();
                cb_.setItems(FXCollections.observableArrayList(dropDownOptions_));
                cb_.setValue(temp);
                cb_.getSelectionModel().select(temp);
                disableChangeListener_ = false;
            }
        };
        descriptionReader_ = (descriptionReader == null ? (conceptVersion) -> {
            return conceptVersion == null ? "" : OTFUtility.getDescription(conceptVersion);
        } : descriptionReader);
        dropDownOptions_ = dropDownOptions == null
                ? AppContext.getService(CommonlyUsedConcepts.class).getObservableConcepts()
                : dropDownOptions;
        dropDownOptions_.addListener(new WeakListChangeListener<SimpleDisplayConcept>(listChangeListener_));
        conceptBinding_ = new ObjectBinding<ConceptVersionBI>() {
            @Override
            protected ConceptVersionBI computeValue() {
                return c_;
            }
        };

        flagAsInvalidWhenBlank_ = flagAsInvalidWhenBlank;
        cb_ = new ComboBox<>();
        cb_.setConverter(new StringConverter<SimpleDisplayConcept>() {
            @Override
            public String toString(SimpleDisplayConcept object) {
                return object == null ? "" : object.getDescription();
            }

            @Override
            public SimpleDisplayConcept fromString(String string) {
                return new SimpleDisplayConcept(string, 0);
            }
        });
        cb_.setValue(new SimpleDisplayConcept("", 0));
        cb_.setEditable(true);
        cb_.setMaxWidth(Double.MAX_VALUE);
        cb_.setPrefWidth(ComboBox.USE_COMPUTED_SIZE);
        cb_.setMinWidth(200.0);
        cb_.setPromptText("Type, drop or select a concept");

        cb_.setItems(FXCollections.observableArrayList(dropDownOptions_));
        cb_.setVisibleRowCount(11);

        cm_ = new ContextMenu();

        MenuItem copyText = new MenuItem("Copy Description");
        copyText.setGraphic(Images.COPY.createImageView());
        copyText.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                CustomClipboard.set(cb_.getEditor().getText());
            }
        });
        cm_.getItems().add(copyText);

        CommonMenusNIdProvider nidProvider = new CommonMenusNIdProvider() {
            @Override
            public Set<Integer> getNIds() {
                Set<Integer> nids = new HashSet<>();
                if (c_ != null) {
                    nids.add(c_.getNid());
                }
                return nids;
            }
        };
        CommonMenuBuilderI menuBuilder = CommonMenus.CommonMenuBuilder.newInstance();
        menuBuilder.setInvisibleWhenFalse(isValid);
        CommonMenus.addCommonMenus(cm_, menuBuilder, nidProvider);

        cb_.getEditor().setContextMenu(cm_);

        updateGUI();

        new LookAheadConceptPopup(cb_);

        if (cb_.getValue().getNid() == 0) {
            if (flagAsInvalidWhenBlank_) {
                isValid.setInvalid("Concept Required");
            }
        } else {
            isValid.setValid();
        }

        cb_.valueProperty().addListener(new ChangeListener<SimpleDisplayConcept>() {
            @Override
            public void changed(ObservableValue<? extends SimpleDisplayConcept> observable,
                    SimpleDisplayConcept oldValue, SimpleDisplayConcept newValue) {
                if (newValue == null) {
                    logger.debug("Combo Value Changed - null entry");
                } else {
                    logger.debug("Combo Value Changed: {} {}", newValue.getDescription(), newValue.getNid());
                }

                if (disableChangeListener_) {
                    logger.debug("change listener disabled");
                    return;
                }

                if (newValue == null) {
                    //This can happen if someone calls clearSelection() - it passes in a null.
                    cb_.setValue(new SimpleDisplayConcept("", 0));
                    return;
                } else {
                    if (newValue.shouldIgnoreChange()) {
                        logger.debug("One time change ignore");
                        return;
                    }
                    //Whenever the focus leaves the combo box editor, a new combo box is generated.  But, the new box will have 0 for an id.  detect and ignore
                    if (oldValue != null && oldValue.getDescription().equals(newValue.getDescription())
                            && newValue.getNid() == 0) {
                        logger.debug("Not a real change, ignore");
                        newValue.setNid(oldValue.getNid());
                        return;
                    }
                    lookup();
                }
            }
        });

        AppContext.getService(DragRegistry.class).setupDragAndDrop(cb_, new SingleConceptIdProvider() {
            @Override
            public String getConceptId() {
                return cb_.getValue().getNid() + "";
            }
        }, true);

        pi_ = new ProgressIndicator(ProgressIndicator.INDETERMINATE_PROGRESS);
        pi_.visibleProperty().bind(isLookupInProgress_);
        pi_.setPrefHeight(16.0);
        pi_.setPrefWidth(16.0);
        pi_.setMaxWidth(16.0);
        pi_.setMaxHeight(16.0);

        lookupFailImage_ = Images.EXCLAMATION.createImageView();
        lookupFailImage_.visibleProperty().bind(isValid.not().and(isLookupInProgress_.not()));
        Tooltip t = new Tooltip();
        t.textProperty().bind(isValid.getReasonWhyInvalid());
        Tooltip.install(lookupFailImage_, t);

        StackPane sp = new StackPane();
        sp.setMaxWidth(Double.MAX_VALUE);
        sp.getChildren().add(cb_);
        sp.getChildren().add(lookupFailImage_);
        sp.getChildren().add(pi_);
        StackPane.setAlignment(cb_, Pos.CENTER_LEFT);
        StackPane.setAlignment(lookupFailImage_, Pos.CENTER_RIGHT);
        StackPane.setMargin(lookupFailImage_, new Insets(0.0, 30.0, 0.0, 0.0));
        StackPane.setAlignment(pi_, Pos.CENTER_RIGHT);
        StackPane.setMargin(pi_, new Insets(0.0, 30.0, 0.0, 0.0));

        hbox_ = new HBox();
        hbox_.setSpacing(5.0);
        hbox_.setAlignment(Pos.CENTER_LEFT);

        hbox_.getChildren().add(sp);
        HBox.setHgrow(sp, Priority.SOMETIMES);
    }

    public void addMenu(MenuItem mi) {
        cm_.getItems().add(mi);
    }

    private void updateGUI() {
        logger.debug("update gui - is concept null? {}", c_ == null);
        if (c_ == null) {
            //Keep the user entry, if it was invalid, so they can edit it.
            codeSetComboBoxConcept_ = new SimpleDisplayConcept(
                    (cb_.getValue() != null ? cb_.getValue().getDescription() : ""), 0, true);
            cb_.setTooltip(null);
        } else {
            codeSetComboBoxConcept_ = new SimpleDisplayConcept(descriptionReader_.apply(c_), c_.getNid(), true);

            //In case the description is too long, also put it in a tooltip
            Tooltip t = new Tooltip(codeSetComboBoxConcept_.getDescription());
            cb_.setTooltip(t);
        }
        cb_.setValue(codeSetComboBoxConcept_);
    }

    private synchronized void lookup() {
        lookupsCurrentlyInProgress_.incrementAndGet();
        isLookupInProgress_.invalidate();
        if (cb_.getValue().getNid() != 0) {
            OTFUtility.getConceptVersion(cb_.getValue().getNid(), this, null);
        } else {
            OTFUtility.lookupIdentifier(cb_.getValue().getDescription(), this, null);
        }
    }

    public HBox getNode() {
        return hbox_;
    }

    public ConceptVersionBI getConcept() {
        if (isLookupInProgress_.get()) {
            synchronized (lookupsCurrentlyInProgress_) {
                while (lookupsCurrentlyInProgress_.get() > 0) {
                    try {
                        lookupsCurrentlyInProgress_.wait();
                    } catch (InterruptedException e) {
                        // noop
                    }
                }
            }
        }
        return c_;
    }

    public ConceptVersionBI getConceptNoWait() {
        return c_;
    }

    protected String getDisplayedText() {
        return cb_.getValue().getDescription();
    }

    protected void set(String newValue) {
        cb_.setValue(new SimpleDisplayConcept(newValue, 0));
    }

    public void set(ConceptVersionBI newValue) {
        cb_.setValue(new SimpleDisplayConcept(newValue, descriptionReader_));
    }

    public void set(SimpleDisplayConcept newValue) {
        if (newValue == null) {
            cb_.setValue(new SimpleDisplayConcept("", 0));
        } else {
            cb_.setValue(newValue);
        }
    }

    public SimpleValidBooleanProperty isValid() {
        return isValid;
    }

    public void revalidate() {
        lookup();
    }

    @Override
    public void lookupComplete(final ConceptVersionBI concept, final long submitTime, Integer callId) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                logger.debug("lookupComplete - found '{}'", (concept == null ? "-null-" : concept.toUserString()));
                synchronized (lookupsCurrentlyInProgress_) {
                    lookupsCurrentlyInProgress_.decrementAndGet();
                    isLookupInProgress_.invalidate();
                    lookupsCurrentlyInProgress_.notifyAll();
                }

                if (submitTime < lookupUpdateTime_) {
                    // Throw it away, we already got back a newer lookup.
                    logger.debug("throwing away a lookup");
                    return;
                } else {
                    lookupUpdateTime_ = submitTime;
                }

                if (concept != null) {
                    c_ = concept;
                    AppContext.getService(CommonlyUsedConcepts.class).addConcept(new SimpleDisplayConcept(c_));
                    isValid.setValid();
                } else {
                    // lookup failed
                    c_ = null;
                    if (StringUtils.isNotBlank(cb_.getValue().getDescription())) {
                        isValid.setInvalid("The specified concept was not found in the database");
                    } else if (flagAsInvalidWhenBlank_) {
                        isValid.setInvalid("Concept required");
                    } else {
                        isValid.setValid();
                    }
                }
                updateGUI();
                conceptBinding_.invalidate();
            }
        });
    }

    public void setPromptText(String promptText) {
        cb_.setPromptText(promptText);
    }

    public ObjectBinding<ConceptVersionBI> getConceptProperty() {
        return conceptBinding_;
    }

    public void clear() {
        logger.debug("Clear called");
        Runnable r = new Runnable() {
            @Override
            public void run() {
                cb_.setValue(new SimpleDisplayConcept("", 0));
            }
        };

        if (Platform.isFxApplicationThread()) {
            r.run();
        } else {
            Platform.runLater(r);
        }
    }

    /**
     * If, for some reason, you want a concept node selection box that is completely disabled - call this method after constructing
     * the concept node.  Though one wonders, why you wouldn't just use a label in this case....
     */
    public void disableEdit() {
        AppContext.getService(DragRegistry.class).removeDragCapability(cb_);
        cb_.setEditable(false);
        dropDownOptions_.removeListener(listChangeListener_);
        listChangeListener_ = null;
        cb_.setItems(FXCollections.observableArrayList());
        cb_.setBackground(new Background(new BackgroundFill(Color.LIGHTGRAY, new CornerRadii(0), new Insets(0))));
    }
}