/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2014, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.javafx.util;
import java.util.regex.Pattern;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
/**
* Auto-completion for combobox.
* Perform text auto-completion on textfields of editable combo-box. combo-box
* {@link ComboBox#items } are adapted based on user typing.
*
* @author Johann Sorel (Geomatys)
* @author Alexis Manin (Geomatys)
*/
public class ComboBoxCompletion {
private final ComboBox comboBox;
private ObservableList baseData;
private ObservableList filteredData;
public ComboBoxCompletion(final ComboBox comboBox) {
this.comboBox = comboBox;
this.comboBox.setOnKeyReleased(this::onKeyReleased);
baseData = comboBox.getItems();
comboBox.itemsProperty().addListener(this::updateBaseData);
}
/**
* Update available {@link ComboBox#items} according to typed value.
* @param event
*/
private void onKeyReleased(KeyEvent event) {
if (baseData == null || baseData.isEmpty()) {
return;
}
final KeyCode code = event.getCode();
if (event.isControlDown() || code == KeyCode.HOME || code == KeyCode.END || code == KeyCode.TAB) {
return;
}
final TextField editor = comboBox.getEditor();
final int caretPosition = editor.getCaretPosition();
if (code == KeyCode.DOWN) {
if (!comboBox.isShowing()) {
comboBox.show();
editor.positionCaret(caretPosition);
}
} else {
final String completeText = editor.getText();
final String currentText = editor.getText(0, caretPosition);
if (currentText != null) {
comboBox.hide(); // hide to force resize when we'll have to show it back.
final Pattern searchPattern = buildPattern(currentText);
filteredData = FXCollections.observableArrayList(
baseData.filtered((Object t) -> searchPattern.matcher(comboBox.getConverter().toString(t)).find()));
comboBox.setItems(filteredData);
if (!filteredData.isEmpty()) {
comboBox.show();
}
// we're forced to set back editor text, because change of items
// also change it.
editor.setText(completeText);
editor.positionCaret(caretPosition);
}
}
}
private void updateBaseData(ObservableValue observable, Object oldValue, Object newValue) {
if (comboBox.getItems() != filteredData) {
baseData = comboBox.getItems();
}
}
/**
* Adapt text typed in editor to build a permissive regex to search for
* correspondance in combo box items.
*
* @param fieldText The text to transform in regex pattern.
* @return created pattern
*/
public static Pattern buildPattern(final String fieldText) {
final String pattern = fieldText
// We do not need groups in this particular case, so we assume user has real parenthesis in his text.
.replaceAll("(\\()|(\\))", "\\$1")
// In case user mistype word separation (Ex : typed "Point10" instead of "Point 10" or "type-1" instead of "type - 1").
.replaceAll("([a-zA-Z]+|\\d+|\\W+)\\s*", "$1\\\\s*");
return Pattern.compile("(?i)"+pattern);
}
/**
* Build a new combobox completion but does not return it to avoid warnings
* "new instance ignored".
*
* @param combobox to apply completion on
*/
public static void autocomplete(final ComboBox combobox){
new ComboBoxCompletion(combobox);
}
}