package com.mattc.autotyper.gui.fx;
import static com.mattc.autotyper.gui.fx.AutoCompleteUtils.selectCompletionCandidates;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.stage.Popup;
import javafx.stage.Stage;
import com.google.common.collect.Lists;
import com.mattc.autotyper.meta.FXCompatible;
import com.mattc.autotyper.meta.FXParseable;
import com.mattc.autotyper.util.Console;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A TextField that handles AutoCompletion
*
* @author Matthew
*/
@FXCompatible
@FXParseable("%at")
public class AutoCompleteTextField extends TextField implements AutoCompleteControl<String> {
private final AtomicBoolean caused = new AtomicBoolean(false);
private final ObservableList<String> data;
private final ListView<String> listView;
private final Popup popup;
private int limit = 5;
@SuppressWarnings("unused")
private AutoCompleteTextField() {
this(Lists.newArrayList());
}
public AutoCompleteTextField(List<String> data) {
super();
this.data = FXCollections.observableList(data);
this.listView = new ListView<>(this.data);
this.popup = new Popup();
this.popup.getContent().add(this.listView);
this.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
if (AutoCompleteTextField.this.popup.isShowing()) {
AutoCompleteTextField.this.hidePopup();
}
});
this.addEventFilter(EventType.ROOT, (event) -> {
if (event instanceof KeyEvent) {
final KeyEvent evt = (KeyEvent) event;
if ((evt.getCode() == KeyCode.SPACE) && evt.isControlDown()) {
if (!AutoCompleteTextField.this.isPopupShowing()) {
organizeData(getText().trim());
showPopup();
}
setSelection(0);
} else if ((evt.getCode() == KeyCode.ESCAPE) && AutoCompleteTextField.this.popup.isShowing()) {
AutoCompleteTextField.this.hidePopup();
evt.consume();
}
}
});
textProperty().addListener((obs, oldTest, newText) -> {
if (!newText.trim().isEmpty() && (getScene() != null)) {
organizeData(newText);
if (!isPopupShowing() && AutoCompleteTextField.this.listView.getItems().size() > 0) {
showPopup();
}
} else if (AutoCompleteTextField.this.popup.isShowing() && newText.trim().isEmpty()) {
AutoCompleteTextField.this.hidePopup();
}
});
focusedProperty().addListener((obs, oldValue, newValue) -> {
final boolean isFocused = newValue;
if (!isFocused && AutoCompleteTextField.this.popup.isShowing()) {
hidePopup();
}
});
widthProperty().addListener((obs, oldValue, newValue) -> AutoCompleteTextField.this.listView.setPrefWidth(newValue.doubleValue()));
this.listView.setPrefHeight(200);
this.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
// Mouse Click Select Item
this.listView.setOnMouseClicked((e) -> {
if (e.getButton() == MouseButton.PRIMARY)
setTextToSelection();
});
this.listView.setOnKeyPressed((event) -> {
if (!AutoCompleteTextField.this.popup.isShowing()) return;
final int index = AutoCompleteTextField.this.listView.getSelectionModel().getSelectedIndex();
if (event.getCode() == KeyCode.UP) {
setSelection(index - 1);
event.consume();
} else if (event.getCode() == KeyCode.DOWN) {
setSelection(index + 1);
event.consume();
}
if (event.getCode() == KeyCode.ENTER)
setTextToSelection();
});
}
public AutoCompleteTextField(String... data) {
this(Lists.newArrayList(data));
}
@Override
public void setData(List<String> data) {
this.data.clear();
this.data.addAll(data);
}
public void addData(String data) {
this.data.add(data);
}
@Override
public ObservableList<String> getData() {
return this.data;
}
@Override
public ListView<String> getListView() {
return this.listView;
}
@Override
public void setMaxResults(int max) {
this.limit = max;
}
@Override
public int getMaxResults() {
return this.limit;
}
public boolean isPopupShowing() {
return this.popup.isShowing();
}
public void hidePopup() {
if (this.popup.isShowing()) {
this.popup.hide();
this.listView.getItems().clear();
}
}
public void showPopup() {
int count = this.listView.getItems().size();
Console.debug("Popup Request: AutoComplete Items = " + count + ", Will Show Popup? = " + (count != 0));
if (count == 0)
return;
this.listView.getSelectionModel().clearSelection();
final Point2D origin = FXGuiUtils.getScreenCoordinates(AutoCompleteTextField.this);
this.listView.setItems(selectCompletionCandidates(AutoCompleteTextField.this.data, getText(), true));
this.popup.show(AutoCompleteTextField.this, origin.getX(), origin.getY() + getHeight());
}
/**
* Installs listeners that ensure the Popup will follow the Stage as it is moved.
*
* @param stage Stage
*/
public void installXYChangeListeners(Stage stage) {
stage.xProperty().addListener((obs, oldValue, newValue) -> {
final double dX = newValue.doubleValue() - oldValue.doubleValue();
AutoCompleteTextField.this.popup.setX(AutoCompleteTextField.this.popup.getX() + dX);
});
stage.yProperty().addListener((obs, oldValue, newValue) -> {
final double dY = newValue.doubleValue() - oldValue.doubleValue();
AutoCompleteTextField.this.popup.setY(AutoCompleteTextField.this.popup.getY() + dY);
});
}
private void setSelection(int index) {
AutoCompleteTextField.this.caused.set(true);
if (index >= 0 && index < AutoCompleteTextField.this.listView.getItems().size()) {
AutoCompleteTextField.this.listView.getSelectionModel().select(index);
}
}
private void setTextToSelection() {
final String text = AutoCompleteTextField.this.listView.getSelectionModel().getSelectedItem();
if ((text == null) || text.trim().isEmpty()) return;
setText(text);
hidePopup();
positionCaret(getText().length());
}
private void organizeData(String text) {
ObservableList<String> selected = selectCompletionCandidates(AutoCompleteTextField.this.data, text, true);
FXCollections.sort(selected);
AutoCompleteTextField.this.listView.setItems(selected);
}
}