package bluej.stride.slots;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import bluej.utility.Debug;
import javafx.beans.binding.DoubleExpression;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.effect.BlendMode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.web.WebView;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;
import bluej.prefmgr.PrefMgr;
import bluej.utility.javafx.FXPlatformConsumer;
import bluej.utility.javafx.FXPlatformRunnable;
import threadchecker.OnThread;
import threadchecker.Tag;
import bluej.Config;
import bluej.utility.Utility;
import bluej.utility.javafx.FXSupplier;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.ScalableHeightLabel;
| A code completion suggestion list that pops up beneath a slot to offer suggestions
| for the user to pick from, using either the arrow keys, the mouse, or by typing more and pressing enter.
|
| Suggestions can either be "direct", meaning that what the user has typed so far is
|* (ignoring case) an exact prefix of the suggestion, or they can be "similar" meaning
* that what the user has typed is (ignoring case) within MAX_EDIT_DISTANCE of one
* chunk of the suggestion. Here, chunk means a part of the suggestion that begins
* after an underscore (e.g. actor_details in "get_actor_details") or a case change
* (e.g. ActorDetails in "getActorDetails").
*
* Direct suggestions are always shown first, and similar suggestions are shown beneath
* a suitable label.
*/
@OnThread(Tag.FXPlatform)
public class SuggestionList{
// The next available SuggestionList ID; must be static to be unique across all instances:
|
|private static final AtomicInteger nextSuggListId = new AtomicInteger(1);
|
|// The SuggestionList ID for this item, used for data recording purposes:
|
|private final int suggestionListId;
|
|private final SuggestionListListener listener;
|
|private static class SuggestionListView extends ListView<SuggestionListItem>
|
|{
|
|private final SimpleStyleableDoubleProperty cssTypeWidthProperty = new SimpleStyleableDoubleProperty(TYPE_WIDTH_META_DATA);
|
|public final SimpleStyleableDoubleProperty cssTypeWidthProperty() { return cssTypeWidthProperty;
|
|}
|
|// ListView doesn't offer -fx-pref-width as style, so we implement it ourselves:
|
|private final SimpleStyleableDoubleProperty cssPrefWidthProperty = new SimpleStyleableDoubleProperty(PREF_WIDTH_META_DATA);
|
|public final SimpleStyleableDoubleProperty cssPrefWidthProperty() { return cssPrefWidthProperty;
|
|}
|
|private static final CssMetaData<SuggestionListView, Number> TYPE_WIDTH_META_DATA =
|
|JavaFXUtil.cssSize("-bj-type-width", SuggestionListView::cssTypeWidthProperty);
private static final CssMetaData<SuggestionListView, Number> PREF_WIDTH_META_DATA =
JavaFXUtil.cssSize("-bj-pref-width", SuggestionListView::cssPrefWidthProperty);
private static final List <CssMetaData <? extends Styleable, ? > > cssMetaDataList =
JavaFXUtil.extendCss(ListView.getClassCssMetaData())
|
|.add(TYPE_WIDTH_META_DATA)
|
|.add(PREF_WIDTH_META_DATA)
|
|.build();
|
|public static List <CssMetaData <? extends Styleable, ? > > getClassCssMetaData() { return cssMetaDataList;
|
|}
|
|@Override public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { return getClassCssMetaData();
|
|}
|
|public SuggestionListView(DoubleExpression typeWidth, FXPlatformConsumer<SuggestionListItem> clickListener)
|
|{
|
|setEditable(false);
|
|setCellFactory(lv -> new SuggestionCell(typeWidth, clickListener));
|
|prefWidthProperty().bind(cssPrefWidthProperty);
|
|}
|
|}
|
|/**
| Element containing all the suggestion items:
|
private final SuggestionListView listBox;
| The window containing the pane
|
private final Stage window;
| List of choices available for the user. Each entry represents a different item.
|
private final List<SuggestionDetails> choices;
| This array contains two entries per choice. There is one complete set from
| 0 to choices.size() - 1 which are the direct suggestions, and a second
| set from choices.size() to (2 * choices.size() - 1) which are the similar
| suggestions. We make sure that a suggestion is never shown twice: either
| its direct suggestion or its similar suggestion, or neither, but never both.
|
private final List<SuggestionListItem> doubleSuggestions = new ArrayList<>();
| This is a filtered version of doubleSuggestions, which contains the items which
| are actually shown to the user. It also includes the "related items" label when applicable, which is indicated by null
|*/
private final ObservableList<SuggestionListItem> showingItems = FXCollections.observableArrayList();
/**
* Current eligible completions, map from index in doubleSuggestions to details
| about the completion. As noted above, no choice will appear twice; it will either
| appear once as direct, once as similar, or not at all.
|
private final HashMap<Integer, EligibleDetail> eligible = new HashMap<>();
| The current highlighted item; -1 for no highlight, or otherwise an index
| between 0 and doubleSuggestions.size() - 1 (if it is < choices.size(), it must
| be a direct suggestion, it if is >= choices.size() it must be a similar suggestion).
|
private int highlighted = -1;
| A listener that needs to be updated when the highlighted suggestion changes.
| Integer is index into choices, not doubleSuggestions.
|
private final Consumer<Integer> highlightListener;
| The label that divides the direct and similar suggestions. Only displays when
| there are actual similar suggestions to show
|
private final ScalableHeightLabel similarLabel;
| The label for when there are no eligible suggestions. Only displays when
| there no eligible suggestions;
|
private final ScalableHeightLabel noneLabel;
| The width of the type labels that appear to the left of the suggestions.
| Can be zero, when you don't want to show any types.
|
private final DoubleProperty typeWidth;
| Used when "replaying" last calculateEligible call */
|private String lastPrefix;
/** Used when "replaying" last calculateEligible call */
private boolean lastAllowSimilar;
private boolean expectingToLoseFocus = false;
private ObjectProperty<SuggestionShown> shownState = new SimpleObjectProperty<>(SuggestionShown.COMMON);
|
|private FXPlatformRunnable cancelShowDocsTask;
|
|private Pane docPane;
|
|private boolean hiding = false;
|
|private final BooleanProperty moreLabelAtBottom = new SimpleBooleanProperty(true);
|
|private static class EligibleDetail implements Comparable<EligibleDetail>
|
|{
|
|// The offset into the suggestion string of the matching part
|
|public final int suggestionOffset;
|
|// The edit distance between the matching part and what the user has typed.
|
|public final int distance;
|
|// The length of what the user has typed.
|
|private final int length;
|
|public EligibleDetail(int suggestionOffset, int distance, int length)
|
|{
|
|this.suggestionOffset = suggestionOffset;
|
|this.distance = distance;
|
|this.length = length;
|
|}
|
|@OnThread(value = Tag.FX, ignoreParent = true)
|
|public int compareTo(EligibleDetail e)
|
|{
|
|// If one matches at the start, that is definitely better than one
|
|// that matches later on, even if it has higher edit distance.
|
|// Compare whether one of us and "e" matches at start and other doesn't:
if ((suggestionOffset == 0) == (e.suggestionOffset == 0))
{
// Either both at start or neither; compare edit distance
|
|return Integer.compare(distance, e.distance);
|
|}
|
|else
|
|{
|
|if (suggestionOffset == 0)
|
|return -1; // We start at the beginning and are the better one
|
|else{ return 1;
|
|} // Must be them
|
|}
|
|}
|
|public boolean close()
|
|{
|
|if (distance == 0 && suggestionOffset == 0)
|
|return true; // Always show direct suggestions
|
|if (distance == 0 && suggestionOffset != 0)
|
|return length >= 2; // Only show substring matches after two characters
|
|if (distance == 1)
|
|return length >= 3; // Only show typo matches after three characters
|
|if (distance == 2)
|
|return length >= 10; // Show further matches, but only if you are typing a long identifier
|
|return false; // distance 3 or higher; Too far away
|
|}
|
|}
|
|// Whether the suggestion is common (shown from first trigger) or rare (shown only on second trigger)
|
|public static enum SuggestionShown
|
|{
|
|COMMON, RARE;
|
|}
|
|// package-visible; needs to be seen by SuggestionCell
|
|class SuggestionListItem
|
|{
|
|// Index into the choices list
|
|// When this is -1, it means we are the "related items" divider label.
public final int index;
// Whether the type of the completion matches the expected type of the context. Only valid if type is not null.
public final boolean typeMatch;
|
|public final boolean direct;
|
|public final IntegerProperty eligibleAt = new SimpleIntegerProperty();
|
|public final IntegerProperty eligibleLength = new SimpleIntegerProperty();
|
|public final BooleanProperty eligibleCanTab = new SimpleBooleanProperty(false);
|
|public final BooleanProperty highlighted = new SimpleBooleanProperty(false);
|
|public SuggestionListItem(int index, boolean typeMatch, boolean direct)
|
|{
|
|this.index = index;
|
|this.typeMatch = typeMatch;
|
|this.direct = direct;
|
|}
|
|public SuggestionDetails getDetails()
|
|{
|
|return choices.get(index);
|
|}
|
|@Override
|
|public boolean equals(Object o)
|
|{
|
|if (this == o) return true;
|
|if (o == null || getClass() != o.getClass()) return false;
|
|SuggestionListItem that = (SuggestionListItem) o;
|
|if (index != that.index) return false;
|
|return direct == that.direct;
|
|}
|
|@Override
|
|public int hashCode()
|
|{
|
|return 2 * index + (direct ? 1 : 0);
|
|}
|
|}
|
|public static class SuggestionDetails
|
|{
|
|// The string of the choices to match the user's input against. Cannot be null.
|
|public final String choice;
|
|// Non-matchable bit displayed at the end of a suggestion, e.g. param types. Can be null.
|
|public final String suffix;
|
|// A type to be displayed before the suggestion. Can be null.
|
|public final String type;
|
|// Whether the suggestion is common (shown from first trigger) or rare (shown only on second trigger)
|
|public final SuggestionShown shown;
|
|public SuggestionDetails(String choice)
|
|{
|
|this(choice, null, null, SuggestionShown.COMMON);
|
|}
|
|public SuggestionDetails(String choice, String suffix, String type, SuggestionShown shown)
|
|{
|
|if (choice == null)
|
|throw new IllegalArgumentException();
|
|if (shown == null)
|
|throw new IllegalArgumentException();
|
|this.choice = choice;
|
|this.suffix = suffix;
|
|this.type = type;
|
|this.shown = shown;
|
|}
|
|public boolean hasDocs()
|
|{
|
|return false;
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public Pane makeDocPane()
|
|{
|
|return null;
|
|}
|
|}
|
|public static class SuggestionDetailsWithHTMLDoc extends SuggestionDetails
|
|{
|
|private final String docHTML;
|
|public SuggestionDetailsWithHTMLDoc(String choice, SuggestionShown shown, String docHTML)
|
|{
|
|super(choice, null, null, shown);
|
|this.docHTML = docHTML;
|
|}
|
|public SuggestionDetailsWithHTMLDoc(String choice, String suffix, String type, SuggestionShown shown, String docHTML)
|
|{
|
|super(choice, suffix, type, shown);
|
|this.docHTML = docHTML;
|
|}
|
|public boolean hasDocs()
|
|{
|
|return true;
|
|}
|
|@Override
|
|@OnThread(Tag.FXPlatform)
|
|public Pane makeDocPane()
|
|{
|
|WebView webView = new WebView();
|
|Pane docDisplay = new BorderPane(webView);
|
|JavaFXUtil.addStyleClass(docDisplay, "suggestion-javadoc");
webView.getEngine().setJavaScriptEnabled(false);
webView.getEngine().loadContent(docHTML);
docDisplay.setMaxWidth(400);
docDisplay.setMaxHeight(300);
|
|// Workaround to get transparent background, from:
|
|// http://stackoverflow.com/questions/12421250/transparent-background-in-the-webview-in-javafx
|
|webView.setBlendMode(BlendMode.DARKEN);
|
|return docDisplay;
|
|}
|
|}
|
|public static class SuggestionDetailsWithCustomDoc extends SuggestionDetails
|
|{
|
|private final FXSupplier<Pane> docMaker;
|
|public SuggestionDetailsWithCustomDoc(String choice, String suffix, String type, SuggestionShown shown, FXSupplier<Pane> docMaker)
|
|{
|
|super(choice, suffix, type, shown);
|
|this.docMaker = docMaker;
|
|}
|
|public boolean hasDocs()
|
|{
|
|return true;
|
|}
|
|public Pane makeDocPane()
|
|{
|
|return docMaker.get();
|
|}
|
|}
|
|/**
| Create a SuggestionList.
|
| @param listParent Editor (used to get overlay panes)
| @param choices The strings of the choices to match the user's input against
| @param suffixes Non-matchable bits on the end of a suggestion, e.g. param types.
| Should either be null or same length as choices, one suffix per choice.
| @param types Should either be null, or the same length as choices, one type per choice
| @param highlightListener A listener for the highlighted item changing. Can be null.
| @param clickListener A listener for a choice being clicked (and thus selected). Cannot be null.
|
public SuggestionList(SuggestionListParent listParent, List<? extends SuggestionDetails> choices, String targetType,
SuggestionShown startShown, Consumer<Integer> highlightListener, final SuggestionListListener listener)
{
if (listener == null)
throw new IllegalArgumentException("SuggestionListListener cannot be null");
this.suggestionListId = nextSuggListId.getAndIncrement();
this.choices = FXCollections.observableArrayList(choices);
this.shownState.set(startShown);
this.listener = listener;
this.highlightListener = highlightListener;
this.similarLabel = new ScalableHeightLabel("Related:", false);
similarLabel.setMaxWidth(9999);
this.noneLabel = new ScalableHeightLabel("No completions", false);
noneLabel.setMaxWidth(9999);
JavaFXUtil.addStyleClass(noneLabel, "suggestion-none");
this.typeWidth = new SimpleDoubleProperty();
this.window = new Stage(StageStyle.TRANSPARENT);
this.listBox = new SuggestionListView(this.typeWidth, item -> {
highlighted = doubleSuggestions.indexOf(item);
int highlighted = getHighlighted();
if (highlighted != -1)
{
listener.suggestionListChoiceClicked(this, highlighted);
expectingToLoseFocus = true;
hiding = true;
window.hide();
listener.hidden();
}
});
JavaFXUtil.addStyleClass(listBox, "suggestion-list");
this.typeWidth.bind(choices.stream().allMatch(s -> s.type == null) ? new ReadOnlyDoubleWrapper(0.0) : listBox.cssTypeWidthProperty());
listBox.setBackground(null);
listBox.setItems(this.showingItems);
listBox.setStyle(listParent.getFontCSS().get());
this.docPane = new Pane();
docPane.setMinWidth(400.0);
docPane.setMaxHeight(300.0);
docPane.setBackground(null);
BorderPane listAndDocBorderPane = new BorderPane();
JavaFXUtil.addStyleClass(listAndDocBorderPane, "suggestion-top-level");
AnchorPane listAndMoreAndTransPane = new AnchorPane();
listAndMoreAndTransPane.setBackground(null);
listAndMoreAndTransPane.setPickOnBounds(false);
listBox.setMaxHeight(300.0);
listBox.setPrefHeight(choices.isEmpty() ? 100.0 : 2 * listParent.getFontSize() * choices.size());
listAndDocBorderPane.setCenter(listAndMoreAndTransPane);
BorderPane.setMargin(listAndMoreAndTransPane, new Insets(0, 1, 0, 0));
listAndDocBorderPane.setRight(docPane);
listAndDocBorderPane.setMaxHeight(300.0);
Label moreLabel = new Label("Showing common options. Press Ctrl+Space again to see all options");
JavaFXUtil.addStyleClass(moreLabel, "suggestion-more-label");
listAndDocBorderPane.setBackground(null);
listAndDocBorderPane.setPickOnBounds(false);
AnchorPane moreLabelPane = new AnchorPane(moreLabel);
moreLabel.setMaxWidth(300.0);
AnchorPane.setLeftAnchor(moreLabel, 0.0);
AnchorPane.setTopAnchor(moreLabel, 0.0);
AnchorPane.setBottomAnchor(moreLabel, 0.0);
JavaFXUtil.addStyleClass(moreLabelPane, "suggestion-more-label-pane");
window.setResizable(false);
BorderPane listAndMorePane = new BorderPane();
JavaFXUtil.addStyleClass(listAndMorePane, "suggestion-dialog-lhs");
listAndMorePane.setCenter(listBox);
if (shownState.get() == SuggestionShown.COMMON)
listAndMorePane.setBottom(moreLabelPane);
listAndMoreAndTransPane.getChildren().add(listAndMorePane);
AnchorPane.setLeftAnchor(listAndMorePane, 0.0);
AnchorPane.setRightAnchor(listAndMorePane, 0.0);
AnchorPane.setTopAnchor(listAndMorePane, 0.0);
JavaFXUtil.addChangeListener(moreLabelAtBottom, atBottom -> {
if (atBottom)
{
listAndMorePane.setTop(null);
listAndMorePane.setBottom(shownState.get() == SuggestionShown.COMMON ? moreLabelPane : null);
AnchorPane.setTopAnchor(listAndMorePane, 0.0);
AnchorPane.setBottomAnchor(listAndMorePane, null);
BorderPane.setAlignment(docPane, Pos.TOP_LEFT);
}
else
{
listAndMorePane.setBottom(null);
listAndMorePane.setTop(shownState.get() == SuggestionShown.COMMON ? moreLabelPane : null);
AnchorPane.setTopAnchor(listAndMorePane, null);
AnchorPane.setBottomAnchor(listAndMorePane, 0.0);
BorderPane.setAlignment(docPane, Pos.BOTTOM_LEFT);
}
JavaFXUtil.setPseudoclass("bj-at-top", !atBottom, moreLabelPane);
});
JavaFXUtil.addChangeListener(shownState, s -> {
if (s == SuggestionShown.RARE)
{
listAndMorePane.setTop(null);
listAndMorePane.setBottom(null);
}
});
Scene scene = new Scene(listAndDocBorderPane);
window.setHeight(350.0);
scene.setFill(null);
Config.addEditorStylesheets(scene);
window.setScene(scene);
listParent.setupSuggestionWindow(window);
for (int j = 0; j <= 1; j++)
{
for (int i = 0; i < choices.size(); i++)
{
SuggestionDetails choice = choices.get(i);
SuggestionListItem sugg = new SuggestionListItem(i, targetType != null && choice.type != null ? targetType.equals(choice.type) : false, j == 0);
doubleSuggestions.add(sugg);
}
}
listBox.setPlaceholder(noneLabel);
JavaFXUtil.addFocusListener(window, focused -> {
if (!focused)
{
hideDocDisplay();
hiding = true;
JavaFXUtil.runAfterCurrent(() -> {
window.hide();
if (!expectingToLoseFocus)
listener.suggestionListFocusStolen(getHighlighted());
listener.hidden();
});
}
});
listAndDocBorderPane.addEventFilter(KeyEvent.KEY_TYPED, e -> {
if (e.getCharacter().equals(" ") && e.isControlDown())
{
if (shownState.get() == SuggestionShown.COMMON)
{
shownState.set(SuggestionShown.RARE);
calculateEligible(lastPrefix, lastAllowSimilar, false);
updateVisual(lastPrefix);
}
}
else if (!e.getCharacter().contains("\u0000") && listener.suggestionListKeyTyped(this, e, getHighlighted()) == SuggestionListListener.Response.DISMISS)
{
expectingToLoseFocus = true;
hiding = true;
window.hide();
listener.hidden();
}
e.consume();
});
listAndDocBorderPane.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
switch (e.getCode())
{
case UP:
up();
break;
case DOWN:
down();
break;
case PAGE_UP:
pageUp();
break;
case PAGE_DOWN:
pageDown();
break;
case HOME:
home();
break;
case END:
end();
break;
case SPACE:
if (e.isControlDown())
{
e.consume();
if (shownState.get() == SuggestionShown.COMMON)
{
shownState.set(SuggestionShown.RARE);
calculateEligible(lastPrefix, lastAllowSimilar, false);
updateVisual(lastPrefix);
}
break;
}
default:
int selected = getHighlighted();
if (selected == -1 && eligibleCount() == 1)
selected = getFirstEligible() % choices.size();
if (listener.suggestionListKeyPressed(this, e, selected) == SuggestionListListener.Response.DISMISS)
{
expectingToLoseFocus = true;
hiding = true;
window.hide();
listener.hidden();
}
break;
}
e.consume();
});
}
@OnThread(Tag.FXPlatform)
public void show(final Node reference, final Bounds textBoundsWithinReference)
{
if (eligibleCount() == 1)
{
boolean singleOptionAvailable = true;
if (shownState.get() == SuggestionShown.COMMON)
{
calculateEligible(lastPrefix, lastAllowSimilar, SuggestionShown.RARE, false);
if (eligibleCount() != 1)
{
calculateEligible(lastPrefix, lastAllowSimilar, SuggestionShown.COMMON, false);
singleOptionAvailable = false;
}
}
if (singleOptionAvailable)
{
int choice = getFirstEligible();
if (choice < choices.size())
{
JavaFXUtil.runAfterCurrent(() ->
{
listener.hidden();
listener.suggestionListChoiceClicked(this, choice);
});
return;
}
}
}
window.getScene().getRoot().applyCss();
double xPos = reference.localToScene(reference.getBoundsInLocal()).getMinX();
Window refWindow = reference.getScene().getWindow();
double screenMaxY = Screen.getScreensForRectangle(refWindow.getX(), refWindow.getY(), refWindow.getWidth(), refWindow.getHeight())
.stream().mapToDouble(s -> s.getVisualBounds().getMaxY()).min().orElse(999999.0);
double windowX = refWindow.getX() + reference.getScene().getX() + xPos + textBoundsWithinReference.getMinX() - typeWidth.get() - 1.0f;
double windowY = refWindow.getY() + reference.getScene().getY() + reference.localToScene(reference.getBoundsInLocal()).getMinY() + textBoundsWithinReference.getMinY();
if (screenMaxY < window.getHeight() + windowY)
{
windowY -= 350.0;
moreLabelAtBottom.set(false);
}
else
{
moreLabelAtBottom.set(true);
windowY += textBoundsWithinReference.getHeight();
}
window.setX(windowX);
window.setY(windowY);
|
|
|Debug.message(
|
|"Showing at position: " + windowX + ", " + windowY +
"\n Based on " + refWindow.getX() + ", " + refWindow.getY() +
"\n Scene in Window: " + reference.getScene().getX() + ", " + reference.getScene().getY() +
"\n Reference in scene: " + reference.localToScene(reference.getBoundsInLocal()) +
"\n Bounds in reference: " + textBoundsWithinReference +
"\n type width: " + typeWidth.get()
);*/
if (window.getOwner() == null)
window.initOwner(reference.getScene().getWindow());
window.show();
//org.scenicview.ScenicView.show(window.getScene());
listBox.requestFocus();
|
|}
|
|private void up()
|
|{
|
|// Go backwards through eligible completions to find previous eligible.
|
|// Pressing UP on top item deselects all completions
|
|for (int candidate = highlighted - 1; candidate >= -1; candidate--)
|
|{
|
|if (candidate == -1 || eligible.containsKey(candidate))
|
|{
|
|setHighlighted(candidate, true);
|
|break;
|
|}
|
|}
|
|}
|
|private void down()
|
|{
|
|// This works if highlighted is -1, too; advance to 0:
|
|for (int candidate = highlighted + 1; candidate < doubleSuggestions.size(); candidate++)
|
|{
|
|if (eligible.containsKey(candidate))
|
|{
|
|setHighlighted(candidate, true);
|
|break;
|
|}
|
|}
|
|}
|
|private void home()
|
|{
|
|setHighlighted(getFirstEligible(), true);
|
|}
|
|private void end()
|
|{
|
|setHighlighted(getLastEligible(), true);
|
|}
|
|private void pageUp()
|
|{
|
|for (int i = 0; i < 10; i++)
|
|up();
|
|// But don't leave nothing selected:
|
|if (highlighted == -1)
|
|home();
|
|}
|
|private void pageDown()
|
|{
|
|for (int i = 0; i < 10; i++)
|
|down();
|
|}
|
|protected void setHighlighted(int newHighlight, boolean scrollTo)
|
|{
|
|if (highlighted == newHighlight)
|
|{
|
|if (highlighted != -1 && scrollTo)
|
|listBox.scrollTo(Math.max(0, showingItems.indexOf(doubleSuggestions.get(newHighlight)) - 3));
|
|return;
|
|}
|
|if (highlighted != -1)
|
|doubleSuggestions.get(highlighted).highlighted.set(false);
|
|highlighted = newHighlight;
|
|if (highlighted != -1)
|
|{
|
|doubleSuggestions.get(highlighted).highlighted.set(true);
|
|if (scrollTo)
|
|listBox.scrollTo(Math.max(0, showingItems.indexOf(doubleSuggestions.get(newHighlight)) - 3));
|
|}
|
|if (highlightListener != null)
|
|highlightListener.accept(getHighlighted());
|
|JavaFXUtil.runNowOrLater(() -> showDocsFor(getHighlighted()));
|
|}
|
|public void calculateEligible(String prefix, boolean allowSimilar, boolean canChangeToRare)
|
|{
|
|calculateEligible(prefix, allowSimilar, shownState.get(), canChangeToRare);
|
|}
|
|/**
| Calculates the eligible choices (those that begin with the given prefix).
| Does not actually do any graphical update; for that, call updateVisual
| @param prefix The current prefix that the user has typed
| @param allowSimilar Whether to allow similar items (correct for typos)
| @param canChangeToRare If we are showing common and there are no suggestions, can we switch to rare?
|
public void calculateEligible(String prefix, boolean allowSimilar, SuggestionShown shown, boolean canChangeToRare)
{
lastPrefix = prefix;
lastAllowSimilar = allowSimilar;
eligible.clear();
for (int i = 0; i < choices.size(); i++)
{
String sugg = choices.get(i).choice;
if (choices.get(i).shown.compareTo(shown) > 0)
{
}
else if (sugg.toLowerCase().startsWith(prefix.toLowerCase()))
{
eligible.put(i, new EligibleDetail(0, 0, prefix.length()));
}
else if (allowSimilar)
{
List<Integer> wordStarts = splitIdentLower(sugg);
Optional<EligibleDetail> me = wordStarts.stream().map(j -> new EligibleDetail(j, distanceTo(prefix, sugg, j), prefix.length()))
.filter(EligibleDetail::close)
.sorted()
.findFirst();
if (me.isPresent())
{
eligible.put(i + doubleSuggestions.size() / 2, me.get());
}
}
}
if (eligible.isEmpty() && shown == SuggestionShown.COMMON && canChangeToRare)
{
shownState.set(SuggestionShown.RARE);
calculateEligible(prefix, allowSimilar, SuggestionShown.RARE, false);
}
}
private static int distanceTo(String prefix, String candidate, int offset)
{
prefix = prefix.toLowerCase();
String partialLower = candidate.substring(offset, Math.min(candidate.length(), offset + prefix.length())).toLowerCase();
String partialLowerShort = candidate.substring(offset, Math.min(candidate.length(), offset + Math.max(1, prefix.length() - 1))).toLowerCase();
String partialLowerLong = candidate.substring(offset, Math.min(candidate.length(), offset + 1 + prefix.length())).toLowerCase();
return Math.min(
Utility.editDistance(partialLower, prefix),
Math.min(Utility.editDistance(partialLowerShort, prefix), Utility.editDistance(partialLowerLong, prefix))
);
}
private static boolean hasCase(char c)
{
return Character.isUpperCase(c) != Character.isLowerCase(c);
}
private static List splitIdentLower(String text)
{
int startCurWord = 0;
List<Integer> r = new ArrayList<>();
for (int i = 1
| start at 2nd char
|
i < text.length(); i++)
{
if ((hasCase(text.charAt(i)) && hasCase(text.charAt(i - 1))) &&
(Character.isUpperCase(text.charAt(i)) == Character.isLowerCase(text.charAt(i - 1))
|| Character.isLowerCase(text.charAt(i)) == Character.isUpperCase(text.charAt(i - 1)))
&& (startCurWord == 0 || i - startCurWord > 1))
{
r.add(startCurWord);
startCurWord = i;
}
else if ((text.charAt(i) == '_' || text.charAt(i) == '.') && startCurWord < i - 1)
{
r.add(startCurWord);
startCurWord = i + 1;
}
}
r.add(startCurWord);
return r;
}
| Updates the available options in the dropdown, restricting it to those
| that are currently marked as eligible. Thus this function only has a useful effect
| if you call calculateEligible first.
|
public void updateVisual(String prefix)
{
boolean showingAnySimilar = false;
for (int i = 0; i < doubleSuggestions.size(); i++)
{
if (eligible.containsKey(i))
{
if (i > doubleSuggestions.size() / 2)
showingAnySimilar = true;
}
}
for (Entry<Integer, EligibleDetail> e : eligible.entrySet())
{
boolean canComplete = eligible.size() == 1
|| highlighted == e.getKey()
|| doubleSuggestions.get(e.getKey()).getDetails().choice.equals(prefix);
doubleSuggestions.get(e.getKey()).eligibleAt.set(e.getValue().suggestionOffset);
doubleSuggestions.get(e.getKey()).eligibleLength.set(prefix.length());
doubleSuggestions.get(e.getKey()).eligibleCanTab.set(canComplete);
}
List<SuggestionListItem> newShowingItems = new ArrayList<>();
for (int i = 0; i < doubleSuggestions.size(); i++)
{
if (eligible.containsKey(i))
newShowingItems.add(doubleSuggestions.get(i));
if (i == choices.size() && showingAnySimilar)
newShowingItems.add(new SuggestionListItem(-1, false, false));
}
showingItems.setAll(newShowingItems);
if (highlighted != -1)
{
if (!eligible.containsKey(highlighted))
setHighlighted(getFirstEligible(), true);
else{ setHighlighted(highlighted, true);
}
}
}
public int eligibleCount()
{
return eligible.size();
}
public int getFirstEligible()
{
return eligible.keySet().stream().mapToInt(i -> i).min().orElse(-1);
}
public int getLastEligible()
{
return eligible.keySet().stream().mapToInt(i -> i).max().orElse(-1);
}
@OnThread(Tag.FXPlatform)
public void highlightFirstEligible()
{
setHighlighted(getFirstEligible(), false);
}
private int getHighlighted()
{
return choices.size() == 0 ? -1 : highlighted % choices.size();
}
public static interface SuggestionListListener
{
| An item has been selected by clicking on it.
| @param highlighted The index of the item in the original list passed
| to the SuggestionList constructor (regardless of what is
| currently eligible/ineligible). -1 if the user clicked
| outside a suggestion
|
@OnThread(Tag.FXPlatform)
p.public void suggestionListChoiceClicked(SuggestionList suggestionList, int highlighted);
| A key typed event was been received by the suggestion list window. Listeners
| will usually insert the given key into the source code document.
|
| @param suggestionList The originating suggestion list.
| @param event The key event received.
| @param highlighted The index of the currently highlighted item.
| @return Whether to now close the code completion dialog, or continue showing it
|
@OnThread(Tag.FXPlatform)
p.public Response suggestionListKeyTyped(SuggestionList suggestionList, KeyEvent event, int highlighted);
| A key pressed event was been received by the suggestion list window. Listeners
| will usually listen for keys like ENTER, TAB, ESCAPE to act on them. Note that
| UP and DOWN are handled automatically by SuggestionList itself to move up and down
| the list, but all other key handling is up to the listener.
|
| @param suggestionList The originating suggestion list.
| @param event The key event received.
| @param highlighted The index of the currently highlighted item.
| @return Whether to now close the code completion dialog, or continue showing it
|
@OnThread(Tag.FXPlatform)
p.public Response suggestionListKeyPressed(SuggestionList suggestionList, KeyEvent event, int highlighted);
@OnThread(Tag.FXPlatform)
default void suggestionListFocusStolen(int highlighted)
{
};
@OnThread(Tag.FXPlatform)
default void hidden()
{
};
public static enum Response
{
DISMISS, CONTINUE;
}
}
public Optional getLongestCommonPrefix()
{
return longestCommonPrefix(eligible.entrySet().stream()
.filter(e -> e.getKey() < choices.size())
.map(e -> choices.get(e.getKey()).choice)
.collect(Collectors.toList()));
}
private static Optional longestCommonPrefix(List<String> srcs)
{
return srcs.stream().reduce((a, b) -> {
int maxLen = Math.min(a.length(), b.length());
if (maxLen == 0) return "";
for (int i = 0; i < maxLen; i++)
{
if (a.charAt(i) != b.charAt(i))
{
return a.substring(0, i);
}
}
return a.substring(0, maxLen);
});
}
public DoubleExpression widthProperty()
{
return listBox.widthProperty();
}
public DoubleExpression typeWidthProperty()
{
return typeWidth;
}
public boolean isShowing()
{
return window.isShowing();
}
public boolean isInMiddleOfHiding()
{
return hiding ;
}
@OnThread(Tag.FXPlatform)
private void showDocsFor(int selected)
{
if (cancelShowDocsTask != null)
{
cancelShowDocsTask.run();
cancelShowDocsTask = null;
}
hideDocDisplay();
if (selected != -1 && choices.get(selected).hasDocs())
{
cancelShowDocsTask = JavaFXUtil.runAfter(Duration.millis(500), () -> {
docPane.getChildren().setAll(choices.get(selected).makeDocPane());
});
}
}
private void hideDocDisplay()
{
docPane.getChildren().clear();
}
public int getRecordingId()
{
return suggestionListId;
}
@OnThread(Tag.FXPlatform)
public static interface SuggestionListParent
{
| Gets font size as a complete piece of CSS (including any "-fx-font-size:" etc)
|* ready to set as the inline style.
*/
@OnThread(Tag.FX)
public StringExpression getFontCSS();
/**
* Gets font size as a double, used for size calculation
public double getFontSize();
| Add any necessary listeners to a code completion window
|
public void setupSuggestionWindow(Stage window);
}
}
. SuggestionList
. show
. calculateEligible
. distanceTo
. hasCase
. splitIdentLower
. updateVisual
. eligibleCount
. getFirstEligible
. getLastEligible
. highlightFirstEligible
. getHighlighted
top,
use,
map,
interface SuggestionListListener
. suggestionListChoiceClicked
. suggestionListKeyTyped
. suggestionListKeyPressed
. suggestionListFocusStolen
. hidden
. getLongestCommonPrefix
. longestCommonPrefix
. widthProperty
. typeWidthProperty
. isShowing
. isInMiddleOfHiding
. showDocsFor
. hideDocDisplay
. getRecordingId
top,
use,
map,
interface SuggestionListParent
. getFontSize
. setupSuggestionWindow
809 neLoCode
+ 341 LoComm