package bluej.stride.framedjava.slots;
import java.util.ArrayList;
import java.util.HashMap;
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 bluej.collect.StrideEditReason;
import bluej.editor.stride.FrameCatalogue;
import bluej.stride.framedjava.ast.StructuredSlotFragment;
import bluej.stride.framedjava.ast.links.PossibleLink;
import bluej.stride.framedjava.slots.InfixStructured.RangeType;
import bluej.stride.generic.ExtensionDescription;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import bluej.Config;
import bluej.editor.stride.CodeOverlayPane;
import bluej.stride.framedjava.ast.JavaFragment.PosInSourceDoc;
import bluej.stride.framedjava.errors.CodeError;
import bluej.stride.framedjava.errors.ErrorAndFixDisplay;
import bluej.stride.framedjava.errors.ErrorAndFixDisplay.ErrorFixListener;
import bluej.stride.framedjava.frames.CodeFrame;
import bluej.stride.framedjava.slots.TextOverlayPosition.Line;
import bluej.stride.generic.Frame;
import bluej.stride.generic.Frame.View;
import bluej.stride.generic.FrameContentRow;
import bluej.stride.generic.InteractionManager;
import bluej.stride.generic.InteractionManager.FileCompletion;
import bluej.stride.slots.EditableSlot;
import bluej.stride.slots.Focus;
import bluej.stride.slots.FocusParent;
import bluej.stride.slots.HeaderItem;
import bluej.stride.slots.LinkedIdentifier;
import bluej.stride.slots.SlotLabel;
import bluej.stride.slots.SuggestionList;
import bluej.stride.slots.SuggestionList.SuggestionDetails;
import bluej.stride.slots.SuggestionList.SuggestionDetailsWithCustomDoc;
import bluej.stride.slots.SuggestionList.SuggestionListListener;
import bluej.utility.Utility;
import bluej.utility.javafx.ErrorUnderlineCanvas;
import bluej.utility.javafx.FXBiConsumer;
import bluej.utility.javafx.FXConsumer;
import bluej.utility.javafx.FXFunction;
import bluej.utility.javafx.FXPlatformConsumer;
import bluej.utility.javafx.FXPlatformFunction;
import bluej.utility.javafx.FXRunnable;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.SharedTransition;
import threadchecker.OnThread;
import threadchecker.Tag;
| The StructuredSlot class is used where a single expression is wanted as a slot. For example,
| in the condition of an if statement, the LHS and the RHS of an assignment, and so on.
|
| StructuredSlot implements the EditableSlot interface and also handles any behaviour
| that should be common to the whole slot. For example, error display is handled by StructuredSlot
| because we only want to display at most one error per expression at any time. Similarly, code
| completion is handled here because we only want one code completion showing, and we may want the
| code completion to keep showing even as the user navigates (and moves focus) among the contents of
| the expression.
|
| All the actual graphical components and logic are not directly implemented by this class, but are
| handled by InfixStructured (StructuredSlot always has one, via the topLevel field), so you should
| see that class for more details. StructuredSlot has some role in bridging the gap between all the logic
| bundled in InfixStructured and the "outside world", e.g. when InfixStructured is changed, it calls
|* StructuredSlot.modified to notify the editor, etc.
*
* This class is abstract, as a few details must be specified by some slim, more specific subclasses
public abstract class StructuredSlot<SLOT_FRAGMENT extends StructuredSlotFragment, INFIX extends InfixStructured<?, INFIX>, COMPLETION_CALCULATOR extends StructuredCompletionCalculator> implements EditableSlot, ErrorFixListener, SuggestionListListener{
private final ErrorUnderlineCanvas overlay;
private final Frame parentFrame;
protected final CodeFrame<?> parentCodeFrame;
private final FrameContentRow row;
@OnThread(Tag.FXPlatform)
private final List<CodeError> allErrors = new ArrayList<>();
@OnThread(Tag.FXPlatform)
private final List<CodeError> shownErrors = new ArrayList<>();
private ErrorAndFixDisplay errorAndFixDisplay;
private CodeError hoverErrorCurrentlyShown;
private SLOT_FRAGMENT slotElement;
protected final INFIX topLevel;
protected final InteractionManager editor;
private final List<Underline> underlines = new ArrayList<>();
protected final COMPLETION_CALCULATOR completionCalculator;
private SuggestionList suggestionDisplay;
private Region suggestionNode;
private boolean currentlyCompleting = false;
private List<FileCompletion> fileCompletions;
private Map<KeyCode, Runnable> fileCompletionShortcuts;
private StringExpression targetType;
private boolean beenModified = false;
protected final StringProperty textMirror = new SimpleStringProperty("");
private List<TextOverlayPosition> selectionDrawPositions;
private final List<FXRunnable> lostFocusActions = new ArrayList<>();
private CaretPos suggestionLocation;
private StructuredSlotField suggestionField;
private final SimpleBooleanProperty fakeCaretShowing = new SimpleBooleanProperty(false);
private final List<FrameCatalogue.Hint> hints;
private final ObservableList<String> recentValues = FXCollections.observableArrayList();
private CaretPos mostRecentPos;
private String valueOnGain;
private boolean editable = true;
private final BooleanProperty focusedProperty = new SimpleBooleanProperty(false);
private final BooleanBinding effectivelyFocusedProperty;
private final List<ModificationToken> modificationTokens = new ArrayList<>();
protected final List<FXRunnable> afterModify = new ArrayList<>();
public StructuredSlot(InteractionManager editor,
Frame parentFrame, CodeFrame<?> parentCodeFrame, FrameContentRow row, String stylePrefix, COMPLETION_CALCULATOR completionCalculator, List<FrameCatalogue.Hint> hints)
{
this.editor = editor;
this.parentFrame = parentFrame;
this.parentCodeFrame = parentCodeFrame;
this.row = row;
this.completionCalculator = completionCalculator;
this.hints = hints;
topLevel = newInfix(editor, new ModificationToken());
effectivelyFocusedProperty = focusedProperty.or(fakeCaretShowing);
JavaFXUtil.addChangeListener(textMirror, t -> {
if (!editor.isLoading())
{
modified();
}
else
{
parentFrame.trackBlank();
}
});
this.overlay = row.getOverlay();
overlay.addExtraRedraw(gc -> {
gc.save();
if (selectionDrawPositions != null && !selectionDrawPositions.isEmpty())
{
gc.setFill(editor.getHighlightColor());
for (Line l : TextOverlayPosition.groupIntoLines(selectionDrawPositions)) {
l.transform(overlay::sceneToLocal);
gc.fillRect(l.startX + 1.0
| fudge factor
|
l.topY, l.endX - l.startX, l.bottomY - l.topY);
}
}
if (fakeCaretShowing.get())
{
gc.setStroke(Color.BLACK);
double x = overlay.sceneToLocal(topLevel.calculateOverlayPos(suggestionLocation).getSceneX(), 0).getX();
gc.strokeLine(x, 0, x, suggestionField.heightProperty().get());
}
gc.restore();
});
JavaFXUtil.addChangeListener(fakeCaretShowing, b -> JavaFXUtil.runNowOrLater(() -> overlay.redraw()));
}
@OnThread(Tag.FXPlatform)
@Override
public void flagErrorsAsOld()
{
allErrors.forEach(CodeError::flagAsOld);
}
@OnThread(Tag.FXPlatform)
@Override
public void removeOldErrors()
{
allErrors.removeIf(CodeError::isFlaggedAsOld);
recalculateShownErrors();
}
@OnThread(Tag.FXPlatform)
private void recalculateShownErrors()
{
shownErrors.clear();
List<CodeError> sortedErrors = allErrors.stream()
.sorted((a, b) -> CodeError.compareErrors(a, b)).collect(Collectors.toList());
sortedErrors.forEach(e -> {
if (shownErrors.stream().allMatch(shown -> !shown.overlaps(e)))
{
shownErrors.add(e);
e.setShowingIndicator(true);
}
else
{
e.setShowingIndicator(false);
}
});
clearErrorMarkers();
shownErrors.forEach(e -> drawErrorMarker(e.getStartPosition(), e.getEndPosition(), e.isJavaPos(), b -> showErrorHover(b ? e : null), e.visibleProperty()));
CaretPos curPos = topLevel.getCurrentPos();
if (curPos != null)
JavaFXUtil.runNowOrLater(() -> showErrorAtCaret(curPos));
}
@OnThread(Tag.FXPlatform)
private void showErrorHover(CodeError error)
{
if (errorAndFixDisplay != null)
{
if (error != null && errorAndFixDisplay.getError().equals(error)){
hoverErrorCurrentlyShown = error;
return;
}
CaretPos caretPos = topLevel.getCurrentPos();
if (caretPos != null)
{
final int caretPosition = topLevel.caretPosToStringPos(caretPos, true);
Optional<CodeError> errorAtCaret = shownErrors.stream()
.filter(e -> e.getStartPosition() <= caretPosition && caretPosition <= e.getEndPosition())
.findFirst();
if (error == null && errorAtCaret.isPresent() && errorAtCaret.get().equals(errorAndFixDisplay.getError())){
hoverErrorCurrentlyShown = null;
return;
}
}
errorAndFixDisplay.hide();
errorAndFixDisplay = null;
}
if (error != null && error.visibleProperty().get())
{
hoverErrorCurrentlyShown = error;
errorAndFixDisplay = new ErrorAndFixDisplay(editor, error, this);
errorAndFixDisplay.showBelow((Region)error.getRelevantNode(), Duration.ZERO);
}
}
@OnThread(Tag.FXPlatform)
private void showErrorAtCaret(CaretPos curPos)
{
if (curPos == null) {
if (errorAndFixDisplay != null)
{
errorAndFixDisplay.hide();
errorAndFixDisplay = null;
}
return;
}
int caretPosition = topLevel.caretPosToStringPos(curPos, true);
Optional<CodeError> errorAtCaret = shownErrors.stream()
.filter(e -> e.getStartPosition() <= caretPosition && caretPosition <= e.getEndPosition())
.findFirst();
if (errorAtCaret.isPresent() && errorAndFixDisplay != null && errorAndFixDisplay.getError().equals(errorAtCaret.get())) {
return;
}
if (errorAndFixDisplay != null) {
errorAndFixDisplay.hide();
errorAndFixDisplay = null;
}
if (errorAtCaret.isPresent() && errorAtCaret.get().visibleProperty().get()) {
errorAndFixDisplay = new ErrorAndFixDisplay(editor, errorAtCaret.get(), this);
errorAndFixDisplay.showBelow(topLevel.getNodeForPos(curPos));
}
}
@OnThread(Tag.FXPlatform)
public void addError(CodeError err)
{
allErrors.add(err);
err.bindFresh(getFreshExtra(err).or(getParentFrame().freshProperty()), editor);
recalculateShownErrors();
}
protected BooleanExpression getFreshExtra(CodeError err)
{
return new ReadOnlyBooleanWrapper(false);
}
@Override
@OnThread(Tag.FXPlatform)
public void fixedError(CodeError err)
{
allErrors.remove(err);
recalculateShownErrors();
}
@OnThread(Tag.FXPlatform)
public void drawErrorMarker(int startPos, int endPos, boolean javaPos, FXPlatformConsumer<Boolean> onHover, ObservableBooleanValue visible)
{
if ((startPos == 0 && endPos == 0) || getText().length() == 0)
overlay.addErrorMarker(this, 0, Integer.MAX_VALUE, false, onHover, visible);
else{ overlay.addErrorMarker(this, startPos, endPos, javaPos, onHover, visible);
}
}
protected abstract SLOT_FRAGMENT makeSlotFragment(String content, String javaCode);
public SLOT_FRAGMENT getSlotElement()
{
if (slotElement == null || beenModified) {
slotElement = makeSlotFragment(getText(), getJavaCode());
beenModified = false;
}
return slotElement;
}
@OnThread(Tag.FXPlatform)
public void clearErrorMarkers()
{
overlay.clearErrorMarkers(this);
}
public void setSimplePromptText(String t)
{
FXRunnable action = () -> {
topLevel.withContent((fields, ops) -> {
if (ops.isEmpty() && fields.size() == 1 && fields.get(0) instanceof StructuredSlotField)
((StructuredSlotField)fields.get(0)).setPromptText(t);
else if (fields.size() > 0)
((StructuredSlotField)fields.get(0)).setPromptText("");
});
};
afterModify.add(action);
action.run();
}
public void setMethodCallPromptText(String t)
{
FXRunnable action = () -> {
topLevel.withContent((fields, ops) -> {
if (ops.size() == 0 || fields.size() == 0)
return;
for (int i = 0; i < fields.size() - 1
| Don't look at last field
|
i++)
{
if (fields.get(i) instanceof StructuredSlotField)
{
StructuredSlotField f = (StructuredSlotField)fields.get(i);
if (ops.get(i) == null && fields.get(i + 1) instanceof BracketedStructured)
{
if (i == 0 || (ops.get(i - 1) != null && ops.get(i - 1).get().equals(".")))
{
f.setPromptText(t);
}
}
}
}
});
};
afterModify.add(action);
action.run();
}
public void onTextPropertyChange(FXConsumer<String> listener)
{
JavaFXUtil.addChangeListener(textMirror, listener);
}
public void onTextPropertyChangeOld(FXBiConsumer<String, String> listener)
{
textMirror.addListener((a, oldVal, newVal) -> listener.accept(oldVal, newVal));
}
public String getText()
{
return topLevel.getCopyText(null, null);
}
@Override
public boolean isFocused()
{
return topLevel.isFocused();
}
@Override
public int getFocusInfo()
{
CaretPos pos = topLevel.getCurrentPos();
if (pos == null)
pos = mostRecentPos;
if (pos == null)
return 0;
else{ return topLevel.caretPosToStringPos(pos, false);
}
}
@Override
public Node recallFocus(int info)
{
return topLevel.positionCaret(topLevel.stringPosToCaretPos(info, false));
}
@Override
@OnThread(Tag.FXPlatform)
public Stream getCurrentErrors()
{
return shownErrors.stream();
}
@OnThread(Tag.FXPlatform)
public void setText(String text)
{
modificationPlatform(token -> {
topLevel.blank(token);
if (!"".equals(text))
{
topLevel.insert(topLevel.getFirstField(), 0, text);
}
});
}
@Override
public void focusAndPositionAtError(CodeError err)
{
requestFocus();
topLevel.positionCaret(javaPosToCaretPos(err.getStartPosition()));
}
@Override
@OnThread(Tag.FXPlatform)
public void addUnderline(Underline u)
{
underlines.add(u);
drawUnderlines();
}
@Override
@OnThread(Tag.FXPlatform)
public void removeAllUnderlines()
{
underlines.clear();
drawUnderlines();
}
@OnThread(Tag.FXPlatform)
private void drawUnderlines()
{
overlay.clearUnderlines();
underlines.forEach(u -> overlay.addUnderline(this, u.getStartPosition(), u.getEndPosition(), u.getOnClick()));
}
@Override
public void cleanup()
{
if (suggestionDisplay != null)
hideSuggestionDisplay();
if (errorAndFixDisplay != null)
{
final ErrorAndFixDisplay errorAndFixDisplayToHide = this.errorAndFixDisplay;
JavaFXUtil.runNowOrLater(() -> errorAndFixDisplayToHide.hide());
this.errorAndFixDisplay = null;
}
JavaFXUtil.runNowOrLater(() -> {
shownErrors.clear();
clearErrorMarkers();
overlay.clearUnderlines();
});
}
@OnThread(Tag.FXPlatform)
public void replace(int startPosInSlot, int endPosInSlot, boolean javaPos, String s)
{
if (javaPos)
{
startPosInSlot = topLevel.caretPosToStringPos(javaPosToCaretPos(startPosInSlot), false);
endPosInSlot = topLevel.caretPosToStringPos(javaPosToCaretPos(endPosInSlot), false);
}
String prev = getText();
String updated = prev.substring(0, startPosInSlot) + s + prev.substring(endPosInSlot);
setText(updated);
}
|
|
|@Override
|
|public int getSlotElementPositionInLine()
|
|{} return topLevel.getSlotElement().getPositionInLine();
|
|}
|
|@Override
|
|public boolean hasSlotElementPosition()
|
|{} return topLevel.getSlotElement().positionRecorded();
|
|}
@Override
public void requestFocus(Focus on)
{
topLevel.requestFocus();
if (on == Focus.LEFT) {
topLevel.home(null);
}
else if (on == Focus.RIGHT) {
topLevel.end();
}
else if (on == Focus.SELECT_ALL) {
topLevel.selectAll(null);
}
}
public boolean isEmpty()
{
return topLevel.isEmpty();
}
protected void modified()
{
beenModified = true;
editor.modifiedFrame(parentFrame, false);
JavaFXUtil.runNowOrLater(() -> editor.afterRegenerateAndReparse(null));
}
p.public double sceneToOverlayX(double sceneX)
{
return overlay.sceneToLocal(sceneX, 0.0).getX();
}
p.public double sceneToOverlayY(double sceneY)
{
return overlay.sceneToLocal(0.0, sceneY).getY();
}
p.public void clearSelection(boolean invalidateErrors)
{
selectionDrawPositions = null;
JavaFXUtil.runNowOrLater(() -> {
if (invalidateErrors)
{
clearErrorMarkers();
}
overlay.redraw();
});
}
void drawSelection(List<TextOverlayPosition> positions)
{
selectionDrawPositions = positions;
JavaFXUtil.runNowOrLater(overlay::redraw);
}
public void bindTargetType(StringExpression targetTypeBinding)
{
this.targetType = targetTypeBinding;
}
public void setTargetType(String targetType)
{
this.targetType = new SimpleStringProperty(targetType);
}
public static Label makeBracket(String content, boolean opening, InfixStructured parent)
{
Label l = new Label(content);
JavaFXUtil.addStyleClass(l, "expression-bracket", opening ? "expression-bracket-opening" : "expression-bracket-closing");
l.setOnMousePressed(e -> {
if (parent != null)
parent.moveTo(e.getSceneX(), e.getSceneY(), true);
e.consume();
});
l.setOnMouseMoved(e -> { if (e.isShortcutDown()) parent.getSlot().getOverlay().hoverAtPos(-1);
});
l.setOnMouseReleased(MouseEvent::consume);
l.setOnMouseClicked(MouseEvent::consume);
l.setOnMouseDragged(MouseEvent::consume);
l.setOnDragDetected(MouseEvent::consume);
return l;
}
public static SlotLabel makeBracketSlot(String content, boolean opening, InfixStructured parent)
{
SlotLabel l = new SlotLabel(content);
JavaFXUtil.addStyleClass(l, "expression-bracket", opening ? "expression-bracket-opening" : "expression-bracket-closing");
l.setOnMousePressed(e -> {
if (parent != null)
parent.moveTo(e.getSceneX(), e.getSceneY(), true);
e.consume();
});
l.setOnMouseReleased(MouseEvent::consume);
l.setOnMouseClicked(MouseEvent::consume);
l.setOnMouseDragged(MouseEvent::consume);
l.setOnDragDetected(MouseEvent::consume);
return l;
}
void hideSuggestionDisplay()
{
if (suggestionDisplay != null) {
suggestionDisplay = null;
}
fileCompletions = null;
}
| Shows code completion for this slot (intended for use in new, blank slots)
|
@OnThread(Tag.FXPlatform)
public void showSuggestion()
{
showSuggestionDisplay(topLevel.getFirstField(), 0, false);
}
@OnThread(Tag.FXPlatform)
p.public void showSuggestionDisplay(StructuredSlotField field, int caretPosition, boolean stringLiteral)
{
if (suggestionDisplay != null) {
hideSuggestionDisplay();
}
suggestionField = field;
suggestionNode = field.getComponents().get(0);
FXPlatformConsumer<SuggestionList> withSuggList = suggList -> {
suggestionDisplay = suggList;
updateSuggestions(true);
suggestionDisplay.highlightFirstEligible();
suggestionDisplay.show(suggestionNode, new BoundingBox(0, 0, 0, field.heightProperty().get()));
fakeCaretShowing.set(true);
};
suggestionLocation = topLevel.getCurrentPos();
if (stringLiteral) {
fileCompletions = editor.getAvailableFilenames();
Function<FileCompletion, SuggestionDetails> func = f -> new SuggestionDetailsWithCustomDoc(f.getFile().getName(), null, f.getType(), SuggestionList.SuggestionShown.COMMON, () -> makeFileCompletionPreview(f)) {
@Override
public boolean hasDocs()
{
return Config.isGreenfoot();
}
};
withSuggList.accept(new SuggestionList(editor, Utility.mapList(fileCompletions, func), null, SuggestionList.SuggestionShown.RARE, null, StructuredSlot.this));
}
else {
currentlyCompleting = true;
editor.afterRegenerateAndReparse(() -> {
final int stringPos = topLevel.caretPosToStringPos(topLevel.getCurrentPos(), true);
PosInSourceDoc posInFile = getSlotElement().getPosInSourceDoc(stringPos);
completionCalculator.withCalculatedSuggestionList(posInFile, this.asExpressionSlot(), parentCodeFrame.getCode(), StructuredSlot.this, (targetType == null
| || not at start getStartOfCurWord() != 0
|
? null : targetType.get(), field == topLevel.getFirstField(), suggList -> {
editor.recordCodeCompletionStarted(getSlotElement(), stringPos, field.getText().substring(0, caretPosition), suggList.getRecordingId());
withSuggList.accept(suggList);
});
});
}
}
private Pane makeFileCompletionPreview(FileCompletion fc)
{
Pane javadocDisplay = new BorderPane(fc.getPreview(300, 250));
JavaFXUtil.addStyleClass(javadocDisplay, "suggestion-file-preview");
CodeOverlayPane.setDropShadow(javadocDisplay);
fileCompletionShortcuts = fc.getShortcuts();
return javadocDisplay;
}
private String getCurSuggestionWord()
{
if (suggestionLocation == null)
return null;
else{ return topLevel.getCopyText(replaceLastWithZero(suggestionLocation), suggestionLocation);
}
}
private static CaretPos replaceLastWithZero(CaretPos p)
{
if (p.subPos == null)
return new CaretPos(0, null);
else{ return new CaretPos(p.index, replaceLastWithZero(p.subPos));
}
}
@OnThread(Tag.FXPlatform)
private void updateSuggestions(boolean initialState)
{
if (suggestionDisplay != null)
{
String prefix = getCurSuggestionWord();
if (prefix == null)
{
hideSuggestionDisplay();
}
else
{
suggestionDisplay.calculateEligible(prefix, true, initialState);
suggestionDisplay.updateVisual(prefix);
}
}
}
@OnThread(Tag.FXPlatform)
private void executeSuggestion(int selected, ModificationToken token, int codeCompletionId)
{
if (selected == -1)
return;
String name;
List<String> params;
char opening;
if (fileCompletions != null)
{
FileCompletion fc = fileCompletions.get(selected);
name = fc.getFile().getName();
params = null;
opening = '\0';
}
else
{
name = completionCalculator.getName(selected);
params = completionCalculator.getParams(selected);
opening = completionCalculator.getOpening(selected);
}
topLevel.insertSuggestion(suggestionLocation, name, opening, params, token);
modified();
String completion = name + (params == null ? "" : "(" + params.stream().collect(Collectors.joining(",")) + ")");
editor.recordCodeCompletionEnded(getSlotElement(), topLevel.caretPosToStringPos(suggestionLocation, false), getCurSuggestionWord(), completion, codeCompletionId);
}
void up()
{
if (errorAndFixDisplay != null && errorAndFixDisplay.hasFixes() && errorAndFixDisplay.isShowing())
{
errorAndFixDisplay.up();
}
else
{
List<TextOverlayPosition> overlayPositions = topLevel.getAllStartEndPositionsBetween(null, null).collect(Collectors.toList());
TextOverlayPosition cur = topLevel.calculateOverlayPos(topLevel.getCurrentPos());
List<Line> lines = TextOverlayPosition.groupIntoLines(overlayPositions);
int curLine = -1;
for (int i = 0; i < lines.size();i++)
{
if (lines.get(i).topY <= cur.getSceneTopY() && lines.get(i).bottomY >= cur.getSceneBottomY())
{
curLine = i;
break;
}
}
if (curLine > 0)
{
double nearestDist = 9999.0;
StructuredSlotField nearest = null;
for (int i = 0; i < lines.get(curLine - 1).positions.size(); i++)
{
TextOverlayPosition p = lines.get(curLine - 1).positions.get(i);
double dist = Math.abs(p.getSceneX() - cur.getSceneX());
if (dist < nearestDist && p.getSource() != null)
{
nearestDist = dist;
nearest = p.getSource();
}
}
if (nearest != null)
{
nearest.focusAtPos(nearest.getNearest(cur.getSceneX(), cur.getSceneTopY(), false, false).getPos());
return;
}
}
row.focusUp(this, false);
}
}
void down()
{
if (errorAndFixDisplay != null && errorAndFixDisplay.hasFixes() && errorAndFixDisplay.isShowing())
{
errorAndFixDisplay.down();
}
else
{
List<TextOverlayPosition> overlayPositions = topLevel.getAllStartEndPositionsBetween(null, null).collect(Collectors.toList());
TextOverlayPosition cur = topLevel.calculateOverlayPos(topLevel.getCurrentPos());
List<Line> lines = TextOverlayPosition.groupIntoLines(overlayPositions);
int curLine = -1;
for (int i = 0; i < lines.size();i++)
{
if (lines.get(i).topY <= cur.getSceneTopY() && lines.get(i).bottomY >= cur.getSceneBottomY())
{
curLine = i;
break;
}
}
if (curLine < lines.size() - 1)
{
double nearestDist = 9999.0;
StructuredSlotField nearest = null;
for (int i = 0; i < lines.get(curLine + 1).positions.size(); i++)
{
TextOverlayPosition p = lines.get(curLine + 1).positions.get(i);
double dist = Math.abs(p.getSceneX() - cur.getSceneX());
if (dist < nearestDist && p.getSource() != null)
{
nearestDist = dist;
nearest = p.getSource();
}
}
if (nearest != null)
{
nearest.focusAtPos(nearest.getNearest(cur.getSceneX(), cur.getSceneBottomY(), false, false).getPos());
return;
}
}
row.focusDown(this);
}
}
@OnThread(Tag.FXPlatform)
void enter()
{
if (errorAndFixDisplay != null && errorAndFixDisplay.hasFixes())
{
errorAndFixDisplay.executeSelected();
}
else
{
row.focusEnter(this);
}
}
public String getJavaCode()
{
if (topLevel.isCurlyLiteral())
return getCurlyLiteralPrefix() + topLevel.getJavaCode();
else{ return topLevel.getJavaCode();
}
}
@OnThread(Tag.FXPlatform)
public void caretMoved()
{
CaretPos pos = topLevel.getCurrentPos();
showErrorAtCaret(pos);
topLevel.showHighlightedBrackets(null, pos);
if (pos != null)
mostRecentPos = pos;
}
@OnThread(Tag.FXPlatform)
public void escape()
{
if (errorAndFixDisplay != null)
{
errorAndFixDisplay.hide();
}
else
{
row.escape(this);
}
}
@Override
public ObservableList getComponents()
{
return topLevel.getComponents();
}
@Override
public TextOverlayPosition getOverlayLocation(int stringCaretPos, boolean javaPos)
{
CaretPos p;
if (stringCaretPos == Integer.MAX_VALUE)
return topLevel.calculateOverlayEnd();
else if (stringCaretPos < 0)
p = topLevel.getStartPos();
else
{
p = javaPos ? javaPosToCaretPos(stringCaretPos) : topLevel.stringPosToCaretPos(stringCaretPos, false);
if (p == null)
p = topLevel.getEndPos();
}
return topLevel.calculateOverlayPos(p);
}
public List getAllLines(int start, int end, boolean javaPos)
{
CaretPos startPos = topLevel.stringPosToCaretPos(start, javaPos);
CaretPos endPos;
if (end == Integer.MAX_VALUE)
{
endPos = null;
}
else
{
endPos = topLevel.stringPosToCaretPos(end, javaPos);
}
return TextOverlayPosition.groupIntoLines(topLevel.getAllStartEndPositionsBetween(startPos, endPos).collect(Collectors.toList()));
}
p.public CaretPos javaPosToCaretPos(int pos)
{
if (topLevel.isCurlyLiteral())
pos -= getCurlyLiteralPrefix().length();
return topLevel.stringPosToCaretPos(Math.max(0, pos), true);
}
public InfixStructured getTopLevel()
{
return topLevel;
}
FocusParent<HeaderItem> getSlotParent()
{
return row;
}
| Returns true if the method has transferred focus out of the slot
|
@OnThread(Tag.FXPlatform)
public boolean backspaceAtStart()
{
return row.backspaceAtStart(this);
}
public void addClosingChar(char c)
{
topLevel.addClosingChar(c);
}
public boolean checkFilePreviewShortcut(KeyCode code)
{
if (fileCompletionShortcuts != null && fileCompletionShortcuts.containsKey(code))
{
fileCompletionShortcuts.get(code).run();
return true;
}
return false;
}
@OnThread(Tag.FXPlatform)
public boolean isShowingSuggestions()
{
return suggestionDisplay != null && suggestionDisplay.isShowing() && !suggestionDisplay.isInMiddleOfHiding();
}
public abstract List extends PossibleLink> findLinks();
@Override
public void lostFocus()
{
if (focusedProperty.get())
{
recentValues.removeAll(getText());
recentValues.removeAll(valueOnGain);
recentValues.add(0, valueOnGain);
while (recentValues.size() > 3)
recentValues.remove(3);
}
editor.endRecordingState(this);
topLevel.getAllExpressions().forEach(InfixStructured::deselect);
notifyLostFocusExcept(null);
lostFocusActions.forEach(FXRunnable::run);
}
focusedProperty.set(false);
}
p.public void notifyGainFocus(StructuredSlotField focus)
{
notifyLostFocusExcept(focus);
if (!focusedProperty.get())
{
valueOnGain = getText();
editor.beginRecordingState(this);
}
focusedProperty.set(true);
}
private void notifyLostFocusExcept(StructuredSlotField except)
{
topLevel.getAllExpressions().forEach(e -> e.notifyLostFocus(except));
}
public void onLostFocus(FXRunnable action)
{
lostFocusActions.add(action);
}
public void addFocusListener(Frame frame)
{
onLostFocus(frame::checkForEmptySlot);
}
ErrorUnderlineCanvas getOverlay()
{
return overlay;
}
@Override
public Frame getParentFrame()
{
return parentFrame;
}
public List findPlainVarReferences(String name)
{
return topLevel.findPlainVarUse(name);
}
public String getCurlyLiteralPrefix()
{
return "";
}
@Override
public void setView(View oldView, View newView, SharedTransition animate)
{
topLevel.setView(oldView, newView, animate, Optional.empty());
}
public boolean isConstantRange()
{
return topLevel.checkRangeExpression() == RangeType.RANGE_CONSTANT;
}
@Override
public Map getMenuItems(boolean contextMenu)
{
HashMap<TopLevelMenu, MenuItems> itemMap = new HashMap<>();
final Menu recentMenu = new Menu(Config.getString("frame.slot.recent"));
recentMenu.setDisable(true);
recentValues.addListener((ListChangeListener)c -> {
recentMenu.getItems().clear();
if (recentValues.isEmpty())
{
recentMenu.setDisable(true);
}
else
{
recentMenu.setDisable(false);
recentValues.forEach(v -> {
MenuItem item = new MenuItem(v);
item.setOnAction(e -> {
editor.recordEdits(StrideEditReason.FLUSH);
setText(v);
modified();
editor.recordEdits(StrideEditReason.UNDO_LOCAL);
});
recentMenu.getItems().add(item);
});
}
});
final ObservableList<SortedMenuItem> originalItems = FXCollections.observableArrayList();
final FXConsumer<ObservableList<SortedMenuItem>> setToOriginal = l -> {
if (contextMenu)
l.setAll(MenuItemOrder.RECENT_VALUES.item(recentMenu));
else{ l.clear();
}
};
setToOriginal.accept(originalItems);
itemMap.put(TopLevelMenu.EDIT, new MenuItems(originalItems) {
@OnThread(Tag.FXPlatform)
public void onShowing()
{
Stream<InfixStructured<?, ?>> allExpressions = getTopLevel().getAllExpressions();
InfixStructured exp = allExpressions.filter(i -> i.isFocused()).findFirst().orElse(null);
if (exp == null)
{
setToOriginal.accept(items);
}
else
{
MenuItem cut = JavaFXUtil.makeMenuItem(Config.getString("frame.slot.cut"), exp::cut, new KeyCodeCombination(KeyCode.X, KeyCodeCombination.SHORTCUT_DOWN));
MenuItem copy = JavaFXUtil.makeMenuItem(Config.getString("frame.slot.copy"), exp::copy, new KeyCodeCombination(KeyCode.C, KeyCodeCombination.SHORTCUT_DOWN));
MenuItem paste = JavaFXUtil.makeMenuItem(Config.getString("frame.slot.paste"), exp::paste, Config.isMacOS() ? null : new KeyCodeCombination(KeyCode.V, KeyCodeCombination.SHORTCUT_DOWN));
boolean inSelection = exp.isInSelection();
cut.setDisable(!inSelection);
copy.setDisable(!inSelection);
paste.setDisable(!Clipboard.getSystemClipboard().hasString());
setToOriginal.accept(items);
items.addAll(
MenuItemOrder.CUT.item(cut),
MenuItemOrder.COPY.item(copy),
MenuItemOrder.PASTE.item(paste)
);
}
if (hoverErrorCurrentlyShown != null ){
errorAndFixDisplay.hide();
}
}
});
if (contextMenu)
{
final SortedMenuItem scanningItem = MenuItemOrder.GOTO_DEFINITION.item(new MenuItem("Scanning..."));
scanningItem.getItem().setDisable(true);
itemMap.put(TopLevelMenu.VIEW, new MenuItems(FXCollections.observableArrayList())
{
private void removeScanning()
{
if (items.size() == 1 && items.get(0) == scanningItem)
items.clear();
}
@OnThread(Tag.FXPlatform)
public void onShowing()
{
items.setAll(scanningItem);
CaretPos caretPos = getTopLevel().getCurrentPos();
FXPlatformConsumer<Optional<LinkedIdentifier>> withLink = optLink -> {
removeScanning();
optLink.ifPresent(defLink -> {
items.add(MenuItemOrder.GOTO_DEFINITION.item(JavaFXUtil.makeMenuItem(Config.getString("frame.slot.goto")
.replace("$", defLink.getName()), defLink.getOnClick(), null)));
});
};
withLinksAtPos(caretPos, withLink);
JavaFXUtil.runAfter(Duration.millis(1000), this::removeScanning);
}
public void onHidden()
{
items.clear();
}
});
}
return itemMap;
}
@OnThread(Tag.FXPlatform)
p.public void withLinksAtPos(CaretPos caretPos, FXPlatformConsumer<Optional<LinkedIdentifier>> withLink)
{
List<? extends PossibleLink> possibleLinks = findLinks();
possibleLinks.removeIf(possLink -> {
CaretPos startCaretPos = javaPosToCaretPos(possLink.getStartPosition());
CaretPos endCaretPos = javaPosToCaretPos(possLink.getEndPosition());
return !CaretPos.between(startCaretPos, endCaretPos, caretPos);
});
possibleLinks.forEach(possLink -> editor.searchLink(possLink, withLink));
}
@Override
@OnThread(Tag.FXPlatform)
public Response suggestionListKeyPressed(SuggestionList suggestionList, KeyEvent event, int highlighted)
{
switch (event.getCode())
{
case ENTER:
if (highlighted != -1)
{
modificationPlatform(token -> executeSuggestion(highlighted, token, suggestionList.getRecordingId()));
return Response.DISMISS;
}
case ESCAPE:
return Response.DISMISS;
case BACK_SPACE:
CaretPos updatedLocation = modificationReturnPlatform(token -> topLevel.deletePreviousAtPos(suggestionLocation, token));
if (updatedLocation == null || !updatedLocation.init().equals(suggestionLocation.init()))
{
JavaFXUtil.runAfterCurrent(() -> topLevel.positionCaret(updatedLocation));
return Response.DISMISS;
}
else
{
suggestionLocation = updatedLocation;
overlay.redraw();
updateSuggestions(false);
return Response.CONTINUE;
}
case TAB:
if (event.isShiftDown())
row.focusLeft(StructuredSlot.this);
else
{
row.focusRight(StructuredSlot.this);
completeIfPossible(highlighted, suggestionList);
}
return Response.DISMISS;
}
return Response.CONTINUE;
}
@OnThread(Tag.FXPlatform)
private void completeIfPossible(int highlighted, SuggestionList suggestionList)
{
if (highlighted != -1)
{
modificationPlatform(token -> executeSuggestion(highlighted, token, suggestionList.getRecordingId()));
}
else if (suggestionDisplay.eligibleCount() == 1 && getText().length() > 0)
{
modificationPlatform(token -> executeSuggestion(suggestionDisplay.getFirstEligible(), token, suggestionList.getRecordingId()));
}
}
@OnThread(Tag.FXPlatform)
@Override
public Response suggestionListKeyTyped(SuggestionList suggestionList, KeyEvent event, int highlighted)
{
return modificationReturnPlatform(token -> {
CaretPos updatedLocation = null;
if (!"\b".equals(event.getCharacter()))
{
updatedLocation = topLevel.insertAtPos(suggestionLocation, event.getCharacter(), token);
}
else{ return Response.CONTINUE;
}
if (!updatedLocation.init().equals(suggestionLocation.init()))
{
return Response.DISMISS;
}
else
{
suggestionLocation = updatedLocation;
overlay.redraw();
updateSuggestions(true);
return Response.CONTINUE;
}
});
}
@Override
@OnThread(Tag.FXPlatform)
public void suggestionListChoiceClicked(SuggestionList suggestionList, int highlighted)
{
modificationPlatform(token -> executeSuggestion(highlighted, token, suggestionList.getRecordingId()));
}
@Override
public void hidden()
{
fakeCaretShowing.set(false);
}
@OnThread(Tag.FXPlatform)
public boolean suggestingFor(CaretPos fieldPos)
{
return fieldPos != null && suggestionLocation != null && fieldPos.equals(suggestionLocation.init())
&& suggestionDisplay != null && suggestionDisplay.isShowing();
}
@OnThread(Tag.FXPlatform)
public boolean deleteAtEnd()
{
if (row != null)
{
return row.deleteAtEnd(this);
}
return false;
}
@OnThread(Tag.FXPlatform)
public void setSplitText(String beforeCursor, String afterCursor)
{
modificationPlatform(token -> {
topLevel.blank(token);
CaretPos p = topLevel.insertImpl(topLevel.getFirstField(), 0, beforeCursor, false, token);
topLevel.insertAtPos(p, afterCursor, token);
token.after(() -> topLevel.positionCaret(p));
});
}
public boolean isCurrentlyCompleting()
{
return currentlyCompleting;
}
List<ExtensionDescription> getExtensions()
{
return row.getExtensions();
}
@OnThread(Tag.FXPlatform)
p.public void notifyModifiedPress(KeyCode c)
{
row.notifyModifiedPress(c);
}
void focusNext()
{
row.focusRight(this);
}
public abstract boolean canCollapse();
public static class SplitInfo
{
public final String lhs;
public final String rhs;
public SplitInfo(String lhs, String rhs)
{
this.lhs = lhs; this.rhs = rhs;
}
}
public SplitInfo trySplitOnEquals()
{
return topLevel.trySplitOn("=");
}
public final List getHints()
{
return hints;
}
@Override
public boolean isAlmostBlank()
{
return topLevel.isAlmostBlank();
}
@Override
public boolean isEditable()
{
return editable;
}
@Override
public void setEditable(boolean editable)
{
this.editable = editable;
topLevel.setEditable(editable);
}
@Override
public ObservableBooleanValue effectivelyFocusedProperty()
{
return effectivelyFocusedProperty;
}
@Override
public int calculateEffort()
{
return topLevel.calculateEffort();
}
| Helper pair class for recording references to plain variables: gives
| the graphical node containing the variable, and a callback which will rename
| the use of that variable.
|
public static class PlainVarReference
{
public final FXConsumer<String> rename;
public final Region refNode;
PlainVarReference(FXConsumer<String> rename, Region refNode)
{
this.rename = rename;
this.refNode = refNode;
}
}
protected abstract INFIX newInfix(InteractionManager editor, ModificationToken token);
| Runs a block of code (passed as parameter) which modifies the slot.
|
| Calls to this function can be safely nested, although it should be avoided
| where possible. You should make sure that all related modifications are encompassed
| within a single modificationReturn call, so that the outside world only
| sees a single complete modification.
|
| @param modificationAction A block of code to run which modifies the slot.
| @param <T> The return type of the inner function.
| @return The return value of the inner function.
|
@OnThread(Tag.FX)
<T> T modificationReturn(FXFunction<ModificationToken, T> modificationAction)
{
ModificationToken token = new ModificationToken();
modificationTokens.add(token);
T ret = modificationAction.apply(token);
if (modificationTokens.get(modificationTokens.size() - 1) != token)
throw new IllegalStateException("Modifications did not nest");
modificationTokens.remove(token);
if (modificationTokens.isEmpty())
{
textMirror.set(topLevel.calculateText());
token.runAfters();
afterModify.forEach(FXRunnable::run);
}
else
{
modificationTokens.get(0).afters.addAll(token.afters);
}
return ret;
}
@OnThread(Tag.FXPlatform)
<T> T modificationReturnPlatform(FXPlatformFunction<ModificationToken, T> modificationAction)
{
return modificationReturn((FXFunction<ModificationToken, T>)(modificationAction::apply));
}
void modification(FXConsumer<ModificationToken> modificationAction)
{
modificationReturn(t -> {modificationAction.accept(t);return 0;
});
}
@OnThread(Tag.FXPlatform)
void modificationPlatform(FXPlatformConsumer<ModificationToken> modificationAction)
{
modificationReturnPlatform(t -> {modificationAction.accept(t);return 0;
});
}
static <T> T testingModification(FXFunction<ModificationToken, T> modificationAction)
{
return modificationAction.apply(new ModificationToken());
}
p.public void afterCurrentModification(FXRunnable action)
{
if (modificationTokens.isEmpty())
action.run();
else{ modificationTokens.get(0).after(action);
}
}
| We want to make sure that modifications to the slot's content only
| occur inside the modification/modificationReturn functions above.
| To this end, we require a ModificationToken parameter to any actions
| which modify a structured slot (StructuredSlotField.setText, fields.add, etc).
|
| To get an instance of this class, you must use the modification/modificationReturn
| function rather than constructing one directly. This ensures the post-modification
| actions are run correctly after a complete change.
|
public static class ModificationToken
{
private List<FXRunnable> afters = new ArrayList<>();
private ModificationToken()
{
}
public void check()
{
}
public void after(FXRunnable action)
{
afters.add(action);
}
private void runAfters()
{
afters.forEach(FXRunnable::run);
}
}
}
. StructuredSlot
. flagErrorsAsOld
. removeOldErrors
. recalculateShownErrors
. showErrorHover
. showErrorAtCaret
. addError
. getFreshExtra
. fixedError
. drawErrorMarker
. makeSlotFragment
. getSlotElement
. clearErrorMarkers
. setSimplePromptText
. setMethodCallPromptText
. onTextPropertyChange
. onTextPropertyChangeOld
. getText
. isFocused
. getFocusInfo
. recallFocus
. getCurrentErrors
. setText
. focusAndPositionAtError
. addUnderline
. removeAllUnderlines
. drawUnderlines
. cleanup
. replace
. requestFocus
. isEmpty
. modified
. sceneToOverlayX
. sceneToOverlayY
. clearSelection
. drawSelection
. bindTargetType
. setTargetType
. makeBracket
. makeBracketSlot
. hideSuggestionDisplay
. showSuggestion
. showSuggestionDisplay
. hasDocs
. makeFileCompletionPreview
. getCurSuggestionWord
. replaceLastWithZero
. updateSuggestions
. executeSuggestion
. up
. down
. enter
. getJavaCode
. caretMoved
. escape
. getComponents
. getOverlayLocation
. getAllLines
. javaPosToCaretPos
. getTopLevel
. backspaceAtStart
. addClosingChar
. checkFilePreviewShortcut
. isShowingSuggestions
. findLinks
. lostFocus
. notifyGainFocus
. notifyLostFocusExcept
. onLostFocus
. addFocusListener
. getParentFrame
. findPlainVarReferences
. getCurlyLiteralPrefix
. setView
. isConstantRange
. getMenuItems
. onShowing
. removeScanning
. onShowing
. onHidden
. withLinksAtPos
. suggestionListKeyPressed
. completeIfPossible
. suggestionListKeyTyped
. suggestionListChoiceClicked
. hidden
. suggestingFor
. deleteAtEnd
. setSplitText
. isCurrentlyCompleting
. notifyModifiedPress
. focusNext
. canCollapse
top,
use,
map,
class SplitInfo
. SplitInfo
. trySplitOnEquals
. getHints
. isAlmostBlank
. isEditable
. setEditable
. effectivelyFocusedProperty
. calculateEffort
top,
use,
map,
class PlainVarReference
. newInfix
. modification
. modificationPlatform
. afterCurrentModification
top,
use,
map,
class ModificationToken
. ModificationToken
. check
. after
. runAfters
1975 neLoCode
+ 44 LoComm