/**
* List Category Labels are greyed out, non-selectable list items that help
* organize the list into categories for easier viewing. This constant
* establishes a standard for how to uniquely mark them as such.
* <p>
* TODO: Move this to a ControlsFX Utility class for general use.
*/
public static final String LIST_CATEGORY_LABEL_PREFIX = "------";
// TODO: Find a way to do this without so much class casting.
box.setCellFactory( param -> {
final ListCell< T > cell = new ListCell< T >() {
@Override
public void updateItem( final T item, final boolean empty ) {
super.updateItem( item, empty );
if ( ( item != null ) && ( item instanceof String ) ) {
final String itemText = ( String ) item;
setText( itemText );
// Selectively enable or disable each item based on
// whether it is a List Category Label.
final boolean disable = itemText.startsWith( LIST_CATEGORY_LABEL_PREFIX );
setDisable( disable );
if ( disable ) {
setTextFill( Color.DARKGRAY );
}
else {
setTextFill( Color.BLACK );
}
}
else {
setText( null );
}
}
};
return cell;
} );
/**
* Return the Predicate to filter the pop-up items based on the given search
* text.
*/
@SuppressWarnings("nls")
private Predicate< T > predicate( final String searchText ) {
// OK, if the display text contains all words, ignoring case, and also
// skipping any category labels as they would be confusing in the
// context of filtered lists (and also in some cases would result in
// false hits vs. empty filters that revert back to the full list).
final String[] lowerCaseSearchWords = searchText.toLowerCase().split( " " );
return value -> {
final String displayText = getDisplayText( value );
final String lowerCaseDisplayText = displayText.toLowerCase();
return Arrays.stream( lowerCaseSearchWords )
.allMatch( word -> !lowerCaseDisplayText
.startsWith( LIST_CATEGORY_LABEL_PREFIX )
&& lowerCaseDisplayText.contains( word ) );
};
}
/**
* Used to alter the behaviour. React on ENTER, TAB and ESC.
*/
private void checkApplyAndCancel( final KeyEvent keyEvent ) {
final ComboBox< T > comboBox = getSkinnable();
final ObservableList< T > filteredItems = filteredComboBox.getItems();
// If we are dealing with text, post-filter the list for items that
// serve as List Category Labels, to avoid invalid selections.
// TODO: Find a way to do this without so much class casting.
final FilteredList< T > postfilteredItems = filteredItems
.filtered( s -> ( s instanceof String )
? !( ( String ) s ).startsWith( LIST_CATEGORY_LABEL_PREFIX )
: true );
final KeyCode keyCode = keyEvent.getCode();
switch ( keyCode ) {
case ENTER:
// Code logic addition to prevent errors when the filter found no
// items, so that we revert to the previous value rather than risk
// an invalid blank selection (blank text is like a null).
if ( ( postfilteredItems == null ) || postfilteredItems.isEmpty() ) {
// Cancel the selection by reverting to the previous value, or
// apply the typed "search" value if the Combo Box is editable.
// TODO: Find a way to do this without so much class casting.
final String searchText = searchField.getText().trim();
final T searchDisplayText = ( T ) getDisplayText( ( T ) searchText );
if ( comboBox.isEditable() && !searchText.isEmpty()
&& !searchText.startsWith( LIST_CATEGORY_LABEL_PREFIX ) ) {
comboBox.setValue( searchDisplayText );
}
else {
comboBox.setValue( previousValue );
}
}
else if ( filteredComboBox.getSelectionModel().isEmpty() ) {
// Select the first item if no selection.
filteredComboBox.getSelectionModel().selectFirst();
}
// Hide the temporary Filter Box now that we are done.
comboBox.hide();
// Request focus on the main ComboBox, or else the focus would be
// somewhere else after we remove the temporary Filter Box.
comboBox.requestFocus();
break;
case ESCAPE:
// Cancel the selection by reverting to the previous value.
comboBox.setValue( previousValue );
// Hide the temporary Filter Box now that we are done.
comboBox.hide();
// Request focus on the main ComboBox, or else the focus would be
// somewhere else after we remove the temporary Filter Box.
comboBox.requestFocus();
break;
case TAB:
// Code logic addition to prevent errors when the filter found no
// items, so that we revert to the previous value rather than risk
// an invalid blank selection (blank text is like a null).
if ( ( postfilteredItems == null ) || postfilteredItems.isEmpty() ) {
// Cancel the selection by reverting to the previous value, or
// apply the typed "search" value if the ComboBox is editable.
// TODO: Find a way to do this without so much casting.
final String searchText = searchField.getText().trim();
final T searchDisplayText = ( T ) getDisplayText( ( T ) searchText );
if ( comboBox.isEditable() && !searchText.isEmpty()
&& !searchText.startsWith( LIST_CATEGORY_LABEL_PREFIX ) ) {
comboBox.setValue( searchDisplayText );
}
else {
comboBox.setValue( previousValue );
}
}
else if ( filteredComboBox.getSelectionModel().isEmpty() ) {
// Select the first item if no selection.
filteredComboBox.getSelectionModel().selectFirst();
}
// Hide the temporary Filter Box now that we are done.
comboBox.hide();
// Request focus on the main ComboBox, or else the focus would be
// somewhere else after we remove the temporary Filter Box.
comboBox.requestFocus();
break;
// $CASES-OMITTED$
default:
break;
}
}