2016-02-05 27 views
5

Ho implementato un ComboBox in cui il suo elenco è filtrato dall'input nello ComboBoxTextField. Funziona come ci si potrebbe aspettare un filtro di un tale controllo per funzionare. Tutti gli elementi dell'elenco che iniziano con il testo di input sono mostrati nell'elenco.Impostazione del predicato per FilteredList in ComboBox influisce sull'input

Ho solo un piccolo problema. Se seleziono un elemento dall'elenco e poi tento di rimuovere l'ultimo carattere nel campo di testo, non succede nulla. Se seleziono un elemento dall'elenco e poi provo a rimuovere qualsiasi altro carattere rispetto all'ultimo, viene rimossa l'intera stringa. Entrambi questi problemi si verificano solo se questa è la prima cosa che faccio nello ComboBox. Se scrivo prima qualcosa nella casella combinata o se seleziono una voce per la seconda volta, nessuno dei problemi descritti si verifica.

Ciò che è veramente strano per me è che questi problemi sembrano essere causati dal fatto che il predicato sia impostato (se commento l'invocazione di , tutto funziona correttamente). Questo è strano poiché penso che dovrebbe influenzare solo l'elenco per il quale è stato impostato il predicato. Non dovrebbe influire sul resto dello ComboBox.

import javafx.application.Application; 
import javafx.beans.value.ChangeListener; 
import javafx.beans.value.ObservableValue; 
import javafx.collections.FXCollections; 
import javafx.collections.ObservableList; 
import javafx.collections.transformation.FilteredList; 
import javafx.scene.Scene; 
import javafx.scene.control.ComboBox; 
import javafx.scene.layout.VBox; 
import javafx.stage.Stage; 
import javafx.util.StringConverter; 

public class TestInputFilter extends Application { 
    public void start(Stage stage) { 
     VBox root = new VBox(); 

     ComboBox<ComboBoxItem> cb = new ComboBox<ComboBoxItem>(); 
     cb.setEditable(true); 

     cb.setConverter(new StringConverter<ComboBoxItem>() { 

      @Override 
      // To convert the ComboBoxItem to a String we just call its 
      // toString() method. 
      public String toString(ComboBoxItem object) { 
       return object == null ? null : object.toString(); 
      } 

      @Override 
      // To convert the String to a ComboBoxItem we loop through all of 
      // the items in the combobox dropdown and select anyone that starts 
      // with the String. If we don't find a match we create our own 
      // ComboBoxItem. 
      public ComboBoxItem fromString(String string) { 
       return cb.getItems().stream().filter(item -> item.getText().startsWith(string)).findFirst() 
         .orElse(new ComboBoxItem(string)); 
      } 
     }); 

     ObservableList<ComboBoxItem> options = FXCollections.observableArrayList(new ComboBoxItem("One is a number"), 
       new ComboBoxItem("Two is a number"), new ComboBoxItem("Three is a number"), 
       new ComboBoxItem("Four is a number"), new ComboBoxItem("Five is a number"), 
       new ComboBoxItem("Six is a number"), new ComboBoxItem("Seven is a number")); 
     FilteredList<ComboBoxItem> filteredOptions = new FilteredList<ComboBoxItem>(options, p -> true); 
     cb.setItems(filteredOptions); 

     InputFilter inputFilter = new InputFilter(cb, filteredOptions); 
     cb.getEditor().textProperty().addListener(inputFilter); 

     root.getChildren().add(cb); 

     stage.setScene(new Scene(root)); 
     stage.show(); 
    } 

    public static void main(String[] args) { 
     launch(); 
    } 

    class ComboBoxItem { 

     private String text; 

     public ComboBoxItem(String text) { 
      this.text = text; 
     } 

     public String getText() { 
      return text; 
     } 

     @Override 
     public String toString() { 
      return text; 
     } 
    } 

    class InputFilter implements ChangeListener<String> { 

     private ComboBox<ComboBoxItem> box; 
     private FilteredList<ComboBoxItem> items; 

     public InputFilter(ComboBox<ComboBoxItem> box, FilteredList<ComboBoxItem> items) { 
      this.box = box; 
      this.items = items; 
     } 

     @Override 
     public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { 
      String value = newValue; 
      // If any item is selected we get the first word of that item. 
      String selected = box.getSelectionModel().getSelectedItem() != null 
        ? box.getSelectionModel().getSelectedItem().getText() : null; 

      // If an item is selected and the value of in the editor is the same 
      // as the selected item we don't filter the list. 
      if (selected != null && value.equals(selected)) { 
       items.setPredicate(item -> { 
        return true; 
       }); 
      } else { 
       items.setPredicate(item -> { 
        if (item.getText().toUpperCase().startsWith(value.toUpperCase())) { 
         return true; 
        } else { 
         return false; 
        } 
       }); 
      } 
     } 
    } 
} 

Edit: ho cercato di ignorare gli ascoltatori chiave in un disperato tentativo di risolvere il problema:

cb.getEditor().addEventFilter(KeyEvent.KEY_PRESSED, e -> { 
    TextField editor = cb.getEditor(); 
    int caretPos = cb.getEditor().getCaretPosition(); 
    StringBuilder text = new StringBuilder(cb.getEditor().getText()); 

    // If BACKSPACE is pressed we remove the character at the index 
    // before the caret position. 
    if (e.getCode().equals(KeyCode.BACK_SPACE)) { 
     // BACKSPACE should only remove a character if the caret 
     // position isn't zero. 
     if (caretPos > 0) { 
      text.deleteCharAt(--caretPos); 
     } 
     e.consume(); 
    } 
    // If DELETE is pressed we remove the character at the caret 
    // position. 
    else if (e.getCode().equals(KeyCode.DELETE)) { 
     // DELETE should only remove a character if the caret isn't 
     // positioned after that last character in the text. 
     if (caretPos < text.length()) { 
      text.deleteCharAt(caretPos); 
     } 
    } 
    // If LEFT key is pressed we move the caret one step to the left. 
    else if (e.getCode().equals(KeyCode.LEFT)) { 
     caretPos--; 
    } 
    // If RIGHT key is pressed we move the caret one step to the right. 
    else if (e.getCode().equals(KeyCode.RIGHT)) { 
     caretPos++; 
    } 
    // Otherwise we just add the key text to the text. 
    // TODO We are currently not handling UP/DOWN keys (should move 
    // caret to the end/beginning of the text). 
    // TODO We are currently not handling keys that doesn't represent 
    // any symbol, like ALT. Since they don't have a text, they will 
    // just move the caret one step to the right. In this case, that 
    // caret should just hold its current position. 
    else { 
     text.insert(caretPos++, e.getText()); 
     e.consume(); 
    } 

    final int finalPos = caretPos; 

    // We set the editor text to the new text and finally we move the 
    // caret to its new position. 
    editor.setText(text.toString()); 
    Platform.runLater(() -> editor.positionCaret(finalPos)); 
}); 

// We just consume KEY_RELEASED and KEY_TYPED since we don't want to 
// have duplicated input. 
cb.getEditor().addEventFilter(KeyEvent.KEY_RELEASED, e -> { 
    e.consume(); 
}); 
cb.getEditor().addEventFilter(KeyEvent.KEY_TYPED, e -> { 
    e.consume(); 
}); 

Purtroppo, questo non risolve il problema neanche. Se io ad es. scegliere la "Tre è un numero" elemento e quindi provare a rimuovere l'ultimo "e" in "Tre", questo è il valore che la proprietà di testo passerà tra:

TextProperty: Three is a number 
TextPropery: Thre is a number 
TextPropery: 

Quindi rimuove il carattere corretto a prima, ma poi rimuove l'intero String per qualche motivo. Come accennato prima, ciò accade solo perché il predicato è stato impostato, e succede solo quando faccio il primo input dopo aver selezionato un oggetto per la prima volta.

risposta

2

Jonatan,

Come Manuel ha dichiarato uno dei problemi è che setPredicate() attiverà il metodo modificato() due volte da quando si modifica il modello combobox, tuttavia il vero problema è che la casella combinata sovrascrive i valori dell'editor con qualsiasi valore appaia adatto. Ecco la spiegazione per i sintomi:

Se seleziono un elemento dalla lista, e quindi provare a rimuovere l'ultimo carattere nel campo di testo, non succede nulla.

In questo caso, la cancellazione dell'ultimo carattere sta realmente accadendo tuttavia la prima chiamata a setPredicate() corrisponde a un elemento possibile (esattamente lo stesso articolo che è stato eliminato l'ultimo carattere di) e cambia il contenuto ComboBox solo un oggetto Ciò provoca una chiamata in cui la casella combinata ripristina il valore dell'editor con la casella combinata corrente.getValue() stringa dando l'illusione che non succede nulla. Inoltre provoca una seconda chiamata al metodo changed(), ma a questo punto il testo dell'editor è già stato modificato.

