Skip to content

Commit

Permalink
Implement additional hooks into mouse behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanMartinez committed Mar 24, 2017
1 parent cd7dce6 commit 9dc6abe
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -266,6 +265,26 @@ private static int clamp(int min, int val, int max) {
@Override public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); }
@Override public ObjectProperty<Duration> 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<IntConsumer> 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<IntConsumer> 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<IntConsumer> 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<IntConsumer> onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText);
@Override public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); }
@Override public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,26 +199,16 @@ class StyledTextAreaBehavior {
)
);

InputMapTemplate<StyledTextAreaBehavior, ? super MouseEvent> 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<StyledTextAreaBehavior, ? super MouseEvent> 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<StyledTextAreaBehavior> viewIsEnabled = viewIsDisabled.negate();
Expand All @@ -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<StyledTextAreaBehavior, ? super MouseEvent> 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()
)
);

Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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());
Expand Down

0 comments on commit 9dc6abe

Please sign in to comment.