From 9dc6abe28d6cf88535a1b0cc8442d44758e04dc7 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 23 Mar 2017 20:34:19 -0700 Subject: [PATCH] Implement additional hooks into mouse behavior --- .../fxmisc/richtext/GenericStyledArea.java | 33 +++++-- .../richtext/StyledTextAreaBehavior.java | 99 +++++++++++-------- 2 files changed, 83 insertions(+), 49 deletions(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 733d821ec..e0cd0c2e7 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -91,7 +91,6 @@ import org.reactfx.SuspendableEventStream; import org.reactfx.SuspendableNo; import org.reactfx.collection.LiveList; -import org.reactfx.collection.SuspendableList; import org.reactfx.util.Tuple2; import org.reactfx.value.SuspendableVal; import org.reactfx.value.SuspendableVar; @@ -266,6 +265,26 @@ private static int clamp(int min, int val, int max) { @Override public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } @Override public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } + /** + * Triggered when either the user presses the mouse in the area and drags the mouse, creating a new selection, + * or when the user has already created a selection via this process and continues to drag it further, + * expanding the selection. + */ + private final Property onNewSelectionDrag = new SimpleObjectProperty<>(i -> moveTo(i, SelectionPolicy.ADJUST)); + public final void setOnNewSelectionDrag(IntConsumer consumer) { onNewSelectionDrag.setValue(consumer); } + public final IntConsumer getOnNewSelectionDrag() { return onNewSelectionDrag.getValue(); } + + /** Triggered when user finishes {@link #onNewSelectionDrag} by releasing the mouse. Default value does nothing. */ + private final Property onNewSelectionDragEnd = new SimpleObjectProperty<>(i -> {}); + public final void setOnNewSelectionDragEnd(IntConsumer consumer) { onNewSelectionDragEnd.setValue(consumer); } + public final IntConsumer getOnNewSelectionDragEnd() { return onNewSelectionDragEnd.getValue(); } + + /** Triggered when user presses mouse above the selected text, and user drags mouse but has not yet released mouse*/ + private final Property onSelectionDrag = new SimpleObjectProperty<>(this::displaceCaret); + public final void setOnSelectionDrag(IntConsumer consumer) { onSelectionDrag.setValue(consumer); } + public final IntConsumer getOnSelectionDrag() { return onSelectionDrag.getValue(); } + + /** Triggered when user presses user drags the selected text to another location and has released the mouse */ private final Property onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText); @Override public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); } @Override public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); } @@ -603,7 +622,7 @@ public GenericStyledArea( if (indexOfChange < caretPosition) { // if caret is within the changed content, move it to indexOfChange // otherwise offset it by changeLength - positionCaret( + displaceCaret( caretPosition < endOfChange ? indexOfChange : caretPosition + changeLength @@ -1459,12 +1478,12 @@ private Guard suspend(Suspendable... suspendables) { } /** - * Positions only the caret. Doesn't move the anchor and doesn't change - * the selection. Can be used to achieve the special case of positioning - * the caret outside or inside the selection, as opposed to always being - * at the boundary. Use with care. + * Positions only the caret without also moving the selection that is bound to the caret. Do not use this when + * you meant to use {@link #moveTo(int)}. This method doesn't move the selection's anchor and doesn't change + * the selection. It can be used to achieve the special case of positioning the caret outside or inside the + * selection, as opposed to always being at the boundary. Useful for {@link #getOnSelectionDrag()} */ - void positionCaret(int pos) { + public void displaceCaret(int pos) { try(Guard g = suspend(caretPosition, currentParagraph, caretColumn)) { internalCaretPosition.setValue(pos); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java index 23fd96ebb..51e0dea6c 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java @@ -199,26 +199,16 @@ class StyledTextAreaBehavior { ) ); - InputMapTemplate mouseClickedTemplate = sequence( - consumeUnless( - mouseClicked(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 2), - viewIsDisabled, - (b, e) -> b.view.selectWord() - ), - consumeUnless( - mouseClicked(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 3), - viewIsDisabled, - (b, e) -> b.view.selectParagraph() - ) - ); - InputMapTemplate mouseDragTemplate = sequence( + process(eventType(MouseEvent.DRAG_DETECTED), (b, e) -> { + b.processDragDetection(); + return Result.PROCEED; + }), consumeUnless( mouseDragged().onlyIf(e -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !e.isSecondaryButtonDown()), viewIsDisabled, StyledTextAreaBehavior::handleMouseDragged - ), - consume(eventType(MouseEvent.DRAG_DETECTED), StyledTextAreaBehavior::handleDragDetected) + ) ); Predicate viewIsEnabled = viewIsDisabled.negate(); @@ -230,8 +220,31 @@ class StyledTextAreaBehavior { }), consumeUnless( EventPattern.mouseReleased(), - viewIsEnabled, - StyledTextAreaBehavior::handleMouseReleased + viewIsEnabled.and(b -> b.dragSelection == DragState.NO_DRAG), + StyledTextAreaBehavior::handleNewSelectionDragEnd + ), + consumeUnless( + EventPattern.mouseReleased(), + viewIsEnabled.and(b -> b.dragSelection == DragState.DRAG), + StyledTextAreaBehavior::handleSelectionDragDrop + ) + ); + + InputMapTemplate mouseClickedTemplate = sequence( + consumeUnless( + mouseClicked(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 1), + viewIsDisabled, + StyledTextAreaBehavior::handleSingleClick + ), + consumeUnless( + mouseClicked(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 2), + viewIsDisabled, + (b, e) -> b.view.selectWord() + ), + consumeUnless( + mouseClicked(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 3), + viewIsDisabled, + (b, e) -> b.view.selectParagraph() ) ); @@ -475,12 +488,11 @@ private void handleFirstLeftPress(MouseEvent e) { dragSelection = DragState.POTENTIAL_DRAG; } else { dragSelection = DragState.NO_DRAG; - view.moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR); } } - private void handleDragDetected(MouseEvent e) { - if(dragSelection == DragState.POTENTIAL_DRAG) { + private void processDragDetection() { + if (dragSelection == DragState.POTENTIAL_DRAG) { dragSelection = DragState.DRAG; } } @@ -495,37 +507,40 @@ private void handleMouseDragged(MouseEvent e) { } } - private void handleMouseReleased(MouseEvent e) { - switch (dragSelection) { - case POTENTIAL_DRAG: - CharacterHit hit = view.hit(e.getX(), e.getY()); - view.moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR); - break; - case DRAG: - // only handle drags if mouse was released inside of view - if (view.getLayoutBounds().contains(e.getX(), e.getY())) { - // move selection to the target position - CharacterHit h = view.hit(e.getX(), e.getY()); - view.getOnSelectionDrop().accept(h.getInsertionIndex()); - // do nothing, handled by mouseDragReleased - } - break; - case NO_DRAG: - // so do nothing, caret is already repositioned in mousePressed - } - } - private void dragTo(Point2D p) { CharacterHit hit = view.hit(p.getX(), p.getY()); if(dragSelection == DragState.DRAG || dragSelection == DragState.POTENTIAL_DRAG) { // MOUSE_DRAGGED may arrive even before DRAG_DETECTED - view.positionCaret(hit.getInsertionIndex()); + view.getOnSelectionDrag().accept(hit.getInsertionIndex()); } else { - view.moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST); + view.getOnNewSelectionDrag().accept(hit.getInsertionIndex()); + } + } + + private void handleNewSelectionDragEnd(MouseEvent e) { + CharacterHit h = view.hit(e.getX(), e.getY()); + view.getOnNewSelectionDragEnd().accept(h.getInsertionIndex()); + } + + private void handleSelectionDragDrop(MouseEvent e) { + // only handle drags if mouse was released inside of view + if (view.getLayoutBounds().contains(e.getX(), e.getY())) { + // move selection to the target position + CharacterHit h = view.hit(e.getX(), e.getY()); + view.getOnSelectionDrop().accept(h.getInsertionIndex()); + // do nothing, handled by mouseDragReleased } } + private void handleSingleClick(MouseEvent e) { + // ensure focus + view.requestFocus(); + + CharacterHit hit = view.hit(e.getX(), e.getY()); + view.moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR); + } + private static Point2D project(Point2D p, Bounds bounds) { double x = clamp(p.getX(), bounds.getMinX(), bounds.getMaxX()); double y = clamp(p.getY(), bounds.getMinY(), bounds.getMaxY());