This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 157bdb055f32d2dafbd86bd0261da54285b74c60 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Oct 30 21:47:55 2021 +0200 Add visual indications about seek operations. --- .../apache/sis/internal/gui/io/FileAccessItem.java | 178 ++++++++++++++++++--- 1 file changed, 153 insertions(+), 25 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java index 8586781..133b17b 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java @@ -23,11 +23,17 @@ import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import javafx.application.Platform; import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.event.ActionEvent; import javafx.scene.Node; +import javafx.scene.Group; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; +import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; import javafx.scene.shape.StrokeType; +import javafx.animation.FadeTransition; +import javafx.util.Duration; import org.apache.sis.measure.Range; import org.apache.sis.util.collection.RangeSet; @@ -41,7 +47,7 @@ import org.apache.sis.util.collection.RangeSet; * @since 1.2 * @module */ -final class FileAccessItem implements Runnable { +final class FileAccessItem implements Runnable, EventHandler<ActionEvent> { /** * The height of bars in pixels. */ @@ -68,6 +74,22 @@ final class FileAccessItem implements Runnable { private static final Color BORDER_COLOR = FILL_COLOR.darker(); /** + * Width of the cursor in pixels. + */ + private static final int CURSOR_WIDTH = 10; + + /** + * The amount of time to keep cursor visible for showing that + * a read or write operation is in progress. + */ + private static final Duration CURSOR_DURATION = Duration.seconds(4); + + /** + * The amount of time to keep seek positions before to let them fade away. + */ + private static final Duration SEEK_DURATION = Duration.minutes(1); + + /** * The list of rows shown by the table. * This is used for removing this item when the file is closed. */ @@ -91,6 +113,29 @@ final class FileAccessItem implements Runnable { final Pane accessView; /** + * The group of rectangles to keep showing after they have been added. + * Some rectangles may be merged together, but the visual effect is that fill area are only added. + */ + private final ObservableList<Node> staticGroup; + + /** + * The group of lines showing seek positions. + */ + private final ObservableList<Node> seeksGroup; + + /** + * An animation containing a rectangle showing the current position of read or write operation. + * The rectangle is drawn on top of {@link #staticView} and fades away after access stopped. + * This field is reset to {@code null} after the animation stopped. + */ + private FadeTransition cursor; + + /** + * Position (in bytes) of the cursor. + */ + private long cursorPosition; + + /** * Size of the file in bytes. */ private long fileSize; @@ -107,10 +152,16 @@ final class FileAccessItem implements Runnable { * @param filename text to show in the "File" column. */ FileAccessItem(final List<FileAccessItem> owner, final String filename) { + final Group staticView, seeksView; this.owner = owner; this.filename = filename; - accessRanges = RangeSet.create(Long.class, true, false); - accessView = new Pane(); + staticView = new Group(); + seeksView = new Group(); + staticGroup = staticView.getChildren(); + seeksGroup = seeksView .getChildren(); + accessView = new Pane(staticView, seeksView); + accessRanges = RangeSet.create(Long.class, true, false); + staticView.setAutoSizeChildren(false); /* * Background rectangle. */ @@ -120,43 +171,119 @@ final class FileAccessItem implements Runnable { background.setStroke(FILL_COLOR.brighter()); background.setFill(Color.TRANSPARENT); background.setStrokeType(StrokeType.INSIDE); - accessView.getChildren().add(background); - accessView.widthProperty().addListener((p,o,n) -> { - columnWidth = n.doubleValue() - MARGIN_RIGHT; - adjustSizes(true); - }); + staticGroup.add(background); + accessView.widthProperty().addListener((p,o,n) -> resize(n.doubleValue())); + } + + /** + * Sets a new total width (in pixels) for the bars to draw in {@link #accessView}. + * This method adjust the sizes of all bars and the positions of cursor and seeks. + */ + private void resize(final double width) { + final double old = columnWidth; + columnWidth = width - MARGIN_RIGHT; + final double scale = columnWidth / fileSize; + if (Double.isFinite(scale)) { + adjustSizes(scale, true); + if (cursor != null) { + final Rectangle r = (Rectangle) cursor.getNode(); + r.setX(Math.max(0, Math.min(scale*cursorPosition - CURSOR_WIDTH/2, columnWidth - CURSOR_WIDTH))); + } + final double ratio = columnWidth / old; + for (final Node node : seeksGroup) { + final Line line = (Line) node; + final double x = line.getStartX() * ratio; + line.setStartX(x); + line.setEndX(x); + } + } + } + + /** + * Adds a seek position. The position will be kept for some time before to fade away. + */ + private void addSeek(final long position) { + final double x = position * (columnWidth / fileSize); + final Line line = new Line(x, MARGIN_TOP, x, MARGIN_TOP + HEIGHT); + line.setStroke(Color.DARKBLUE); + seeksGroup.add(line); + final FadeTransition t = new FadeTransition(CURSOR_DURATION, line); + t.setDelay(SEEK_DURATION); + t.setFromValue(1); + t.setToValue(0); + t.setOnFinished(this); + t.play(); } /** * Reports a read or write operation on a range of bytes. - * This method is invoked by the {@link Observer} wrapper. + * This method must be invoked from JavaFX thread. * * @param position offset of the first byte read or written. * @param count number of bytes read or written. * @param write {@code false} for a read operation, or {@code true} for a write operation. */ private void addRange(final long position, final int count, final boolean write) { - Platform.runLater(() -> { - if (accessRanges.add(position, position + count)) { - adjustSizes(false); + cursorPosition = position; + final boolean add = accessRanges.add(position, position + count); + final double scale = columnWidth / fileSize; + if (Double.isFinite(scale)) { + if (add) { + adjustSizes(scale, false); } - }); + final Rectangle r; + if (cursor == null) { + r = new Rectangle(0, MARGIN_TOP, CURSOR_WIDTH, HEIGHT); + r.setArcWidth(CURSOR_WIDTH/2 - 1); + r.setArcHeight(HEIGHT/2 - 2); + r.setStroke(Color.ORANGE); + r.setFill(Color.YELLOW); + accessView.getChildren().add(r); + cursor = new FadeTransition(CURSOR_DURATION, r); + cursor.setOnFinished(this); + cursor.setFromValue(1); + cursor.setToValue(0); + } else { + r = (Rectangle) cursor.getNode(); + } + r.setX(Math.max(0, Math.min(scale*position - CURSOR_WIDTH/2, columnWidth - CURSOR_WIDTH))); + cursor.playFromStart(); + } + } + + /** + * Invoked when an animation effect finished. + * This method discards the animation and geometry objects for letting GC do its work. + */ + @Override + public void handle(final ActionEvent event) { + final FadeTransition animation = (FadeTransition) event.getSource(); + final ObservableList<Node> list; + if (animation == cursor) { + cursor = null; + list = accessView.getChildren(); + } else { + list = seeksGroup; + } + final boolean removed = list.remove(animation.getNode()); + assert removed : animation; } /** * Recomputes all rectangles from current {@link #columnWidth} and {@link #accessRanges}. * + * <h4>Implementation note:</h4> + * This method is inefficient as it iterates over all ranges instead of only the ranges that changed. + * It should be okay in the common case where file accesses happens often on consecutive blocks, + * in which case ranges get merged together and the total number of elements in {@link #accessRanges} + * stay stable or even reduce. + * * @param resized {@code true} if this method is invoked because of a change of column width * with (presumably) no change in {@link #accessRanges}, or {@code false} if invoked * after a new range has been added with (presumably) no change in {@link #columnWidth}. */ - final void adjustSizes(final boolean resized) { - final double scale = columnWidth / fileSize; - if (!Double.isFinite(scale)) { - return; - } - final ObservableList<Node> children = accessView.getChildren(); - final ListIterator<Node> bars = children.listIterator(); + final void adjustSizes(final double scale, final boolean resized) { + final ListIterator<Node> bars = staticGroup.listIterator(); ((Rectangle) bars.next()).setWidth(columnWidth); // Background. /* * Adjust the position and width of all rectangles. @@ -188,7 +315,7 @@ final class FileAccessItem implements Runnable { bars.add(r); } // Remove all remaining children, if any. - children.remove(bars.nextIndex(), children.size()); + staticGroup.remove(bars.nextIndex(), staticGroup.size()); } /** @@ -215,7 +342,7 @@ final class FileAccessItem implements Runnable { public int read(final ByteBuffer dst) throws IOException { final long position = position(); final int count = channel.read(dst); - addRange(position, count, false); + Platform.runLater(() -> addRange(position, count, false)); return count; } @@ -226,7 +353,7 @@ final class FileAccessItem implements Runnable { public int write(final ByteBuffer src) throws IOException { final long position = position(); final int count = channel.write(src); - addRange(position, count, true); + Platform.runLater(() -> addRange(position, count, true)); return count; } @@ -242,8 +369,9 @@ final class FileAccessItem implements Runnable { * Forwards to the wrapper channel. */ @Override - public SeekableByteChannel position(final long newPosition) throws IOException { - channel.position(newPosition); + public SeekableByteChannel position(final long position) throws IOException { + channel.position(position); + Platform.runLater(() -> addSeek(position)); return this; }