Welcome to jMDA

To generate software automatically has been a strong ambition since the early days of software development.

jMDA is a new approach in this area. It streamlines proven, widely known and accepted open source technologies into a most comprehensible and easy to use set of Java libraries that are extremely powerful and flexible at the same time. The main purpose of jMDA is

  • to leverage a comprehensible and easy to use modelling environment,

  • to provide convenient and complete access to modelling information and

  • to make available easy to use software generator facilities.

The introduction will briefly explain the main drivers behind this project, the jMDA book provides more detailed information about the most important concepts and the open source software is available here.

Sunday 10 June 2018

javafx - combobox with autocompletion for custom types

Hi,

I'd like to share a solution for a javafx combobox with autocompletion for custom types. First of all I will show how to use it. Here is an example for a simple custom type that I will use for this example:

    private class Data implements Comparable<Data>
    {
        private String string;
        private Data(String string) { this.string = string; }
        @Override public int compareTo(Data o) { return string.compareTo(o.string); }
        @Override public String toString() { return string; }
    }

Next here comes an example for using autocompletion for the above custom type with the combobox:

        // create combo box (for custom type Data) as usual
        ComboBox<Data> cmbBx = new ComboBox<>();

        // cell factory that provides a list cell for a given Data item (see updateItem)
        Callback<ListView<Data>, ListCell<Data>> cellFactory =
                new Callback<ListView<Data>, ListCell<Data>>()
                {
                    @Override public ListCell<Data> call(ListView<Data> lv)
                    {
                        return new ListCell<Data>()
                        {
                            @Override protected void updateItem(Data item, boolean empty)
                            {
                                super.updateItem(item, empty);
                                if (item == null || empty) { setGraphic(null); }
                                else { setText(item.string); }
                            }
                        };
                    }
                };
        // converter that converts a given Data item to String and vice versa
        StringConverter<Data> converter =
                new StringConverter<Data>()
                {
                    @Override public String toString(Data item)
                    {
                        if (item == null) return null;
                        return item.string;
                    }

                    @Override public Data fromString(String string)
                    {
                        for (Data item : cmbBx.getItems())
                        {
                            if (item.string.equals(string)) return item;
                        }
                        return null;
                    }
                };

        // use the cell factory method to provide content for the button cell
        cmbBx.setButtonCell(cellFactory.call(null));
        // let the cell factory deliver the content of the cells
        cmbBx.setCellFactory(cellFactory);
        // set a condition that lets a Data item appear in the list of the auto complete suggestions or not
        Predicate<Data> predicate =
                data ->
                {
                    String cmbBxEditorTextToLowerCase = cmbBx.getEditor().getText().toLowerCase();
                    String dataStringToLowerCase = data.string.toLowerCase();
                    boolean result = dataStringToLowerCase.contains(cmbBxEditorTextToLowerCase);
                    return result;
                };

        cmbBx.setConverter(converter);

        // sample data
        ObservableList<Data> items = FXCollections.observableArrayList();
        items.add(new Data("apple1"));
        items.add(new Data("apple2"));
        items.add(new Data("apple3"));
        items.add(new Data("ball1"));
        items.add(new Data("ball2"));
        items.add(new Data("ball3"));
        items.add(new Data("cat1"));
        items.add(new Data("cat2"));
        items.add(new Data("cat3"));

        cmbBx.setItems(items);

The above preparations is pretty much conventional stuff that users of combo boxes have to deal with anyway in javafx.

However there is the predicate that should be mentioned here because it allows for "filtering" items of the combobox depending on the user input in the combo box editor. For the sample data above this means, that if the editor content is "a" all items are contained in the list of suggestions and if it is "t" then only the cats are contained.

To add this behaviour to combo boxes you simply do this:

        AutoCompleteComboBoxListener<Data> autoCompleteComboBoxListener =
                new AutoCompleteComboBoxListener<Data>(cmbBx, predicate);

One pretty little feature of AutoCompleteComboBoxListener is that you can change the items of the combo box on the fly by calling AutoCompleteComboBoxListener.repopulate(ObservableList<T> newPopulation).

So finally here is the code for AutoCompleteComboBoxListener:

import java.util.function.Predicate;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class AutoCompleteComboBoxListener<T> implements EventHandler<KeyEvent>
{
//    private final static Logger LOGGER = LogManager.getLogger(AutoCompleteComboBoxListener.class);

    // constructor injection
    private ComboBox<T> cmbBx;
    private Predicate<T> predicate;

    /** populated in constructor, holds a copy of the initial items */
    private ObservableList<T> initialItems = FXCollections.observableArrayList();

    public AutoCompleteComboBoxListener(final ComboBox<T> cmbBx, final Predicate<T> predicate)
    {
        this.cmbBx = cmbBx;
        this.predicate = predicate;

        initialItems.addAll(cmbBx.getItems());

        // if combobox was not editable all effort in this class would be superfluous
        cmbBx.setEditable(true);
        cmbBx.setOnAction
        (
                e ->
                {
                    cmbBx.getEditor().end();
//                    LOGGER.debug("editor: " + cmbBx.getEditor().getText() + ", value: " + cmbBx.getValue());
                }
        );
        cmbBx.setOnKeyReleased(this);
    }

    @Override public void handle(KeyEvent keyEvent)
    {
        // handle events that allow for early return
        if (keyEvent.getCode() == KeyCode.ENTER)
        {
            cmbBx.setValue(cmbBx.getSelectionModel().getSelectedItem());
//            LOGGER.debug("editor: " + cmbBx.getEditor().getText() + ", value: " + cmbBx.getValue());
            return;
        }
        if (keyEvent.getCode() == KeyCode.DOWN)
        {
            if (cmbBx.isShowing() == false)
            {
                cmbBx.show();
            }
            return;
        }
        if (   keyEvent.getCode() == KeyCode.LEFT
            || keyEvent.getCode() == KeyCode.RIGHT
            || keyEvent.getCode() == KeyCode.HOME
            || keyEvent.getCode() == KeyCode.END
            || keyEvent.getCode() == KeyCode.TAB
            || keyEvent.getCode() == KeyCode.BACK_SPACE
            || keyEvent.getCode() == KeyCode.DELETE
            || keyEvent.isControlDown()
            || keyEvent.isAltDown()
            || keyEvent.isShiftDown()
           )
        {
            return;
        }

        repopulate(initialItems);

        if (!cmbBx.getItems().isEmpty())
        {
            cmbBx.show();
        }
    }

    public void repopulate(ObservableList<T> newPopulation)
    {
        initialItems = newPopulation;

        boolean showing = cmbBx.isShowing();

        // combobox items will be repopulated, so make sure the "old" items do not show any longer on the
        // screen
        cmbBx.hide();

        // repopulate combobox.items with those initial items that match the predicate
        ObservableList<T> list = FXCollections.observableArrayList();
        cmbBx.setItems(list);

        initialItems
                .forEach
                (
                        item ->
                        {
                            if (predicate.test(item))
                            {
                                list.add(item);
                            }
                        }
                );

        if (showing) cmbBx.show();
    }
}

Cheers
Roger