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 5f5b1cec004469c5bd837c5a293a67e24e87fa63 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Mar 20 16:47:12 2022 +0100 Add navigation in `GridView` using keyboard and mose drag events. --- .../org/apache/sis/gui/coverage/GridViewSkin.java | 127 ++++++++++++++++++--- .../java/org/apache/sis/gui/map/MapCanvas.java | 7 +- .../java/org/apache/sis/gui/map/StatusBar.java | 1 + .../org/apache/sis/internal/gui/MouseDrags.java | 55 +++++++++ 4 files changed, 172 insertions(+), 18 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java index c65ba81..a2c64d6 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java @@ -23,6 +23,7 @@ import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.VPos; import javafx.scene.Node; +import javafx.scene.Cursor; import javafx.scene.control.ScrollBar; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.control.skin.VirtualContainerBase; @@ -32,8 +33,12 @@ import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.FontWeight; import javafx.scene.text.Font; +import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; +import javafx.scene.input.KeyEvent; import javafx.event.EventHandler; +import javafx.event.EventType; +import org.apache.sis.internal.gui.MouseDrags; import org.apache.sis.internal.gui.Styles; @@ -51,7 +56,7 @@ import org.apache.sis.internal.gui.Styles; * </ul> * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ @@ -128,11 +133,26 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme private boolean hasErrors; /** - * A rectangle around selected cells in the content area or in the row/column header. + * A rectangle around selected the cell in the content area or in the row/column header. */ private final Rectangle selection, selectedRow, selectedColumn; /** + * {@code true} if a drag event is in progress. + * + * @see #onDrag(MouseEvent) + */ + private boolean isDragging; + + /** + * Cursor position at the time of previous pan event. + * This is used for computing the translation to apply during drag events. + * + * @see #onDrag(MouseEvent) + */ + private double xPanPrevious, yPanPrevious; + + /** * Creates a new skin for the specified view. */ GridViewSkin(final GridView view) { @@ -174,11 +194,6 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme selection .setManaged(false); selectedRow .setManaged(false); selectedColumn.setManaged(false); - /* - * The status bar where to show coordinates of selected cell. - * Mouse exit event is handled by `hideSelection(…)`. - */ - flow.setOnMouseEntered(view.statusBar); flow.setOnMouseExited((e) -> hideSelection()); /* * The list of children is initially empty. We need to add the virtual flow @@ -191,6 +206,11 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme bar.setManaged(false); getChildren().addAll(topBackground, leftBackground, selectedColumn, selectedRow, headerRow, selection, bar, flow); + /* + * Keyboard and drag events for moving the viewed bounds. + */ + view.addEventHandler(KeyEvent.KEY_PRESSED, this::onKeyTyped); + MouseDrags.setHandlers(view, this::onDrag); } /** @@ -199,26 +219,28 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme * * <p>This listener is registered for each {@link GridRow} instances. * It is not designed for other kinds of event source.</p> + * + * @see #onDrag(MouseEvent) */ @Override public final void handle(final MouseEvent event) { double x = event.getX() - (leftPosition + headerWidth); boolean visible = (x >= 0); if (visible) { - final double visibleColumn = Math.floor(x / cellWidth); - visible = (visibleColumn >= 0); + final double column = Math.floor(x / cellWidth); + visible = (column >= 0); if (visible) { final GridRow row = (GridRow) event.getSource(); double y = row.getLayoutY(); - visible = y < getVirtualFlow().getHeight(); + visible = y < ((Flow) getVirtualFlow()).getVisibleHeight(); if (visible) { - x = visibleColumn * cellWidth + leftBackground.getWidth() + row.getLayoutX(); + x = column * cellWidth + leftBackground.getWidth() + row.getLayoutX(); y += topBackground.getHeight(); selection.setX(x); // NOT equivalent to `relocate(x,y)`. selection.setY(y); selectedRow.setY(y); selectedColumn.setX(x); - getSkinnable().formatCoordinates(firstVisibleColumn + (int) visibleColumn, row.getIndex()); + getSkinnable().formatCoordinates(firstVisibleColumn + (int) column, row.getIndex()); } } } @@ -231,7 +253,69 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme } /** - * Hides the selection when the mouse moved outside the grid view area. + * Invoked when the user presses the button, drags the grid and releases the button. + * The position of the selection rectangles become invalid has a result of the drag. + * Instead of bothering to adjust it, we hide it. It may also be less confusing for + * the user, because it shows that we are not exploring values of different cells. + * + * @see #handle(MouseEvent) + */ + private void onDrag(final MouseEvent event) { + if (event.getButton() == MouseButton.PRIMARY) { + final double x = event.getX() - leftBackground.getWidth(); + final double y = event.getY() - topBackground.getHeight(); + final Flow flow = (Flow) getVirtualFlow(); + if (x >= 0 && y >= 0 && y < flow.getVisibleHeight()) { + final GridView view = getSkinnable(); + final EventType<? extends MouseEvent> type = event.getEventType(); + if (type == MouseEvent.MOUSE_PRESSED) { + view.setCursor(Cursor.CLOSED_HAND); + view.requestFocus(); + xPanPrevious = x; + yPanPrevious = y; + isDragging = true; + hideSelection(); + } else if (isDragging) { + if (type == MouseEvent.MOUSE_RELEASED) { + view.setCursor(Cursor.DEFAULT); + isDragging = false; + } + xPanPrevious -= flow.scrollHorizontal(xPanPrevious - x); + yPanPrevious -= flow.scrollPixels(yPanPrevious - y); + } + event.consume(); + } + } + } + + /** + * Invoked when the user presses a key. This handler provides navigation in the direction of arrow keys. + * The selection rectangles are hidden because otherwise the user may be surprised to see the whole grid + * scrolling instead of the selection rectangle moving. + */ + private void onKeyTyped(final KeyEvent event) { + double tx=0, ty=0; + switch (event.getCode()) { + case RIGHT: case KP_RIGHT: tx = 1; break; + case LEFT: case KP_LEFT: tx = -1; break; + case DOWN: case KP_DOWN: ty = +1; break; + case UP: case KP_UP: ty = -1; break; + default: return; + } + if (event.isShiftDown()) { + tx *= 10; + ty *= 10; + } + final Flow flow = (Flow) getVirtualFlow(); + flow.scrollPixels(flow.getFixedCellSize() * ty); + flow.scrollHorizontal(cellWidth * tx); + hideSelection(); + event.consume(); + } + + /** + * Hides the selection when the mouse moved outside the grid view area, + * or when a drag or scrolling action is performed. */ private void hideSelection() { selection .setVisible(false); @@ -346,13 +430,14 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme * Those two properties are used for creating the minimal amount * of {@link GridCell}s needed for rendering the {@link GridRow}. */ - static final class Flow extends VirtualFlow<GridRow> implements ChangeListener<Number> { + private static final class Flow extends VirtualFlow<GridRow> implements ChangeListener<Number> { /** * Creates a new flow for the given view. This method registers listeners * on the properties that may require a redrawn of the full view port. */ @SuppressWarnings("ThisEscapedInObjectConstruction") Flow(final GridView view) { + setPannable(false); // We will use our own pan listeners. getHbar().valueProperty().addListener(this); view.bandProperty.addListener(this); view.cellSpacing .addListener(this); @@ -368,6 +453,20 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme } /** + * Attempts to scroll horizontally the view by the given amount of pixels. + * + * @param delta the amount of pixels to scroll. + * @return the number of pixels actually moved. + */ + final double scrollHorizontal(final double delta) { + final ScrollBar bar = getHbar(); + final double previous = bar.getValue(); + final double value = Math.max(bar.getMin(), Math.min(bar.getMax(), previous + delta)); + bar.setValue(value); + return value - previous; + } + + /** * Returns the height of the view area, not counting the horizontal scroll bar. * This height does not include the row header neither, because it is managed by * a separated node ({@link #headerRow}). diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java index 81d94ff..be9019d 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java @@ -77,6 +77,7 @@ import org.apache.sis.internal.system.DelayedRunnable; import org.apache.sis.internal.gui.BackgroundThreads; import org.apache.sis.internal.gui.ExceptionReporter; import org.apache.sis.internal.gui.GUIUtilities; +import org.apache.sis.internal.gui.MouseDrags; import org.apache.sis.internal.gui.Resources; import org.apache.sis.internal.referencing.AxisDirections; import org.apache.sis.portrayal.PlanarCanvas; @@ -269,7 +270,7 @@ public abstract class MapCanvas extends PlanarCanvas { private double xPanStart, yPanStart; /** - * {@code true} if a drag even is in progress. + * {@code true} if a drag event is in progress. * * @see #onDrag(MouseEvent) */ @@ -345,11 +346,9 @@ public abstract class MapCanvas extends PlanarCanvas { view.setOnZoom ((e) -> applyZoomOrRotate(e, e.getZoomFactor(), 0)); view.setOnRotate((e) -> applyZoomOrRotate(e, 1, e.getAngle())); view.setOnScroll(this::onScroll); - view.setOnMousePressed(this::onDrag); - view.setOnMouseDragged(this::onDrag); - view.setOnMouseReleased(this::onDrag); view.setFocusTraversable(true); view.addEventHandler(KeyEvent.KEY_PRESSED, this::onKeyTyped); + MouseDrags.setHandlers(view, this::onDrag); /* * Do not set a preferred size, otherwise `repaint()` is invoked twice: once with the preferred size * and once with the actual size of the parent window. Actually the `repaint()` method appears to be diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java index 5e0f78c..bb85708 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java @@ -1171,6 +1171,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * Do not use `position.setVisible(false)` because * we want the Tooltip to continue to be available. */ + lastX = lastY = Double.NaN; position.setText(outsideText); if (isSampleValuesVisible) { sampleValues.setText(sampleValuesProvider.get().evaluate(null)); diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/MouseDrags.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/MouseDrags.java new file mode 100644 index 0000000..6c567fe --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/MouseDrags.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.gui; + +import javafx.event.EventHandler; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; + + +/** + * Utility methods for handling mouse dragging events. + * Current version does not offer much service. + * But this class provides a place where future versions may implement "smooth dragging". + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final class MouseDrags { + /** + * Do not allow (for now) instantiation of this class. + */ + private MouseDrags() { + } + + /** + * Sets the given handle on "mouse pressed", "mouse dragged" and "mouse released" listeners. + * This convenience method is uses as a way to register the same lambda for the 3 kinds of listener. + * By contrast, repeating a lambda expression such as {@code this::onDrag} three times results in 3 + * different lambda instances to be created. + * + * @param view the view on which to register the listener. + * @param handler the listener to register on the specified view. + */ + public static void setHandlers(final Region view, final EventHandler<? super MouseEvent> handler) { + view.setOnMousePressed (handler); + view.setOnMouseDragged (handler); + view.setOnMouseReleased(handler); + } +}