|
15 | 15 | import java.util.function.BiFunction; |
16 | 16 | import java.util.function.Consumer; |
17 | 17 | import java.util.function.Function; |
18 | | -import java.util.function.IntConsumer; |
19 | 18 | import java.util.function.IntFunction; |
20 | 19 | import java.util.function.IntSupplier; |
21 | 20 | import java.util.function.IntUnaryOperator; |
|
47 | 46 | import javafx.scene.Node; |
48 | 47 | import javafx.scene.control.ContextMenu; |
49 | 48 | import javafx.scene.control.IndexRange; |
| 49 | +import javafx.scene.input.MouseEvent; |
50 | 50 | import javafx.scene.layout.Background; |
51 | 51 | import javafx.scene.layout.BackgroundFill; |
52 | 52 | import javafx.scene.layout.CornerRadii; |
|
159 | 159 | * |
160 | 160 | * <h3>Overriding default mouse behavior</h3> |
161 | 161 | * |
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 -> 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 -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !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 -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !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 -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !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 | + * |
168 | 231 | * |
169 | 232 | * @param <PS> type of style that can be applied to paragraphs (e.g. {@link TextFlow}. |
170 | 233 | * @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) { |
266 | 329 | @Override public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } |
267 | 330 | @Override public ObjectProperty<Duration> mouseOverTextDelayProperty() { return mouseOverTextDelay; } |
268 | 331 |
|
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(); } |
272 | 377 |
|
273 | 378 | private final ObjectProperty<IntFunction<? extends Node>> paragraphGraphicFactory = new SimpleObjectProperty<>(null); |
274 | 379 | @Override |
@@ -603,7 +708,7 @@ public GenericStyledArea( |
603 | 708 | if (indexOfChange < caretPosition) { |
604 | 709 | // if caret is within the changed content, move it to indexOfChange |
605 | 710 | // otherwise offset it by changeLength |
606 | | - positionCaret( |
| 711 | + displaceCaret( |
607 | 712 | caretPosition < endOfChange |
608 | 713 | ? indexOfChange |
609 | 714 | : caretPosition + changeLength |
@@ -1134,6 +1239,29 @@ public void nextPage(SelectionPolicy selectionPolicy) { |
1134 | 1239 | moveTo(hit.getInsertionIndex(), selectionPolicy); |
1135 | 1240 | } |
1136 | 1241 |
|
| 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 | + |
1137 | 1265 | @Override |
1138 | 1266 | public void setStyle(int from, int to, S style) { |
1139 | 1267 | content.setStyle(from, to, style); |
@@ -1448,18 +1576,6 @@ private Guard suspend(Suspendable... suspendables) { |
1448 | 1576 | return Suspendable.combine(beingUpdated, Suspendable.combine(suspendables)).suspend(); |
1449 | 1577 | } |
1450 | 1578 |
|
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 | | - |
1463 | 1579 | void clearTargetCaretOffset() { |
1464 | 1580 | targetCaretOffset = Optional.empty(); |
1465 | 1581 | } |
|
0 commit comments