package bluej.utility.javafx;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import bluej.stride.framedjava.slots.TextOverlayPosition.Line;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import bluej.stride.framedjava.slots.TextOverlayPosition;
import bluej.stride.slots.EditableSlot;
import threadchecker.OnThread;
import threadchecker.Tag;
| There are several points in the interface where we need to show
| red squiggly error underlines, and/or straight hyperlink underlines
| on top of some other text. This ErrorUnderlineCanvas class manages
| the display of those errors and underlines.
|
public class ErrorUnderlineCanvas
{
| By default, canvases are not resizeable. We need to be resizeable so that
| when we are put in a StackPane in front of another node (our usual use-case),
| we always resize to match the size of the underlying node.
|
private final Canvas canvas;
| A class to hold information about a hyperlink.
|
private static class HyperlinkInfo
{
| More detailed information (see UnderlineInfo)
|
private final UnderlineInfo positionInfo;
| The position of the start of the link, in characters
|
private final int start;
| The position of the end of the link, in characters
|
private final int end;
| The action to take if the user clicks the link
|
private final FXPlatformRunnable onClick;
| Whether the link is currently showing or not
|
private boolean showing = true;
private HyperlinkInfo(UnderlineInfo positionInfo, int start, int end,
FXPlatformRunnable onClick)
{
this.positionInfo = positionInfo;
this.start = start;
this.end = end;
this.onClick = onClick;
}
}
| A class to hold information about an error underline.
|
private static class ErrorInfo
{
| The source slot, used to given more information about the error's position
|
private final UnderlineInfo positionInfo;
| The position of the start of the error underline, in characters
|
private final int start;
| The position of the end of the error underline, in characters
|
private final int end;
| Whether the position is relative to the original Java source,
| or to the Stride source. This matters when you have things like
| the <: operator, which becomes instanceof in the Java source, and thus
| affects the positions differently. Generally, Java positions will
| probably have come from a javac compiler error and Stride positions will
| have come from an early error.
|
private final boolean javaPos;
| The action to execute when the user moves the mouse in (pass true) to
| hover over a link, and when the user moves the mouse out (pass false) again.
|
private final FXPlatformConsumer<Boolean> onHover;
private ErrorInfo(UnderlineInfo slot, int start, int end,
boolean javaPos, FXPlatformConsumer<Boolean> onHover)
{
this.positionInfo = slot;
this.start = start;
this.end = end;
this.javaPos = javaPos;
this.onHover = onHover;
}
}
| More information about the position of underline extents
|
public static interface UnderlineInfo
{
| Given a position (and whether it is a Java position or not),
| gives back the corresponding TextOverlayPosition
|
public TextOverlayPosition getOverlayLocation(int caretPos, boolean javaPos);
| Gets all the lines between two given positions (which may be java positions or not).
|
| Default implementation assumes a single text-line slot, and thus
| just delegates to getOverlayLocation for each end. Override for multi-line slots.
|
default public List getAllLines(int start, int end, boolean javaPos)
{
return TextOverlayPosition.groupIntoLines(Arrays.asList(getOverlayLocation(start, javaPos), getOverlayLocation(end, javaPos)));
}
}
| An error which will be added to the errors list when, and only when,
| the specified boolean property changes. Generally used to add an error
| once a frame becomes non-fresh.
|
private class PendingError implements ChangeListener<Boolean>
{
| The value to listen to for changes. On first change, we will add the error to the errors list.
|
private final ObservableValue<Boolean> prop;
| The error to add to the list once prop changes.
|
private final ErrorInfo error;
public PendingError(ObservableValue<Boolean> prop, ErrorInfo err)
{
this.prop = prop;
this.error = err;
prop.addListener(this);
}
@Override
public void changed(ObservableValue<? extends Boolean> a, Boolean b, Boolean c)
{
errors.add(error);
JavaFXUtil.runNowOrLater(() -> redraw());
cancel();
}
public void cancel()
{
prop.removeListener(this);
}
}
| All the hyper links in the canvas (may not actually be drawn, but are being tracked)
|
private final List<HyperlinkInfo> hyperlinks = new ArrayList<>();
| All the errors in the canvas (all will be drawn)
|
private final List<ErrorInfo> errors = new ArrayList<>();
| Any extra drawing actions to take when redrawing the canvas.
| This includes things like drawing on selection in expression slots,
| or drawing fake carets during code completion.
|
@OnThread(value = Tag.FX, requireSynchronized = true)
private final List<FXConsumer<GraphicsContext>> extraRedraw = new ArrayList<>();
| The pending errors (see PendingError class)
|
private final List<PendingError> pending = new ArrayList<>();
| As soon as the user moves to a position where a hover would do something,
| we start a timer to run the hover-begin action. If this item is non-null, it cancels
| that timer. So if you run this while the timer is ticking, it will stop
| any hover-begin from ever being executed. If you run this after the hover has
| begun, it does nothing, but is harmless.
|
private FXPlatformRunnable cancelBeginHover;
| Once the hover has begun, this gets set to the action which will stop the hover
| (by passing false to it). So when the user moves the mouse away, we run
| this to stop the hover.
|
private FXPlatformConsumer<Boolean> currentHover;
| Creates an ErrorUnderlineCanvas.
|
| @param mouseableContainer The node on which to listen for mouse events relating
| to hover, etc. We can't listen on our own canvas, because we make it mouse
| transparent. (Otherwise the canvas would steal all events away from the underlying
| node we overlay.)
|
public ErrorUnderlineCanvas(Node mouseableContainer)
{
canvas = new ResizableCanvas(this::redraw);
canvas.setMouseTransparent(true);
mouseableContainer.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
synchronized (ErrorUnderlineCanvas.this)
{
if (cancelBeginHover != null)
cancelBeginHover.run();
if (currentHover != null)
{
currentHover.accept(false);
currentHover = null;
}
cancelBeginHover = JavaFXUtil.runAfter(Duration.millis(500), () -> {
hoverAt(e.getSceneX(), e.getSceneY());
});
}
});
mouseableContainer.addEventFilter(MouseEvent.MOUSE_EXITED, e -> {
synchronized (ErrorUnderlineCanvas.this)
{
if (cancelBeginHover != null)
{
cancelBeginHover.run();
cancelBeginHover = null;
}
}
});
}
| Clears the canvas, then redraws all errors, all currently-showing hyperlinks, and any extra redraw actions.
|
@OnThread(Tag.FXPlatform)
public void redraw()
{
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(0.75);
gc.setStroke(Color.RED);
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
for (ErrorInfo error : errors)
{
for (Line line : error.positionInfo.getAllLines(error.start, error.end, error.javaPos))
{
TextOverlayPosition startTOP = line.getStart();
TextOverlayPosition endTOP = line.getEnd();
Point2D start = canvas.sceneToLocal(startTOP.getSceneX(), startTOP.getSceneBaselineY());
Point2D end = canvas.sceneToLocal(endTOP.getSceneX(), endTOP.getSceneBaselineY());
double width = end.getX() - start.getX();
width = Math.max(width, 15.0);
int n = (int) (width / 2) + 1;
double[] xPoints = new double[n + 1];
double[] yPoints = new double[n + 1];
for (int j = 0; j <= n; j++)
{
xPoints[j] = start.getX() + j * 2;
yPoints[j] = start.getY() + 3 * (j % 2);
}
gc.strokePolyline(xPoints, yPoints, n + 1);
}
}
gc.setLineWidth(0.75);
gc.setStroke(Color.BLACK);
for (HyperlinkInfo link : hyperlinks)
{
if (link.showing)
{
TextOverlayPosition startTOP = link.positionInfo.getOverlayLocation(link.start, false);
TextOverlayPosition endTOP = link.positionInfo.getOverlayLocation(link.end, false);
Point2D start = canvas.sceneToLocal(startTOP.getSceneX(), startTOP.getSceneBaselineY());
Point2D end = canvas.sceneToLocal(endTOP.getSceneX(), endTOP.getSceneBaselineY());
gc.strokeLine(start.getX(), start.getY(), end.getX(), end.getY());
}
}
doExtraRedraw(gc);
}
@OnThread(Tag.FXPlatform)
private synchronized void doExtraRedraw(GraphicsContext gc)
{
extraRedraw.forEach(c -> c.accept(gc));
}
| Clears all error markers which originate from the given slot
| (according to the parameter passed to addError, checked by reference equality).
|
| Redraws afterwards.
|
@OnThread(Tag.FXPlatform)
public void clearErrorMarkers(UnderlineInfo origin)
{
for (int i = 0; i < errors.size();)
{
if (errors.get(i).positionInfo == origin)
{
errors.remove(i);
}
else{ i += 1;
}
}
pending.forEach(PendingError::cancel);
pending.clear();
redraw();
}
| Draws a red underline between the two given caret positions. Will remain
| until clearErrorMarkers() is called with the same origin parameter.
| @param start The start position
| @param end The end position
| @param javaPos Whether this is a Java position or not
| @param onHover Will be passed true to start the hover display, false again to stop it
| @param visible The property tracking whether the error should be visible yet.
| If false, error will only be added once it turns true (but then
| will remain visible forever after, even if it changes back to false)
|
@OnThread(Tag.FXPlatform)
public void addErrorMarker(UnderlineInfo origin, int start, int end, boolean javaPos, FXPlatformConsumer<Boolean> onHover, ObservableBooleanValue visible)
{
ErrorInfo err = new ErrorInfo(origin, start, end, javaPos, onHover);
if (visible.get() == false)
{
pending.add(new PendingError(visible, err));
}
else
{
errors.add(err);
redraw();
}
}
| Removes all hyperlinks (but does not touch errors), and redraws.
|
@OnThread(Tag.FXPlatform)
public void clearUnderlines()
{
hyperlinks.clear();
redraw();
}
| Adds an underline and redraws.
|
| @param info More information, see UnderlineInfo
| @param startPosition Start of hyperlink
| @param endPosition End of hyperlink
| @param onClick Action to run if the hyperlink is clicked.
|
@OnThread(Tag.FXPlatform)
public void addUnderline(UnderlineInfo info, int startPosition, int endPosition, FXPlatformRunnable onClick)
{
hyperlinks.add(new HyperlinkInfo(info, startPosition, endPosition, onClick));
redraw();
}
| Checks if there is a link at the given X position (assuming a one-line canvas)
| If there is, returns the action the link would trigger. If not, returns null.
|
public FXPlatformRunnable linkFromX(double sceneX)
{
for (HyperlinkInfo link : hyperlinks)
{
double startX = link.positionInfo.getOverlayLocation(link.start, false).getSceneX();
double endX = link.positionInfo.getOverlayLocation(link.end, false).getSceneX();
if (sceneX >= startX && sceneX < endX)
{
return link.onClick;
}
}
return null;
}
| Tells the overlay where the mouse hover is. Only hyperlinks at this position are set to be drawn,
| others are left out
| @param pos position closest to mouse cursor
| @return The action to perform if clicked
|
public FXPlatformRunnable hoverAtPos(int pos)
{
FXPlatformRunnable r = null;
for (HyperlinkInfo link : hyperlinks)
{
link.showing = false;
if (pos >= link.start && pos <= link.end)
{
if (r == null)
{
r = link.onClick;
link.showing = true;
}
}
}
Platform.runLater(this::redraw);
return r;
}
| Adds an extra action to run whenever this canvas redraws.
| The action will take place after all errors and hyperlinks are drawn.
| Extra redraw actions are executed in the order in which they are added.
|
@OnThread(Tag.FX)
public synchronized void addExtraRedraw(FXConsumer<GraphicsContext> redraw)
{
extraRedraw.add(redraw);
}
@OnThread(Tag.FXPlatform)
private synchronized void hoverAt(double sceneX, double sceneY)
{
for (ErrorInfo error : errors)
{
double left = error.positionInfo.getOverlayLocation(error.start, error.javaPos).getSceneX();
double right = error.positionInfo.getOverlayLocation(error.end, error.javaPos).getSceneX();
double top = error.positionInfo.getOverlayLocation(error.start, error.javaPos).getSceneTopY();
double bottom = error.positionInfo.getOverlayLocation(error.end, error.javaPos).getSceneBottomY();
if (top <= sceneY && sceneY <= bottom && left <= sceneX && sceneX <= right)
{
error.onHover.accept(true);
currentHover = error.onHover;
}
}
}
public Node getNode()
{
return canvas;
}
public Point2D localToScene(double x, double y)
{
return canvas.localToScene(x, y);
}
public Point2D sceneToLocal(double x, double y)
{
return canvas.sceneToLocal(x, y);
}
public Point2D sceneToLocal(Point2D p)
{
return canvas.sceneToLocal(p);
}
public double getHeight()
{
return canvas.getHeight();
}
}
top,
use,
map,
class ErrorUnderlineCanvas
top,
use,
map,
class ErrorUnderlineCanvas . HyperlinkInfo
. HyperlinkInfo
top,
use,
map,
class ErrorUnderlineCanvas . HyperlinkInfo . ErrorInfo
. ErrorInfo
top,
use,
map,
interface ErrorUnderlineCanvas . HyperlinkInfo . ErrorInfo . UnderlineInfo
. getOverlayLocation
. getAllLines
. PendingError
. changed
. cancel
. ErrorUnderlineCanvas
. redraw
. doExtraRedraw
. clearErrorMarkers
. addErrorMarker
. clearUnderlines
. addUnderline
. linkFromX
. hoverAtPos
. addExtraRedraw
. hoverAt
. getNode
. localToScene
. sceneToLocal
. sceneToLocal
. getHeight
475 neLoCode
+ 83 LoComm