package bluej.stride.slots;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import bluej.editor.stride.FrameCatalogue;
import bluej.stride.framedjava.ast.JavaFragment;
import bluej.stride.framedjava.ast.links.PossibleLink;
import bluej.stride.framedjava.errors.CodeError;
import bluej.stride.framedjava.slots.TextOverlayPosition;
import bluej.stride.generic.Frame;
import bluej.stride.generic.Frame.View;
import bluej.stride.generic.FrameContentRow;
import bluej.stride.generic.InteractionManager;
import bluej.stride.slots.SuggestionList.SuggestionDetails;
import bluej.stride.slots.SuggestionList.SuggestionListListener;
import bluej.utility.Utility;
import bluej.utility.javafx.ErrorUnderlineCanvas;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.SharedTransition;
import threadchecker.OnThread;
import threadchecker.Tag;
| A choice slot has three overlaid elements (not counting the error underline) that make up
| the display. There is curDisplay, a Label which shows, in black, the text typed so far by the user
| since they entered the slot. Exactly underneath that is futureDisplay, which shows in cyan what
| the current completion would be if selected. There is also dummyField, which serves to show a cursor,
| but is always empty.
|
| You might think we could combine curDisplay and dummyField, but we don't want the cursor to be able
| to leave the end of the line; editing the start of a choice slot makes very little sense, especially
| since it effectively blanks when you enter it.
|
public class ChoiceSlot<T extends Enum<T>> implements EditableSlot, CopyableHeaderItem{
private final InteractionManager editor;
private final Frame parentFrame;
private final FrameContentRow row;
private final ObjectProperty<SuggestionList> dropdown = new SimpleObjectProperty<>(null);
private final List<T> choices;
private T previousSelection;
private T selection;
private final StackPane pane;
private final SlotLabel curDisplay;
private final Label futureDisplay;
private final DummyTextField dummyField;
private final ErrorUnderlineCanvas errorMarker;
private Function<T, Boolean> isValid;
private final BooleanBinding effectivelyFocusedProperty;
public ChoiceSlot(final InteractionManager editor, Frame parentFrame, FrameContentRow row, final List<T> choices, Function<T, Boolean> isValid, final String stylePrefix, Map<T, String> hints)
{
this.editor = editor;
this.parentFrame = parentFrame;
this.row = row;
this.choices = choices;
this.isValid = isValid;
dummyField = new DummyTextField();
curDisplay = new SlotLabel("");
futureDisplay = new Label();
pane = new StackPane();
errorMarker = new ErrorUnderlineCanvas(pane);
pane.getChildren().addAll(futureDisplay, curDisplay.getNode(), dummyField, errorMarker.getNode());
StackPane.setAlignment(curDisplay.getNode(), Pos.CENTER_LEFT);
StackPane.setAlignment(futureDisplay, Pos.CENTER_LEFT);
editor.setupFocusableSlotComponent(this, dummyField, false, row::getExtensions, hints.entrySet().stream().map(e -> new FrameCatalogue.Hint(e.getKey().toString(), e.getValue())).collect(Collectors.toList()));
pane.getStyleClass().addAll("choice-slot", stylePrefix + "choice-slot");
JavaFXUtil.addStyleClass(curDisplay, "choice-current", stylePrefix + "choice-current");
JavaFXUtil.addStyleClass(futureDisplay, "choice-future", stylePrefix + "choice-future");
JavaFXUtil.addStyleClass(dummyField, "choice-dummy", stylePrefix + "choice-dummy");
pane.setOnMouseClicked(e -> {
if (!dummyField.isDisabled()) {
dummyField.requestFocus();
e.consume();
}
});
JavaFXUtil.addFocusListener(dummyField, focused -> {
if (focused)
{
JavaFXUtil.runAfterCurrent(() -> {
if (dummyField.isFocused())
{
editor.beginRecordingState(ChoiceSlot.this);
curDisplay.setText("");
previousSelection = selection;
selection = null;
showSuggestions(previousSelection);
JavaFXUtil.setPseudoclass("bj-transparent", false, pane);
}
});
}
});
DoubleBinding calcWidth = new DoubleBinding() {{ super.bind(curDisplay.fontProperty());
super.bind(curDisplay.textProperty());
super.bind(futureDisplay.fontProperty());
super.bind(futureDisplay.textProperty());
}
@Override
protected double computeValue()
{
return Math.max(10, Math.max(curDisplay.measureString(curDisplay.getText()),
JavaFXUtil.measureString(futureDisplay, futureDisplay.getText())));
}
};
curDisplay.prefWidthProperty().bind(calcWidth);
futureDisplay.prefWidthProperty().bind(calcWidth);
dummyField.prefWidthProperty().bind(calcWidth);
dummyField.translateXProperty().bind(new DoubleBinding() {{ super.bind(curDisplay.fontProperty());
super.bind(curDisplay.textProperty());
}
@Override
protected double computeValue()
{
return curDisplay.measureString(curDisplay.getText());
}
});
curDisplay.minWidthProperty().set(Region.USE_PREF_SIZE);
futureDisplay.setMinWidth(Region.USE_PREF_SIZE);
dummyField.setMinWidth(Region.USE_PREF_SIZE);
pane.heightProperty().addListener((a, b, c) -> JavaFXUtil.runNowOrLater(() -> refreshError()));
pane.widthProperty().addListener((a, b, c) -> JavaFXUtil.runNowOrLater(() -> refreshError()));
effectivelyFocusedProperty = dummyField.focusedProperty().or(dropdown.isNotNull());
setValue(null);
}
@OnThread(Tag.FXPlatform)
class ChoiceSuggestionListener
implements SuggestionListListener
{
public void suggestionListChoiceClicked(SuggestionList suggestionList, int highlighted)
{
if (highlighted != -1)
setValue(choices.get(highlighted));
editor.endRecordingState(ChoiceSlot.this);
row.focusRight(ChoiceSlot.this);
}
@Override
public Response suggestionListKeyTyped(SuggestionList suggestionList, KeyEvent event, int highlighted)
{
if (event.getCharacter().equals(" "))
{
Optional<T> completion = getCompletion(highlighted);
if (completion.isPresent())
{
setValue(completion.get());
row.focusRight(ChoiceSlot.this);
return Response.DISMISS;
}
else
{
return Response.CONTINUE;
}
}
else
{
dummyField.fireEvent(event.copyFor(null, dummyField));
return Response.CONTINUE;
}
}
@Override
public Response suggestionListKeyPressed(SuggestionList suggestionList, KeyEvent event, int highlighted)
{
switch (event.getCode())
{
case ENTER:
row.focusRight(ChoiceSlot.this);
suggestionListFocusStolen(highlighted);
return Response.DISMISS;
case ESCAPE:
setValue(previousSelection);
row.focusRight(ChoiceSlot.this);
return Response.DISMISS;
case LEFT:
row.focusLeft(ChoiceSlot.this);
suggestionListFocusStolen(highlighted);
return Response.DISMISS;
case RIGHT:
row.focusRight(ChoiceSlot.this);
suggestionListFocusStolen(highlighted);
return Response.DISMISS;
case TAB:
if (event.isShiftDown())
row.focusLeft(ChoiceSlot.this);
else{ row.focusRight(ChoiceSlot.this);
}
suggestionListFocusStolen(highlighted);
return Response.DISMISS;
default:
return Response.CONTINUE;
}
}
@Override
public void hidden()
{
JavaFXUtil.setPseudoclass("bj-transparent", true, pane);
editor.endRecordingState(ChoiceSlot.this);
dropdown.set(null);
}
private Optional getCompletion(int highlighted)
{
if (highlighted != -1)
return Optional.of(choices.get(highlighted));
}
else if (dropdown.get().eligibleCount() == 1 && curDisplay.getText().length() > 0)
{
return Optional.of(choices.get(dropdown.get().getFirstEligible()));
}
return Optional.empty();
}
@Override
public void suggestionListFocusStolen(int highlighted)
{
Optional<T> completion = getCompletion(highlighted);
if (completion.isPresent())
{
setValue(completion.get());
}
else
{
setValue(previousSelection);
}
}
}
| Shows the suggestions dropdown, and highlights the given item (null means no highlight)
|
@OnThread(Tag.FXPlatform)
public void showSuggestions(T curHighlight)
{
dropdown.set(
new SuggestionList(editor,
Utility.mapList(choices, t -> new SuggestionDetails(t.toString())), null,
SuggestionList.SuggestionShown.RARE, i -> {
i = i < 0 ? 0 : i; futureDisplay.setText(choices.get(i).toString());
},
new ChoiceSuggestionListener())
);
dropdown.get().show(pane, new BoundingBox(0, 0, 0, pane.heightProperty().get()));
dropdown.get().calculateEligible(curDisplay.getText(), false, false);
dropdown.get().setHighlighted(curHighlight == null ? -1 : choices.indexOf(curHighlight), true);
dropdown.get().updateVisual(curDisplay.getText());
}
| If no selection has been made, defaultVal is returned. Usually
| you don't want a null value, you want some suitable empty value.
|
public T getValue(T defaultVal)
{
if (selection == null)
return defaultVal;
else{ return selection;
}
}
public void setValue(T value)
{
selection = value;
curDisplay.setText(value == null ? "" : value.toString());
futureDisplay.setText(curDisplay.getText());
JavaFXUtil.runNowOrLater(() -> refreshError());
JavaFXUtil.setPseudoclass("bj-transparent", isValid.apply(selection) && !dummyField.isFocused(), pane);
editor.modifiedFrame(parentFrame, false);
}
@OnThread(Tag.FXPlatform)
private void refreshError()
{
if (!isValid.apply(selection))
{
if (errorMarker.getHeight() > 0.0)
{
errorMarker.addErrorMarker(this, 0, Integer.MAX_VALUE, false, b -> {
}, new ReadOnlyBooleanWrapper(true));
}
}
else
{
errorMarker.clearErrorMarkers(this);
}
}
@OnThread(value = Tag.FXPlatform, ignoreParent = true)
private class DummyTextField
extends TextField
{
@OnThread(Tag.FX)
public DummyTextField()
{
}
@Override
public void appendText(String s)
{
insertText(0, s);
}
@Override
public void clear()
{
update("");
}
@Override
public void copy()
{
ClipboardContent c = new ClipboardContent();
c.putString(futureDisplay.getText());
Clipboard.getSystemClipboard().setContent(c);
}
@Override
public void cut()
{
copy();
dropdown.get().setHighlighted(-1, false);
update("");
}
@Override
public boolean deletePreviousChar()
{
if (curDisplay.getText().length() > 0) {
update(curDisplay.getText().substring(0, Math.max(0, curDisplay.getText().length() - 1)));
return true;
}
return false;
}
@Override
public void insertText(int pos, String s)
{
if (pos != 0)
throw new IllegalStateException();
else{ update(curDisplay.getText() + s);
}
}
@Override
public void paste()
{
String clip = Clipboard.getSystemClipboard().getString();
if (clip != null && !clip.equals(""))
update(clip);
}
@Override
public void replaceSelection(String s)
{
appendText(s);
}
@Override
public void replaceText(IndexRange arg0, String s)
{
update(curDisplay.getText() + s);
}
@Override
public void replaceText(int arg0, int arg1, String s)
{
update(curDisplay.getText() + s);
}
private void update(String newVal)
{
dropdown.get().calculateEligible(newVal, false, false);
if (dropdown.get().eligibleCount() == 0)
{
dropdown.get().calculateEligible(curDisplay.getText(), false, false);
}
else
{
curDisplay.setText(newVal);
dropdown.get().updateVisual(newVal);
}
}
}
@Override
public ObservableList getComponents()
{
return FXCollections.observableArrayList((Node)pane);
}
@Override
public void requestFocus(Focus on)
{
dummyField.requestFocus();
|
|
|if (on == Focus.LEFT)
|
|dummyField.positionCaret(0);
|
|else if (on == Focus.RIGHT)
|
|dummyField.positionCaret(dummyField.getLength());
|
|else if (on == Focus.SELECT_ALL)
|
|dummyField.selectAll();
}
public void requestFocus()
{
requestFocus(null);
}
@Override
public boolean isFocused()
{
return dummyField.isFocused();
}
public void flagErrorsAsOld()
{
}
public void removeOldErrors()
{
}
public void cleanup()
{
}
@Override
public int getFocusInfo()
{
return -1;
}
@Override
public Node recallFocus(int info)
{
requestFocus();
return dummyField;
}
public Stream getCurrentErrors()
{
return Stream.empty();
}
public Node getPrimaryFocus()
{
return dummyField;
}
@Override
public TextOverlayPosition getOverlayLocation(int caretPos, boolean javaPos)
{
return TextOverlayPosition.nodeToOverlay(pane, 0.0, 0.0, curDisplay.fontProperty().get().getSize(), pane.getHeight());
}
@Override
public void addError(CodeError err)
{
}
@Override
public void focusAndPositionAtError(CodeError err)
{
}
@Override
public void addUnderline(Underline u)
{
}
@Override
public void removeAllUnderlines()
{
}
@Override
public void saved()
{
}
@Override
public List extends PossibleLink> findLinks()
{
return Collections.emptyList();
}
public void lostFocus()
{
}
public Frame getParentFrame()
{
return parentFrame;
}
public void setView(View oldView, View newView, SharedTransition animate)
{
dummyField.setDisable(newView != View.NORMAL);
curDisplay.setView(oldView, newView, animate);
if (newView != View.JAVA_PREVIEW)
{
animate.addOnStopped(() -> {
curDisplay.minWidthProperty().set(Region.USE_PREF_SIZE);
futureDisplay.setOpacity(1.0);
});
}
else
{
futureDisplay.setOpacity(0.0);
}
}
@Override
public JavaFragment getSlotElement()
{
return null;
}
@Override
public boolean isAlmostBlank()
{
return true;
}
@Override
public boolean isEditable()
{
return !dummyField.disableProperty().get();
}
@Override
public void setEditable(boolean editable)
{
dummyField.setDisable(!editable);
pane.setDisable(!editable);
}
@Override
public Stream extends Node> makeDisplayClone(InteractionManager editor)
{
Stream<Label> labelClone = curDisplay.makeDisplayClone(editor);
StackPane clone = new StackPane();
clone.getChildren().addAll(labelClone.peek(l -> {
l.setAlignment(Pos.CENTER_LEFT);
l.prefWidthProperty().bind(curDisplay.prefWidthProperty());
}).collect(Collectors.toList()));
JavaFXUtil.bindList(clone.getStyleClass(), pane.getStyleClass());
JavaFXUtil.bindPseudoclasses(clone, pane.getPseudoClassStates());
clone.minHeightProperty().bind(pane.heightProperty());
clone.minWidthProperty().bind(pane.widthProperty());
clone.prefHeightProperty().bind(pane.heightProperty());
return Stream.of(clone);
}
@Override
public ObservableBooleanValue effectivelyFocusedProperty()
{
return effectivelyFocusedProperty;
}
@Override
public int calculateEffort()
{
return 1;
}
}
. ChoiceSlot
. computeValue
. computeValue
top,
use,
map,
class ChoiceSuggestionListener
. suggestionListChoiceClicked
. suggestionListKeyTyped
. suggestionListKeyPressed
. hidden
. getCompletion
. suggestionListFocusStolen
. showSuggestions
. getValue
. setValue
. refreshError
top,
use,
map,
class DummyTextField
. DummyTextField
. appendText
. clear
. copy
. cut
. deletePreviousChar
. insertText
. paste
. replaceSelection
. replaceText
. replaceText
. update
. getComponents
. requestFocus
. requestFocus
. isFocused
. flagErrorsAsOld
. removeOldErrors
. cleanup
. getFocusInfo
. recallFocus
. getCurrentErrors
. getPrimaryFocus
. getOverlayLocation
. addError
. focusAndPositionAtError
. addUnderline
. removeAllUnderlines
. saved
. findLinks
. lostFocus
. getParentFrame
. setView
. getSlotElement
. isAlmostBlank
. isEditable
. setEditable
. makeDisplayClone
. effectivelyFocusedProperty
. calculateEffort
791 neLoCode
+ 18 LoComm