Perché questo accade solo la prima volta, ma poi mai più?

Buona domanda! Succede solo una volta, perché modifichi l'intero modello sottostante della casella combinata una volta (che come spiegato prima attiva la seconda chiamata al metodo changed()).

Quindi, dopo lo scenario precedente, se fai clic sul pulsante a discesa (freccia destra) vedrai che è rimasto solo un oggetto, e se tenti di eliminare di nuovo un carattere avrai ancora lo stesso oggetto rimasto, cioè , il modello (contenuto della casella combinata) non è cambiato, poiché setPredicate() corrisponderà sempre agli stessi contenuti, pertanto non causerà una chiamata markInvalid() nella classe TextInputControl poiché il contenuto non ha effettivamente cambiato il che significa che non ripristina la stringa di articolo di nuovo (Se vuoi vedere dove viene effettivamente ripristinato il campo di testo la prima volta, vedi il metodo ComboBoxPopupControl.updateDisplayNode() con i sorgenti JavaFX).

Se seleziono un elemento dalla lista, e quindi provare a rimuovere ogni altra carattere di quello precedente, l'intera stringa viene rimosso.

Nel vostro secondo scenario non corrisponde alla chiamata prima setPredicate() (Non ci sono elementi per soddisfare le vostre condizioni startsWith), che elimina tutti gli elementi int casella combinata rimuovendo la selezione corrente e la stringa editor di troppo.

