Skip to content

Commit 0059115

Browse files
Merge pull request #468 from JordanMartinez/rewriteMouseHandling
Split up mouse handling so that overriding default mouse behavior does not affect other default behavior
2 parents e487947 + 78d4f2b commit 0059115

6 files changed

Lines changed: 698 additions & 140 deletions

File tree

richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import java.util.function.BiFunction;
1616
import java.util.function.Consumer;
1717
import java.util.function.Function;
18-
import java.util.function.IntConsumer;
1918
import java.util.function.IntFunction;
2019
import java.util.function.IntSupplier;
2120
import java.util.function.IntUnaryOperator;
@@ -47,6 +46,7 @@
4746
import javafx.scene.Node;
4847
import javafx.scene.control.ContextMenu;
4948
import javafx.scene.control.IndexRange;
49+
import javafx.scene.input.MouseEvent;
5050
import javafx.scene.layout.Background;
5151
import javafx.scene.layout.BackgroundFill;
5252
import javafx.scene.layout.CornerRadii;
@@ -159,12 +159,75 @@
159159
*
160160
* <h3>Overriding default mouse behavior</h3>
161161
*
162-
* The area's default mouse behavior cannot be partially overridden without it affecting other behavior (to do so,
163-
* one would need to re-implement the entire default behavior with one minor adjustment). Rather, one should
164-
* override the default mouse behavior by changing what happens at various events.
165-
* For example, {@link #getOnSelectionDrop()} overrides what happens when some portion of the area's content is
166-
* selected, then the mouse is pressed on that selection, the mouse moves to a new location, and the mouse is released.
167-
* At that point, {@link #onSelectionDrop} is used to determine what should happen.
162+
* The area's default mouse behavior properly handles auto-scrolling and dragging the selected text to a new location.
163+
* As such, some parts cannot be partially overridden without it affecting other behavior.
164+
*
165+
* <p>The following lists either {@link org.fxmisc.wellbehaved.event.EventPattern}s that cannot be overridden without
166+
* negatively affecting the default mouse behavior or describe how to safely override things in a special way without
167+
* disrupting the auto scroll behavior.</p>
168+
* <ul>
169+
* <li>
170+
* <em>First (1 click count) Primary Button Mouse Pressed Events:</em>
171+
* (<code>EventPattern.mousePressed(MouseButton.PRIMARY).onlyIf(e -&gt; e.getClickCount() == 1)</code>).
172+
* Do not override. Instead, use {@link #onOutsideSelectionMousePress},
173+
* {@link #onInsideSelectionMousePressRelease}, or see next item.
174+
* </li>
175+
* <li>(
176+
* <em>All Other Mouse Pressed Events (e.g., Primary with 2+ click count):</em>
177+
* Aside from hiding the context menu if it is showing (use {@link #hideContextMenu()} some((where in your
178+
* overriding InputMap to maintain this behavior), these can be safely overridden via any of the
179+
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate InputMapTemplate's factory methods} or
180+
* {@link org.fxmisc.wellbehaved.event.InputMap InputMap's factory methods}.
181+
* </li>
182+
* <li>
183+
* <em>Primary-Button-only Mouse Drag Detection Events:</em>
184+
* (<code>EventPattern.eventType(MouseEvent.DRAG_DETECTED).onlyIf(e -&gt; e.getButton() == MouseButton.PRIMARY &amp;&amp; !e.isMiddleButtonDown() &amp;&amp; !e.isSecondaryButtonDown())</code>).
185+
* Do not override. Instead, use {@link #onNewSelectionDrag} or {@link #onSelectionDrag}.
186+
* </li>
187+
* <li>
188+
* <em>Primary-Button-only Mouse Drag Events:</em>
189+
* (<code>EventPattern.mouseDragged().onlyIf(e -&gt; e.getButton() == MouseButton.PRIMARY &amp;&amp; !e.isMiddleButtonDown() &amp;&amp; !e.isSecondaryButtonDown())</code>)
190+
* Do not override, but see next item.
191+
* </li>
192+
* <li>
193+
* <em>All Other Mouse Drag Events:</em>
194+
* You may safely override other Mouse Drag Events using different
195+
* {@link org.fxmisc.wellbehaved.event.EventPattern}s without affecting default behavior only if
196+
* process InputMaps (
197+
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(javafx.event.EventType, BiFunction)},
198+
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(org.fxmisc.wellbehaved.event.EventPattern, BiFunction)},
199+
* {@link org.fxmisc.wellbehaved.event.InputMap#process(javafx.event.EventType, Function)}, or
200+
* {@link org.fxmisc.wellbehaved.event.InputMap#process(org.fxmisc.wellbehaved.event.EventPattern, Function)}
201+
* ) are used and {@link org.fxmisc.wellbehaved.event.InputHandler.Result#PROCEED} is returned.
202+
* The area has a "catch all" Mouse Drag InputMap that will auto scroll towards the mouse drag event when it
203+
* occurs outside the bounds of the area and will stop auto scrolling when the mouse event occurs within the
204+
* area. However, this only works if the event is not consumed before the event reaches that InputMap.
205+
* To insure the auto scroll feature is enabled, set {@link #isAutoScrollOnDragDesired()} to true in your
206+
* process InputMap. If the feature is not desired for that specific drag event, set it to false in the
207+
* process InputMap.
208+
* <em>Note: Due to this "catch-all" nature, all Mouse Drag Events are consumed.</em>
209+
* </li>
210+
* <li>
211+
* <em>Primary-Button-only Mouse Released Events:</em>
212+
* (<code>EventPattern.mouseReleased().onlyIf(e -&gt; e.getButton() == MouseButton.PRIMARY &amp;&amp; !e.isMiddleButtonDown() &amp;&amp; !e.isSecondaryButtonDown())</code>).
213+
* Do not override. Instead, use {@link #onNewSelectionDragEnd}, {@link #onSelectionDrop}, or see next item.
214+
* </li>
215+
* <li>
216+
* <em>All other Mouse Released Events:</em>
217+
* You may override other Mouse Released Events using different
218+
* {@link org.fxmisc.wellbehaved.event.EventPattern}s without affecting default behavior only if
219+
* process InputMaps (
220+
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(javafx.event.EventType, BiFunction)},
221+
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(org.fxmisc.wellbehaved.event.EventPattern, BiFunction)},
222+
* {@link org.fxmisc.wellbehaved.event.InputMap#process(javafx.event.EventType, Function)}, or
223+
* {@link org.fxmisc.wellbehaved.event.InputMap#process(org.fxmisc.wellbehaved.event.EventPattern, Function)}
224+
* ) are used and {@link org.fxmisc.wellbehaved.event.InputHandler.Result#PROCEED} is returned.
225+
* The area has a "catch-all" InputMap that will consume all mouse released events and stop auto scroll if it
226+
* was scrolling. However, this only works if the event is not consumed before the event reaches that InputMap.
227+
* <em>Note: Due to this "catch-all" nature, all Mouse Released Events are consumed.</em>
228+
* </li>
229+
* </ul>
230+
*
168231
*
169232
* @param <PS> type of style that can be applied to paragraphs (e.g. {@link TextFlow}.
170233
* @param <SEG> type of segment used in {@link Paragraph}. Can be only text (plain or styled) or
@@ -266,9 +329,51 @@ private static int clamp(int min, int val, int max) {
266329
@Override public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); }
267330
@Override public ObjectProperty<Duration> mouseOverTextDelayProperty() { return mouseOverTextDelay; }
268331

269-
private final Property<IntConsumer> onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText);
270-
@Override public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); }
271-
@Override public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); }
332+
private final BooleanProperty autoScrollOnDragDesired = new SimpleBooleanProperty(true);
333+
public final void setAutoScrollOnDragDesired(boolean val) { autoScrollOnDragDesired.set(val); }
334+
public final boolean isAutoScrollOnDragDesired() { return autoScrollOnDragDesired.get(); }
335+
336+
private final Property<Consumer<MouseEvent>> onOutsideSelectionMousePress = new SimpleObjectProperty<>(e -> {
337+
CharacterHit hit = hit(e.getX(), e.getY());
338+
moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR);
339+
});
340+
public final void setOnOutsideSelectionMousePress(Consumer<MouseEvent> consumer) { onOutsideSelectionMousePress.setValue(consumer); }
341+
public final Consumer<MouseEvent> getOnOutsideSelectionMousePress() { return onOutsideSelectionMousePress.getValue(); }
342+
343+
private final Property<Consumer<MouseEvent>> onInsideSelectionMousePressRelease = new SimpleObjectProperty<>(e -> {
344+
CharacterHit hit = hit(e.getX(), e.getY());
345+
moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR);
346+
});
347+
public final void setOnInsideSelectionMousePressRelease(Consumer<MouseEvent> consumer) { onInsideSelectionMousePressRelease.setValue(consumer); }
348+
public final Consumer<MouseEvent> getOnInsideSelectionMousePressRelease() { return onInsideSelectionMousePressRelease.getValue(); }
349+
350+
private final Property<Consumer<Point2D>> onNewSelectionDrag = new SimpleObjectProperty<>(p -> {
351+
CharacterHit hit = hit(p.getX(), p.getY());
352+
moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST);
353+
});
354+
public final void setOnNewSelectionDrag(Consumer<Point2D> consumer) { onNewSelectionDrag.setValue(consumer); }
355+
public final Consumer<Point2D> getOnNewSelectionDrag() { return onNewSelectionDrag.getValue(); }
356+
357+
private final Property<Consumer<MouseEvent>> onNewSelectionDragEnd = new SimpleObjectProperty<>(e -> {
358+
CharacterHit hit = hit(e.getX(), e.getY());
359+
moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST);
360+
});
361+
public final void setOnNewSelectionDragEnd(Consumer<MouseEvent> consumer) { onNewSelectionDragEnd.setValue(consumer); }
362+
public final Consumer<MouseEvent> getOnNewSelectionDragEnd() { return onNewSelectionDragEnd.getValue(); }
363+
364+
private final Property<Consumer<Point2D>> onSelectionDrag = new SimpleObjectProperty<>(p -> {
365+
CharacterHit hit = hit(p.getX(), p.getY());
366+
displaceCaret(hit.getInsertionIndex());
367+
});
368+
public final void setOnSelectionDrag(Consumer<Point2D> consumer) { onSelectionDrag.setValue(consumer); }
369+
public final Consumer<Point2D> getOnSelectionDrag() { return onSelectionDrag.getValue(); }
370+
371+
private final Property<Consumer<MouseEvent>> onSelectionDrop = new SimpleObjectProperty<>(e -> {
372+
CharacterHit hit = hit(e.getX(), e.getY());
373+
moveSelectedText(hit.getInsertionIndex());
374+
});
375+
@Override public final void setOnSelectionDrop(Consumer<MouseEvent> consumer) { onSelectionDrop.setValue(consumer); }
376+
@Override public final Consumer<MouseEvent> getOnSelectionDrop() { return onSelectionDrop.getValue(); }
272377

273378
private final ObjectProperty<IntFunction<? extends Node>> paragraphGraphicFactory = new SimpleObjectProperty<>(null);
274379
@Override
@@ -603,7 +708,7 @@ public GenericStyledArea(
603708
if (indexOfChange < caretPosition) {
604709
// if caret is within the changed content, move it to indexOfChange
605710
// otherwise offset it by changeLength
606-
positionCaret(
711+
displaceCaret(
607712
caretPosition < endOfChange
608713
? indexOfChange
609714
: caretPosition + changeLength
@@ -1134,6 +1239,29 @@ public void nextPage(SelectionPolicy selectionPolicy) {
11341239
moveTo(hit.getInsertionIndex(), selectionPolicy);
11351240
}
11361241

1242+
/**
1243+
* Displaces the caret from the selection by positioning only the caret to the new location without
1244+
* also affecting the selection's {@link #getAnchor() anchor} or the {@link #getSelection() selection}.
1245+
* Do not confuse this method with {@link #moveTo(int)}, which is the normal way of moving the caret.
1246+
* This method can be used to achieve the special case of positioning the caret outside or inside the selection,
1247+
* as opposed to always being at the boundary. Use with care.
1248+
*/
1249+
public void displaceCaret(int pos) {
1250+
try(Guard g = suspend(caretPosition, currentParagraph, caretColumn)) {
1251+
internalCaretPosition.setValue(pos);
1252+
}
1253+
}
1254+
1255+
/**
1256+
* Hides the area's context menu if it is not {@code null} and it is {@link ContextMenu#isShowing() showing}.
1257+
*/
1258+
public final void hideContextMenu() {
1259+
ContextMenu menu = getContextMenu();
1260+
if (menu != null && menu.isShowing()) {
1261+
menu.hide();
1262+
}
1263+
}
1264+
11371265
@Override
11381266
public void setStyle(int from, int to, S style) {
11391267
content.setStyle(from, to, style);
@@ -1448,18 +1576,6 @@ private Guard suspend(Suspendable... suspendables) {
14481576
return Suspendable.combine(beingUpdated, Suspendable.combine(suspendables)).suspend();
14491577
}
14501578

1451-
/**
1452-
* Positions only the caret. Doesn't move the anchor and doesn't change
1453-
* the selection. Can be used to achieve the special case of positioning
1454-
* the caret outside or inside the selection, as opposed to always being
1455-
* at the boundary. Use with care.
1456-
*/
1457-
void positionCaret(int pos) {
1458-
try(Guard g = suspend(caretPosition, currentParagraph, caretColumn)) {
1459-
internalCaretPosition.setValue(pos);
1460-
}
1461-
}
1462-
14631579
void clearTargetCaretOffset() {
14641580
targetCaretOffset = Optional.empty();
14651581
}

0 commit comments

Comments
 (0)