org.tentackle.fx.component.config.PrefixSelectionFeature Maven / Gradle / Ivy
/*
* Tentackle - https://tentackle.org
*
* 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.fx.component.config;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.Control;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.scene.input.KeyEvent;
import javafx.util.StringConverter;
import org.tentackle.fx.FxUtilities;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Select items according to a prefix.
* Useful for ComboBox and ChoiceBox but can be applied to any control providing a list of items
* that can be selected.
*
* @author harald
* @param the control type
*/
@SuppressWarnings("rawtypes")
public class PrefixSelectionFeature {
/**
* To disable this feature, invoke:
*
* control.getProperties().remove(PrefixSelectionFeature.ENABLED);
*
*/
public static final String ENABLED = "prefixSelectionEnabled";
protected final T control;
protected final BooleanSupplier asYouTypeCondition;
protected final Supplier itemProvider;
protected final Supplier itemConverter;
protected final Consumer selector;
protected final StringBuilder prefixBuf; // collected prefix string
private long lastPressMillis; // epochal time of last keystroke
private Integer index; // where to start the search, null if n/a
private boolean isDropDownVisible; // true if dropdown is visible
/**
* Creates a prefix selection feature.
*
* @param control the control to add this feature to
* @param asYouTypeCondition the condition to activate preselection by keystrokes
* @param itemProvider the items that can be selected
* @param itemConverter the item to string converter
* @param selector the selector to select an item by its index
*/
public PrefixSelectionFeature(T control,
BooleanSupplier asYouTypeCondition,
Supplier itemProvider,
Supplier itemConverter,
Consumer selector) {
this.control = control;
this.asYouTypeCondition = asYouTypeCondition;
this.itemProvider = itemProvider;
this.itemConverter = itemConverter;
this.selector = selector;
prefixBuf = new StringBuilder();
}
/**
* Configures the control to support this feature.
*/
public void configure() {
control.addEventHandler(KeyEvent.KEY_TYPED, createHandler());
if (control instanceof ComboBoxBase) {
((ComboBoxBase) control).showingProperty().addListener((obs, wasShowing, isShowing) -> {
isDropDownVisible = isShowing;
if (isDropDownVisible && isEnabled()) {
boolean clearIndex = false;
if (index == null && control instanceof ComboBox && ((ComboBox) control).isEditable()) {
String value = ((ComboBox) control).getEditor().getText();
if (value != null) {
prefixBuf.append(value);
select();
clearIndex = true;
}
}
if (index != null) {
scrollToIndexInDropDown();
if (clearIndex) {
index = null;
prefixBuf.setLength(0);
}
}
}
});
}
}
/**
* Scrolls to the selected index in the dropdown.
*/
protected void scrollToIndexInDropDown() {
if (index != null) {
Skin> skin = control.getSkin();
if (skin instanceof ComboBoxListViewSkin) {
((ListView>) ((ComboBoxListViewSkin>) skin).getPopupContent()).scrollTo(index);
}
}
}
/**
* Creates the handler to catch the key events.
*
* @return the handler
*/
protected EventHandler createHandler() {
return event -> {
if (isSelectionByKeyEnabled()) {
String chr = event.getCharacter();
if (chr != null) { // for sure... can it be null at all?
long now = System.currentTimeMillis();
if (now > lastPressMillis + FxUtilities.getInstance().getPrefixSelectionTimeout()) {
// clear after timeout
prefixBuf.setLength(0);
index = null;
}
lastPressMillis = now;
prefixBuf.append(chr);
select();
}
}
};
}
/**
* Returns whether this feature is enabled for the control.
*
* @return true if enabled
*/
protected boolean isEnabled() {
return control.getProperties().containsKey(ENABLED);
}
/**
* Returns whether selection by key is enabled.
*
* @return true if enabled
*/
protected boolean isSelectionByKeyEnabled() {
return asYouTypeCondition.getAsBoolean() && isEnabled();
}
/**
* Select an item according to the current prefix.
*/
protected void select() {
String prefix = getPrefix();
int i = 0;
for (Object item: itemProvider.get()) {
if ((index == null || i >= index) && isItemMatching(prefix, item)) {
index = i;
selector.accept(index);
if (isDropDownVisible) {
scrollToIndexInDropDown();
}
break;
}
i++;
}
}
/**
* Gets the prefix string to be used for {@link #isItemMatching}.
*
* @return the prefix string
*/
protected String getPrefix() {
return prefixBuf.toString().toUpperCase();
}
/**
* Returns whether item is matching prefix string.
*
* @param prefix the prefix string
* @param item the item
* @return true if matching
*/
@SuppressWarnings("unchecked")
protected boolean isItemMatching(String prefix, Object item) {
StringConverter converter = itemConverter == null ? null : itemConverter.get();
String itemText = converter == null ? item.toString() : converter.toString(item);
return itemText != null && itemText.toUpperCase().startsWith(prefix);
}
}