SUGGERIMENTO: prova a dare un senso a te stesso, attiva un punto di interruzione all'interno del metodo changed() per vedere quante volte questo sta entrando e perché (il sorgente JavaFX è necessario se vuoi seguire il ComboBox ei suoi comportamenti dei componenti)

Soluzione: Se si desidera continuare a utilizzare il tuo ChangeListener, si può semplicemente attaccare il problema principale (che è il contenuto Editor di essere sostituite dopo la chiamata setPredicate) ripristinando il testo nell'editor, dopo il filtraggio:

class InputFilter implements ChangeListener<String> { 
    private ComboBox<ComboBoxItem> box; 
    private FilteredList<ComboBoxItem> items; 

    public InputFilter(ComboBox<ComboBoxItem> box, FilteredList<ComboBoxItem> items) { 
     this.box = box; 
     this.items = items; 
    } 

    @Override 
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { 
     String value = newValue; 
     // If any item is selected we get the first word of that item. 
     String selected = box.getSelectionModel().getSelectedItem() != null 
       ? box.getSelectionModel().getSelectedItem().getText() : null; 

     // If an item is selected and the value of in the editor is the same 
     // as the selected item we don't filter the list. 
     if (selected != null && value.equals(selected)) { 
      items.setPredicate(item -> { 
       return true; 
      }); 
     } else { 
      // This will most likely change the box editor contents 
      items.setPredicate(item -> { 
       if (item.getText().toUpperCase().startsWith(value.toUpperCase())) { 
        return true; 
       } else { 
        return false; 
       } 
      }); 

      // Restore the original search text since it was changed 
      box.getEditor().setText(value); 
     } 

     //box.show(); // <-- Uncomment this line for a neat look 
    } 
} 

Personalmente l'ho già fatto prima con i gestori KeyEvent in passato (per evitare più chiamate al mio codice nell'evento changed()), tuttavia puoi sempre usare un semaforo o la tua classe preferita dalla classe java.util.concurrent a evitare qualsiasi rientro indesiderato al tuo metodo, se senti di averne bisogno. In questo momento, getEditor(). SetText() ripristinerà sempre il valore corretto anche se lo stesso metodo scoppia due o tre volte.

Spero che questo aiuti!

+0

Molto ben spiegato. Grazie! Ho solo bisogno di una risposta a una parte della domanda che penso debba ancora essere spiegata prima di accettare questa come risposta corretta: perché questo accade solo la prima volta, ma poi mai più? La tua spiegazione (che ha perfettamente senso) implica che ciò dovrebbe accadere ogni volta. –

+0

Una risposta alla tua domanda sul motivo per cui accade una sola volta è stata aggiunta. Vi consiglio anche di eseguire il debug del vostro codice in caso di dubbio, o almeno di aggiungere alcuni System.out.println (newValue) per una diagnostica rapida. Personalmente, ho imparato molto durante il debugging non solo del mio codice ma delle sorgenti java, all'inizio potrebbe essere travolgente ma in seguito ti rendi conto che è sorprendente. – JavierJ

+0

Ho aggiunto alcuni dettagli aggiuntivi alla risposta, come il richiamo del metodo updateDisplayNode() nella classe ComboBoxPopupControl, che è quello che ripristina la stringa utilizzando il risultato ComboBoxBase.getValue() e quello che lo cancella anche.Il comportamento sembra interessante, fino al punto che potrebbe essere considerato un bug dal momento che getValue() restituisce una stringa completa anche quando getSelectionModel(). GetSelectedIndex() restituisce -1, tuttavia l'API afferma che può restituire anche l'ultimo elemento selezionato (come nel tuo primo scenario). Segna questa domanda come corretta se tutto questo ha risposto alla tua domanda o almeno utile. – JavierJ

1

L'impostazione di un predicato attiverà il tuo ChangeListener, perché stai cambiando gli elementi di ComboBox e quindi il valore di testo dell'editor di cb. La rimozione del listener e il ri-aggiunta impediranno tali azioni impreviste.

Ho aggiunto tre linee al cambiamento (...) - Metodo. Prova, se questa è una soluzione per il tuo problema.

Info: ho usato solo il tuo primo blocco di codice

@Override 
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { 
    String value = newValue; 
    // If any item is selected we get the first word of that item. 
    String selected = box.getSelectionModel().getSelectedItem() != null 
      ? box.getSelectionModel().getSelectedItem().getText() : null; 

    box.getEditor().textProperty().removeListener(this); // new line #1 

    // If an item is selected and the value of in the editor is the same 
    // as the selected item we don't filter the list. 
    if (selected != null && value.equals(selected)) { 
     items.setPredicate(item -> { 
      return true; 
     }); 
    } else { 
     items.setPredicate(item -> { 
      if (item.getText().toUpperCase().startsWith(value.toUpperCase())) { 
       return true; 
      } else { 
       return false; 
      } 
     }); 
     box.getEditor().setText(newValue); // new line #2 
    } 

    box.getEditor().textProperty().addListener(this); // new line #3 
}