package com.wilutions.fx.acpl; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.ComboBox; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.util.Callback; import javafx.util.StringConverter; /** * Creates an auto completion field. An auto completion field is based on a * ComboBox. A special dummy item "theSearchItem" in the ComboBox shows a popup * window that contains a TextField and a ListView. If a key is pressed in the * TextField, suggestions are tried to be found via a callback interface. The * returned suggestions are listed in the ListView. A suggestion is sellected by * pressing ENTER or double-clicking the item in the ListView. */ public class AutoCompletions_off { /** * Display at most 10 suggestions. */ public final static int NB_OF_SUGGESTIONS = 10; /** * Callback interface to find suggestions for a given text. * * @param <T> * Item type */ public static interface Suggest<T> { /** * Find suggestions for given text. * * @param text * Text * @return Collection of suggestions to be displayed in the list view. */ public Collection<T> find(String text); } /** * Default implementation for interface Suggest. * * @param <T> * Item type */ public static class DefaultSuggest<T> implements Suggest<T> { /** * All items. Passed in the constructor. */ protected Collection<T> allItems; /** * Constructor. * * @param allItems * Collection of all items. */ public DefaultSuggest(Collection<T> allItems) { this.allItems = allItems; } /** * Constructor. */ public DefaultSuggest() { } /** * Find suggestions for given text. All items are returned that contain * the given text in their return value of toString. Those items that * start with the given text are ordered at the beginning of the * returned collection. * * @param text * Text * @return Collection of items. */ public Collection<T> find(String text) { ArrayList<T> matches = new ArrayList<T>(allItems); String textLC = text.toLowerCase(); Collections.sort(matches, new Comparator<T>() { public int compare(T o1, T o2) { String s1 = o1.toString().toLowerCase(); String s2 = o2.toString().toLowerCase(); int cmp = 0; if (!textLC.isEmpty()) { int p1 = s1.indexOf(textLC); int p2 = s2.indexOf(textLC); p1 = makeCompareFromPosition(p1); p2 = makeCompareFromPosition(p2); cmp = p1 - p2; } if (cmp == 0) { cmp = s1.compareTo(s2); } return cmp; } }); // Return only items that contain the text. // Therefore, find the first item that does not contain the text. int endIdx = 0; for (; endIdx < matches.size(); endIdx++) { T item = matches.get(endIdx); if (!item.toString().toLowerCase().contains(textLC)) { break; } } // Cut the list at the item that does not contain the text. Collection<T> ret = matches.subList(0, endIdx); return ret; } private int makeCompareFromPosition(int p) { if (p == 0) { // item starts with the given text. // This item should be positioned at the beginning of the list. } else if (p > 0) { // The item contains the text but does not start with it. // Set p=1 since it does not matter where the text is found. p = 1; } else if (p < 0) { // The item does not contain the text. // Move this item at the end of the list. p = Integer.MAX_VALUE; } return p; } } /** * Prepare the given ComboBox for auto completion. This function uses the * {@link DefaultSuggest}. * * @param cbbox * ComboBox * @param theSearchItem * The dummy item that is to be selected to start the auto * completion popup. * @param allItems * All items. */ public static <T> void bindAutoCompletion(ComboBox<T> cbbox, final T theSearchItem, Collection<T> allItems) { bindAutoCompletion(cbbox, theSearchItem, new DefaultSuggest<T>(allItems)); } /** * Prepare the given ComboBox for auto completion. * * @param cbbox * ComboBox * @param theSearchItem * The dummy item that is to be selected to start the auto * completion popup. * @param suggest * Callback interface to find suggestions. */ public static <T> void bindAutoCompletion(ComboBox<T> cbbox, final T theSearchItem, Suggest<T> suggest) { // Make sure the dummy item "theSearchItem" is contained in the ComboBox. if (!cbbox.getItems().contains(theSearchItem)) { cbbox.getItems().add(0, theSearchItem); } // Select the "theSearchItem" if the ComboBox does not have a selected // item. if (cbbox.getSelectionModel().getSelectedIndex() == -1) { cbbox.getSelectionModel().select(theSearchItem); } // Make sure the ComboBox has a converter interface. if (cbbox.getConverter() == null) { cbbox.setConverter(new StringConverter<T>() { @Override public String toString(T object) { return object != null ? object.toString() : null; } @Override public T fromString(String string) { throw new UnsupportedOperationException(); } }); } // Set listener for selection changed event. // If the selection changes to the "theSearchItem", the auto complete // popoup is shown. cbbox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<T>() { @Override public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) { if (newValue != null && newValue.equals(theSearchItem)) { Platform.runLater(() -> { showAutoCompletionPopup(cbbox, theSearchItem, suggest); }); } } }); // Key-pressed handler cbbox.setOnKeyPressed(new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { switch (event.getCode()) { // Show auto complete popup, if SPACE or ENTER is pressed while // "theSearchItem" is selected. case SPACE: case ENTER: if (cbbox.getSelectionModel().getSelectedItem() == theSearchItem) { Platform.runLater(() -> showAutoCompletionPopup(cbbox, theSearchItem, suggest)); } else { cbbox.show(); } break; // Show auto complete popup if CTRL-F is pressed. case F: if (event.isControlDown()) { event.consume(); Platform.runLater(() -> showAutoCompletionPopup(cbbox, theSearchItem, suggest)); } break; // Show the list of the ComboBox if ALT-Down is pressed. case DOWN: if (event.isAltDown()) { event.consume(); cbbox.show(); } break; default: } } }); // Mouse handler cbbox.setOnMouseClicked(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { // Show auto complete popup, if the mouse is clicked while // "theSearchItem" is selected. if (cbbox.getSelectionModel().getSelectedItem() == theSearchItem) { Platform.runLater(() -> showAutoCompletionPopup(cbbox, theSearchItem, suggest)); } } }); // Cell factory cbbox.setCellFactory( new Callback<ListView<T>, ListCell<T>>() { @Override public ListCell<T> call(ListView<T> param) { final ListCell<T> cell = new ListCell<T>() { { super.setPrefWidth(100); } @Override public void updateItem(T item, boolean empty) { super.updateItem(item, empty); if (item != null) { setText(item.toString()); if (item == theSearchItem) { setStyle("-fx-font-style: italic;"); } } else { setText(null); } } }; return cell; } }); } /** * Show auto completion popup. * * @param cbbox * ComboBox * @param theSearchItem * Dummy item * @param suggest * Suggest callback */ private static <T> void showAutoCompletionPopup(ComboBox<T> cbbox, final T theSearchItem, Suggest<T> suggest) { // Edit field TextField textField = new TextField(); // Transfer the selected item from the ComboBox into the TextField. // T item = cbbox.getSelectionModel().getSelectedItem(); // if (item != null && item != theSearchItem) { // String s = cbbox.getConverter().toString(item); // textField.setText(s); // } // List box ListView<T> listView = new ListView<T>(); // Fill ListView with suggestions. // Select the first item in the ListView. // This allows to press ENTER immediately after the popup has been // displayed. listView.setItems(FXCollections.observableArrayList(suggest.find(textField.getText()))); listView.getSelectionModel().select(0); // Arrange EditField and ListBox vertically. VBox vbox = new VBox(); vbox.getChildren().addAll(textField, listView); // Create a new window. Window parent = cbbox.getScene().getWindow(); Stage dlg = new Stage(); dlg.initOwner(parent); dlg.initStyle(StageStyle.UNDECORATED); // Create a scene double wd = cbbox.getBoundsInParent().getWidth(); // * 2; double ht = cbbox.getBoundsInParent().getHeight() * (NB_OF_SUGGESTIONS); Scene scene = new Scene(vbox, wd, ht); dlg.setScene(scene); // Move popup window over ComboBox. final Window window = cbbox.getScene().getWindow(); double x = window.getX() + cbbox.localToScene(0, 0).getX() + cbbox.getScene().getX(); double y = window.getY() + cbbox.localToScene(0, 0).getY() + cbbox.getScene().getY(); dlg.setX(x); dlg.setY(y); // Key released handler for TextField textField.setOnKeyReleased(new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { switch (event.getCode()) { // Move focus to ListView if arrow key UP or DOWN is pressed. case DOWN: case UP: listView.requestFocus(); event.consume(); break; // Do nothing with positioning keys. case LEFT: case RIGHT: case HOME: case END: event.consume(); break; // Find suggestions. default: Platform.runLater(() -> { Collection<T> listItems = suggest.find(textField.getText()); listView.setItems(FXCollections.observableArrayList(listItems)); listView.getSelectionModel().select(0); }); break; } } }); // Key pressed handler for TextField and ListView EventHandler<KeyEvent> keyHandler = new EventHandler<KeyEvent>() { public void handle(KeyEvent event) { switch (event.getCode()) { // Close popup on ESCAPE, do not change ComboBox selection. case ESCAPE: dlg.close(); break; // On TAB or ENTER, select item in ComboBox and close popup. case TAB: case ENTER: { selectedListItemToComboBox(cbbox, listView); dlg.close(); break; } default: } } }; textField.setOnKeyPressed(keyHandler); listView.setOnKeyPressed(keyHandler); // Mouse handler for ListView listView.setOnMouseClicked(new EventHandler<MouseEvent>() { public void handle(MouseEvent mouseEvent) { // On double-click, select item in ComboBox and close popup. if (mouseEvent.getButton().equals(MouseButton.PRIMARY)) { if (mouseEvent.getClickCount() > 1) { selectedListItemToComboBox(cbbox, listView); dlg.close(); } } } }); // Focused handler for popup. // Close -popup if it looses the focus (e.g. mouse click somewhere else). dlg.focusedProperty().addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { if (!newValue) { dlg.close(); } } }); // Show auto completion popup dlg.show(); } /** * Select ComboBox item if ENTER is pressed. * @param cbbox ComboBox * @param listView ListView */ private static <T> void selectedListItemToComboBox(ComboBox<T> cbbox, ListView<T> listView) { T selectedItem = listView.getSelectionModel().getSelectedItem(); if (selectedItem != null) { if (!cbbox.getItems().contains(selectedItem)) { cbbox.getItems().add(selectedItem); } cbbox.getSelectionModel().select(selectedItem); } } }