diff --git a/editor.lib/nbproject/project.properties b/editor.lib/nbproject/project.properties --- a/editor.lib/nbproject/project.properties +++ b/editor.lib/nbproject/project.properties @@ -42,7 +42,7 @@ javac.compilerargs=-Xlint:unchecked javac.source=1.7 -spec.version.base=4.5.0 +spec.version.base=4.6.0 is.autoload=true javadoc.arch=${basedir}/arch.xml diff --git a/editor.lib/src/org/netbeans/editor/BaseKit.java b/editor.lib/src/org/netbeans/editor/BaseKit.java --- a/editor.lib/src/org/netbeans/editor/BaseKit.java +++ b/editor.lib/src/org/netbeans/editor/BaseKit.java @@ -122,6 +122,7 @@ import org.netbeans.modules.editor.lib.KitsTracker; import org.netbeans.api.editor.NavigationHistory; import org.netbeans.api.editor.caret.CaretMoveContext; +import org.netbeans.api.editor.caret.MoveCaretsOrigin; import org.netbeans.spi.editor.caret.CaretMoveHandler; import org.netbeans.lib.editor.util.swing.PositionRegion; import org.netbeans.modules.editor.lib.SettingsConversions; @@ -2551,7 +2552,9 @@ } } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.NORTH) + ); } else { try { int dot = caret.getDot(); @@ -2643,7 +2646,9 @@ } } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.SOUTH) + ); } else { try { int dot = caret.getDot(); @@ -2785,6 +2790,7 @@ } // Update magic caret position + newCaretBounds = target.modelToView(caretInfo.getDot()); magicCaretPosition.y = newCaretBounds.y; context.setMagicCaretPosition(caretInfo, magicCaretPosition); } @@ -2792,7 +2798,9 @@ target.getToolkit().beep(); } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.NORTH) + ); } else { int caretOffset = caret.getDot(); Rectangle caretBounds = ((BaseTextUI)target.getUI()).modelToView(target, caretOffset); @@ -2929,7 +2937,9 @@ } } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.EAST) + ); } } else { try { @@ -3073,7 +3083,9 @@ target.getToolkit().beep(); } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.SOUTH) + ); } else { int caretOffset = caret.getDot(); Rectangle caretBounds = ((BaseTextUI)target.getUI()).modelToView(target, caretOffset); @@ -3205,7 +3217,9 @@ } } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.WEST) + ); } } else { try { @@ -3343,7 +3357,9 @@ } } } - }); + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.WEST) + ); } else { try { int dot = caret.getDot(); @@ -3470,8 +3486,9 @@ } } } - }); - + }, new MoveCaretsOrigin( + MoveCaretsOrigin.DIRECT_NAVIGATION, SwingConstants.EAST) + ); } else { try { // #232675: if bounds are defined, use them rather than line start/end diff --git a/editor.lib2/apichanges.xml b/editor.lib2/apichanges.xml --- a/editor.lib2/apichanges.xml +++ b/editor.lib2/apichanges.xml @@ -107,6 +107,30 @@ + + Support for Navigation Filters, and caret move origins + + + + + +

+ Swing NavigationFilters implemented on top of the Caret API. Caret API caller may describe the operation which causes + the caret to be moved, so that filters and caret listeners can react on specific action groups. +

+

+ A boilerplate NavigationFilter is provided, that supports chaining of filters on the caret +

+
+ + + + + + + + +
Added EditorRegistry.findComponent diff --git a/editor.lib2/manifest.mf b/editor.lib2/manifest.mf --- a/editor.lib2/manifest.mf +++ b/editor.lib2/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.editor.lib2/1 -OpenIDE-Module-Implementation-Version: 45 +OpenIDE-Module-Implementation-Version: 46 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/editor/lib2/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/editor/lib2/resources/layer.xml OpenIDE-Module-Needs: org.netbeans.modules.editor.actions diff --git a/editor.lib2/nbproject/project.properties b/editor.lib2/nbproject/project.properties --- a/editor.lib2/nbproject/project.properties +++ b/editor.lib2/nbproject/project.properties @@ -43,7 +43,7 @@ is.autoload=true javac.source=1.7 javac.compilerargs=-Xlint:unchecked -spec.version.base=2.8.0 +spec.version.base=2.9.0 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java b/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java --- a/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java +++ b/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java @@ -45,11 +45,12 @@ import java.util.List; import javax.swing.text.Document; import javax.swing.text.JTextComponent; +import javax.swing.text.NavigationFilter; import javax.swing.text.Position; import org.netbeans.api.annotations.common.NonNull; /** - * Context for carets moving within {@link CaretMoveHandler}. + * Context for carets moving within {@link org.netbeans.spi.editor.caret.CaretMoveHandler}. * * @author Miloslav Metelka * @since 2.6 @@ -113,19 +114,33 @@ * or true otherwise. */ public boolean setDot(@NonNull CaretInfo caret, @NonNull Position dotPos) { - return setDotAndMark(caret, dotPos, dotPos); + NavigationFilter naviFilter = transaction.getCaret().getNavigationFilterNoDefault(getMoveOrigin()); + if (naviFilter != null) { + FilterBypassImpl fbi = new FilterBypassImpl(transaction, caret, transaction.getDocument()); + naviFilter.setDot(fbi, dotPos.getOffset(), Position.Bias.Forward); + return fbi.getResult(); + } else { + return setDotAndMark(caret, dotPos, dotPos); + } } /** * Move dot of the given getCaret so getCaret selection gets created or changed. * - * @param caret non-null getCaret. + * @param caret Nebnon-null getCaret. * @param dotPos new dot position. * @return false if passed caret is obsolete or invalid (e.g. a member of another {@link EditorCaret}) * or true otherwise. */ public boolean moveDot(@NonNull CaretInfo caret, @NonNull Position dotPos) { - return transaction.moveDot(caret.getCaretItem(), dotPos); + NavigationFilter naviFilter = transaction.getCaret().getNavigationFilterNoDefault(getMoveOrigin()); + if (naviFilter != null) { + FilterBypassImpl fbi = new FilterBypassImpl(transaction, caret, transaction.getDocument()); + naviFilter.moveDot(fbi, dotPos.getOffset(), Position.Bias.Forward); + return fbi.getResult(); + } else { + return transaction.moveDot(caret.getCaretItem(), dotPos); + } } /** @@ -172,5 +187,18 @@ public Document getDocument() { return getComponent().getDocument(); } + + /** + * Describes the origin of the movement, the calling operation. Can be null + * if the origin could not be described. Should identify {@link MoveCaretsOrigin#DIRECT_NAVIGATION} + * actionType for simple navigation actions. + * + * @return movement description + * @see MoveCaretsOrigin + * @since 2.9 + */ + public @NonNull MoveCaretsOrigin getMoveOrigin() { + return transaction.getOrigin(); + } } diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java b/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java --- a/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java +++ b/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java @@ -119,12 +119,14 @@ private boolean fullResort; + private final MoveCaretsOrigin origin; - CaretTransaction(EditorCaret caret, JTextComponent component, Document doc) { + CaretTransaction(EditorCaret caret, JTextComponent component, Document doc, MoveCaretsOrigin origin) { this.editorCaret = caret; this.component = component; this.doc = doc; this.origCaretItems = editorCaret.getCaretItems(); + this.origin = origin; } @@ -265,6 +267,10 @@ // Current impl ignores possible replaceItems content - see getOriginalCarets() return editorCaret.getSortedCarets(); } + + MoveCaretsOrigin getOrigin() { + return origin; + } void replaceCarets(RemoveType removeType, int offset, CaretItem[] addCaretItems) { int size = origCaretItems.size(); diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java --- a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java +++ b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java @@ -41,6 +41,7 @@ */ package org.netbeans.api.editor.caret; +import org.netbeans.spi.editor.caret.CascadingNavigationFilter; import org.netbeans.spi.editor.caret.CaretMoveHandler; import java.awt.AlphaComposite; import java.awt.BasicStroke; @@ -76,7 +77,9 @@ import java.beans.PropertyChangeListener; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; @@ -103,10 +106,12 @@ import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.JTextComponent; +import javax.swing.text.NavigationFilter; import javax.swing.text.Position; import javax.swing.text.StyleConstants; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.editor.EditorUtilities; import org.netbeans.api.editor.document.AtomicLockDocument; import org.netbeans.api.editor.document.AtomicLockEvent; @@ -133,7 +138,9 @@ import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; import org.netbeans.modules.editor.lib2.view.ViewHierarchyListener; import org.netbeans.modules.editor.lib2.view.ViewUtils; +import org.netbeans.spi.editor.caret.NavigationFilterBypass; import org.openide.util.Exceptions; +import org.openide.util.Parameters; import org.openide.util.WeakListeners; /** @@ -159,6 +166,7 @@ // Temporary until rectangular selection gets ported to multi-caret support private static final String RECTANGULAR_SELECTION_PROPERTY = "rectangular-selection"; // NOI18N private static final String RECTANGULAR_SELECTION_REGIONS_PROPERTY = "rectangular-selection-regions"; // NOI18N + private static final String NAVIGATION_FILTER_PROPERTY = EditorCaret.class.getName() + ".navigationFilters"; // NOI18N // -J-Dorg.netbeans.api.editor.caret.EditorCaret.level=FINEST private static final Logger LOG = Logger.getLogger(EditorCaret.class.getName()); @@ -371,6 +379,11 @@ * with the same cursor. */ private boolean showingTextCursor = true; + + /** + * Bottom navigation filter, which delegates to the Component. + */ + private NavigationFilter chainNavigationFilter; public EditorCaret() { caretItems = new GapList<>(); @@ -504,6 +517,28 @@ * @see Caret#setDot(int) */ public @Override void setDot(final int offset) { + setDot(offset, MoveCaretsOrigin.DEFAULT); + } + + /** + * Assign a new offset to the caret and identify the operation which + * originated the caret movement. + *

+ * In addition to {@link #setDot(int)}, + * the caller may identify the operation that originated the caret movement. + * This information is received by {@link NavigationFilter}s or {@link ChangeListener}s + * and may be used to react or modify the caret movements. + *

+ * Use {@code null} or {@link MoveCaretsOrigin#DEFAULT} if the operation not known. Use + * {@link MoveCaretsOrigin#DIRECT_NAVIGATION} action type to identify simple navigational + * actions (pg up, pg down, left, right, ...). + *

+ * @param offset new offset for the caret + * @param orig specifies the operation which caused the caret to move. + * @see #setDot(int) + * @since 2.9 + */ + public void setDot(final int offset, MoveCaretsOrigin orig) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("setDot: offset=" + offset); //NOI18N if (LOG.isLoggable(Level.FINEST)) { @@ -523,7 +558,7 @@ } } } - }); + }, orig); } /** @@ -537,6 +572,29 @@ * @see Caret#moveDot(int) */ public @Override void moveDot(final int offset) { + moveDot(offset, MoveCaretsOrigin.DEFAULT); + } + + /** + * Moves the caret position (dot) to some other position, leaving behind the + * mark. + *

+ * In addition to {@link #setDot(int)}, + * the caller may identify the operation that originated the caret movement. + * This information is received by {@link NavigationFilter}s or {@link EditorCaretListener}s + * and may be used to react or modify the caret movements. + *

+ * Use {@code null} or {@link MoveCaretsOrigin#DEFAULT} if the operation not known. Use + * {@link MoveCaretsOrigin#DIRECT_NAVIGATION} action type to identify simple navigational + * actions (pg up, pg down, left, right, ...). + *

+ * + * @param offset new offset for the caret + * @param orig specifies the operation which caused the caret to move. + * @see #moveDot(int) + * @since 2.9 + */ + public void moveDot(final int offset, MoveCaretsOrigin orig) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("moveDot: offset=" + offset); //NOI18N } @@ -554,7 +612,7 @@ } } } - }); + }, orig); } /** @@ -593,8 +651,39 @@ * or no document installed in the text component. */ public int moveCarets(@NonNull CaretMoveHandler moveHandler) { + Parameters.notNull("moveHandler", moveHandler); return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, null, moveHandler); } + + /** + * Move multiple carets or create/modify selections, specifies the originating operation. + *

+ * In addition to {@link #moveCarets(org.netbeans.spi.editor.caret.CaretMoveHandler)}, the caller may specify + * what operation causes the caret movements in the `origin' parameter, see {@link MoveCaretsOrigin} class. + * This information is received by {@link NavigationFilter}s or {@link EditorCaretListener}s + * and may be used to react or modify the caret movements. + *

+ * Use {@code null} or {@link MoveCaretsOrigin#DEFAULT} if the operation not known. Use + * {@link MoveCaretsOrigin#DIRECT_NAVIGATION} action type to identify simple navigational + * actions (pg up, pg down, left, right, ...). + *

+ * See the {@link #moveCarets(org.netbeans.spi.editor.caret.CaretMoveHandler) } for detailed description of + * how carets are moved. + *

+ * + * @param moveHandler handler which moves individual carets + * @param origin description of the originating operation. Use {@code null} or {@link MoveCaretsOrigin#DEFAULT} for default/unspecified operation. + * @return difference between number of carets, see {@link #moveCarets(org.netbeans.spi.editor.caret.CaretMoveHandler)}. + * @see #moveCarets(org.netbeans.spi.editor.caret.CaretMoveHandler) + * @since 2.9 + */ + public int moveCarets(@NonNull CaretMoveHandler moveHandler, MoveCaretsOrigin origin) { + Parameters.notNull("moveHandler", moveHandler); + if (origin == null) { + origin = MoveCaretsOrigin.DEFAULT; + } + return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, null, moveHandler, origin); + } /** * Create a new caret at the given position with a possible selection. @@ -855,6 +944,8 @@ } listenerImpl.focusGained(null); // emulate focus gained } + + this.chainNavigationFilter = new ChainNavigationFilter(component); invalidateCaretBounds(0); dispatchUpdate(false); @@ -1036,6 +1127,174 @@ } } } + + /** + * Returns the navigation filter for a certain operation. + * {@link NavigationFilter} can be + * registered to receive only limited set of operations. This method returns the filter + * for the specified operation. Use {@link MoveCaretsOrigin#DEFAULT} to get text + * component's navigation filter (equivalent to {@link JTextComponent#getNavigationFilter() + * JTextComponent.getNavigationFilter()}. That filter receives all caret movements. + * @param origin the operation description + * @return the current navigation filter. + * @since 2.9 + */ + public @CheckForNull NavigationFilter getNavigationFilter(@NonNull MoveCaretsOrigin origin) { + Parameters.notNull("origin", origin); + if (origin == MoveCaretsOrigin.DEFAULT) { + return component.getNavigationFilter(); + } + NavigationFilter navi = doGetNavigationFilter(origin.getActionType()); + // Note: a special delegator is returned, since the component's navigation filter queue + // can be manipulated after call to getNavigationFilter. So if we would have returned the global filter instance directly, + // the calling client may unknowingly bypass certain (global) filters registered after call to this method. + // In other words, there are two possible insertion points into the navigation filter chanin + return navi != null ? navi : chainNavigationFilter; + } + + /** + * Variant of {@link #getNavigationFilter}, which does not default to chaining navigation + * filter + * @param origin operation specifier + * @return navigation filter or {@code null} + */ + @CheckForNull NavigationFilter getNavigationFilterNoDefault(@NonNull MoveCaretsOrigin origin) { + if (origin == MoveCaretsOrigin.DEFAULT) { + return component.getNavigationFilter(); + } + NavigationFilter navi2 = doGetNavigationFilter(origin.getActionType()); + return navi2 != null ? navi2 : component.getNavigationFilter(); + } + + /** + * Bottom navigation filter which delegates to the main NavigationFilter in the + * Component (if it exists). + */ + private static class ChainNavigationFilter extends NavigationFilter { + private final JTextComponent component; + + public ChainNavigationFilter(JTextComponent component) { + this.component = component; + } + + @Override + public int getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet) throws BadLocationException { + NavigationFilter chain = component.getNavigationFilter(); + return chain != null ? chain.getNextVisualPositionFrom(text, pos, bias, direction, biasRet) : super.getNextVisualPositionFrom(text, pos, bias, direction, biasRet); + } + + @Override + public void moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) { + NavigationFilter chain = component.getNavigationFilter(); + if (chain != null) { + chain.moveDot(fb, dot, bias); + } else { + super.moveDot(fb, dot, bias); + } + } + + @Override + public void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) { + NavigationFilter chain = component.getNavigationFilter(); + if (chain != null) { + chain.setDot(fb, dot, bias); + } else { + super.setDot(fb, dot, bias); + } + } + } + + /** + * Sets navigation filter for a certain operation type, defined by {@link MoveCaretsOrigin}. + *

+ * The registered filter will receive only those caret movements, which correspond to the + * passed {@link MoveCaretsOrigin}. To receive all caret movements, register for {@link MoveCaretsOrigin#DEFAULT} + * or use {@link JTextComponent#setNavigationFilter}. + *

+ * All the key part(s) of MoveCaretOrigin of a caret operation and `origin' parameter in this function must + * match in order for the filter to be invoked. + *

+ * The NavigationFilter implementation may downcast the passed {@link NavigationFilter.FilterBypass FilterBypass} + * parameter to {@link NavigationFilterBypass} to get full infomration about the movement. + *

+ * @param origin the origin + * @param naviFilter the installed filter + * @see JTextComponent#setNavigationFilter + * @see NavigationFilterBypass + * @since 2.9 + */ + public void setNavigationFilter(MoveCaretsOrigin origin, @NullAllowed NavigationFilter naviFilter) { + if (origin == null) { + origin = MoveCaretsOrigin.DEFAULT; + } + final NavigationFilter prev = getNavigationFilter(origin); + if (naviFilter != null) { + // Note: + // if the caller passes in a non-cascading filter, we would loose the filter chain information. + // the alien filter is wrapped by CascadingNavigationFilter delegator, so the previous filter + // link is preserved. + if (!(naviFilter instanceof CascadingNavigationFilter)) { + final NavigationFilter del = naviFilter; + naviFilter = new CascadingNavigationFilter() { + @Override + public void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) { + del.setDot(fb, dot, bias); + } + + @Override + public void moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) { + del.moveDot(fb, dot, bias); + } + + @Override + public int getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet) throws BadLocationException { + return del.getNextVisualPositionFrom(text, pos, bias, direction, biasRet); + } + }; + } + ((CascadingNavigationFilter)naviFilter).setOwnerAndPrevious(this, origin, prev); + } + if (MoveCaretsOrigin.DEFAULT == origin) { + component.setNavigationFilter(naviFilter); + } else { + doPutNavigationFilter(origin.getActionType(), prev); + } + } + + /** + * Records the navigation filter. Note that the filter is stored in the JTextComponent rather than + * in this Caret. If the Component's UI changes or the caret is recreated for some reason, the + * navigation filters remain registered. + * + * @param type type of nav filter + * @param n the filter instance + */ + private void doPutNavigationFilter(String type, NavigationFilter n) { + if (component == null) { + throw new IllegalStateException("Not attached to a Component"); + } + Map m = (Map)component.getClientProperty(NAVIGATION_FILTER_PROPERTY); + if (m == null) { + if (n == null) { + return; + } + m = new HashMap<>(); + component.putClientProperty(NAVIGATION_FILTER_PROPERTY, m); + } + if (n == null) { + m.remove(type); + } else { + m.put(type, n); + } + } + + private NavigationFilter doGetNavigationFilter(String n) { + if (component == null) { + throw new IllegalStateException("Not attached to a Component"); + } + Map m = (Map)component.getClientProperty(NAVIGATION_FILTER_PROPERTY); + return m == null ? null : m.get(n); + } @Override public int getBlinkRate() { @@ -1143,7 +1402,7 @@ moveDot(newDotOffset); // updates rs and fires state change } else { updateRectangularSelectionPaintRect(); - fireStateChanged(); + fireStateChanged(null); } } catch (BadLocationException ex) { // Leave selection as is @@ -1190,13 +1449,17 @@ * @return */ private int runTransaction(CaretTransaction.RemoveType removeType, int offset, CaretItem[] addCarets, CaretMoveHandler moveHandler) { + return runTransaction(removeType, offset, addCarets, moveHandler, MoveCaretsOrigin.DEFAULT); + } + + private int runTransaction(CaretTransaction.RemoveType removeType, int offset, CaretItem[] addCarets, CaretMoveHandler moveHandler, MoveCaretsOrigin org) { lock(); try { if (activeTransaction == null) { JTextComponent c = component; Document d = activeDoc; if (c != null && d != null) { - activeTransaction = new CaretTransaction(this, c, d); + activeTransaction = new CaretTransaction(this, c, d, org); if (LOG.isLoggable(Level.FINE)) { StringBuilder msgBuilder = new StringBuilder(200); msgBuilder.append("EditorCaret.runTransaction(): removeType=").append(removeType). @@ -1276,7 +1539,7 @@ } if (activeTransaction.isAnyChange()) { // For now clear the lists and use old way TODO update to selective updating and rendering - fireStateChanged(); + fireStateChanged(activeTransaction.getOrigin()); dispatchUpdate(true); resetBlink(); } @@ -1329,14 +1592,14 @@ /** * Notifies listeners that caret position has changed. */ - private void fireStateChanged() { + private void fireStateChanged(final MoveCaretsOrigin origin) { Runnable runnable = new Runnable() { public @Override void run() { JTextComponent c = component; if (c == null || c.getCaret() != EditorCaret.this) { return; } - fireEditorCaretChange(new EditorCaretEvent(EditorCaret.this, 0, Integer.MAX_VALUE)); // [TODO] temp firing without detailed info + fireEditorCaretChange(new EditorCaretEvent(EditorCaret.this, 0, Integer.MAX_VALUE, origin)); // [TODO] temp firing without detailed info ChangeEvent evt = new ChangeEvent(EditorCaret.this); List listeners = changeListenerList.getListeners(); for (ChangeListener l : listeners) { @@ -1883,7 +2146,7 @@ rsDotRect.x = r.x; rsDotRect.width = r.width; updateRectangularSelectionPaintRect(); - fireStateChanged(); + fireStateChanged(null); } } @@ -2120,7 +2383,7 @@ } else { // No rectangular selection RectangularSelectionTransferHandler.uninstall(component); } - fireStateChanged(); + fireStateChanged(null); } } } @@ -2177,7 +2440,7 @@ invalidateCaretBounds(offset); dispatchUpdate(); resetBlink(); - fireStateChanged(); + fireStateChanged(null); modified = false; } } else { diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java --- a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java +++ b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java @@ -41,6 +41,7 @@ */ package org.netbeans.api.editor.caret; +import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; /** @@ -55,10 +56,13 @@ private final int affectedEndOffset; - EditorCaretEvent(EditorCaret source, int affectedStartOffset, int affectedEndOffset) { + private final MoveCaretsOrigin origin; + + EditorCaretEvent(EditorCaret source, int affectedStartOffset, int affectedEndOffset, MoveCaretsOrigin origin) { super(source); this.affectedStartOffset = affectedStartOffset; this.affectedEndOffset = affectedEndOffset; + this.origin = origin; } /** @@ -91,4 +95,15 @@ public int getAffectedEndOffset() { return affectedEndOffset; } + + /** + * Describes the origin of the movement command. May not be filled, if the movement + * does not originate form an user-triggered editor action. + * + * @return the original reason for caret movement, or {@code null}. + * @since 2.9 + */ + public @CheckForNull MoveCaretsOrigin getOrigin() { + return origin; + } } diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/FilterBypassImpl.java b/editor.lib2/src/org/netbeans/api/editor/caret/FilterBypassImpl.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/FilterBypassImpl.java @@ -0,0 +1,244 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import org.netbeans.spi.editor.caret.NavigationFilterBypass; +import java.awt.Graphics; +import java.awt.Point; +import javax.swing.event.ChangeListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import org.openide.util.Exceptions; + +/** + * Implementation of the FilterBypass suitable for multi-caret environment. + * Provides a special {@link Caret} implementatio, which works with the current + * {@link CaretItem} being manipulated, so that if client calls {@link Caret#setDot} + * or the like, the instruction will be executed and bypasses all NavigationFilters. + * + * @author sdedic + */ +class FilterBypassImpl extends NavigationFilterBypass { + private final CaretTransaction transaction; + private final CaretInfo item; + private final Document doc; + private Caret itemCaret; + private boolean result; + + public FilterBypassImpl(CaretTransaction transaction, CaretInfo item, Document doc) { + this.transaction = transaction; + this.item = item; + this.doc = doc; + } + + @Override + public CaretInfo getCaretItem() { + return item; + } + + @Override + public EditorCaret getEditorCaret() { + return transaction.getCaret(); + } + + @Override + public Caret getCaret() { + if (itemCaret == null) { + itemCaret = new ItemCaret(); + } + return itemCaret; + } + + @Override + public MoveCaretsOrigin getOrigin() { + return transaction.getOrigin(); + } + + boolean getResult() { + return result; + } + + @Override + public void setDot(final int dot, Position.Bias bias) { + Position dotPos = createPosition(dot, bias); + result = transaction.setDotAndMark(item.getCaretItem(), + dotPos, dotPos); + } + + private Position createPosition(final int dot, Position.Bias bias) { + final Position[] p = new Position[1]; + + doc.render(new Runnable() { + public void run() { + p[0] = createPosition0(dot, null); + } + }); + return p[0]; + } + + private Position createPosition0(int dot, Position.Bias bias) { + try { + // FIXME: bias is not used. LineDocument should be used + // in preference to document to create biased positions. + // bypass handlers + if (dot < 0) { + return doc.createPosition(0); + } else if (dot > doc.getLength()) { + return doc.createPosition(doc.getLength()); + } else { + return doc.createPosition(dot); + } + } catch (BadLocationException ex) { + // should not happen, checked under doc lock + Exceptions.printStackTrace(ex); + return item.getDotPosition(); + } + } + + @Override + public void moveDot(int dot, Position.Bias bias) { + result = transaction.setDotAndMark(item.getCaretItem(), + createPosition(dot, bias), + item.getMarkPosition() + ); + } + + /** + * Custom caret implementation, which cleverly delegates to CaretItem + * and transaction to carry out most of Caret API tasks against the current + * CaretItem. + * Unsupported operations throw an exception. + */ + private class ItemCaret implements Caret { + private void notPermitted() { + throw new UnsupportedOperationException("Disallowed in NavigationFilter"); // NOI18N + } + @Override + public void install(JTextComponent c) { + notPermitted(); + } + + @Override + public void deinstall(JTextComponent c) { + notPermitted(); + } + + @Override + public void paint(Graphics g) { + notPermitted(); + } + + @Override + public void addChangeListener(ChangeListener l) { + transaction.getCaret().addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l) { + transaction.getCaret().removeChangeListener(l); + } + + @Override + public boolean isVisible() { + return transaction.getCaret().isVisible(); + } + + @Override + public void setVisible(boolean v) { + transaction.getCaret().setVisible(v); + } + + @Override + public boolean isSelectionVisible() { + return transaction.getCaret().isSelectionVisible(); + } + + @Override + public void setSelectionVisible(boolean v) { + transaction.getCaret().setSelectionVisible(v); + } + + @Override + public void setMagicCaretPosition(Point p) { + transaction.setMagicCaretPosition(item.getCaretItem(), p); + } + + @Override + public Point getMagicCaretPosition() { + return item.getMagicCaretPosition(); + } + + @Override + public void setBlinkRate(int rate) { + transaction.getCaret().setBlinkRate(rate); + } + + @Override + public int getBlinkRate() { + return transaction.getCaret().getBlinkRate(); + } + + @Override + public int getDot() { + return item.getDot(); + } + + @Override + public int getMark() { + return item.getMark(); + } + + @Override + public void setDot(int dot) { + FilterBypassImpl.this.setDot(dot, Position.Bias.Forward); + } + + @Override + public void moveDot(int dot) { + FilterBypassImpl.this.moveDot(dot, Position.Bias.Forward); + } + + } +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/MoveCaretsOrigin.java b/editor.lib2/src/org/netbeans/api/editor/caret/MoveCaretsOrigin.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/MoveCaretsOrigin.java @@ -0,0 +1,178 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.spi.editor.caret.NavigationFilterBypass; +import org.openide.util.Parameters; + +/** + * Describes the operation which initiated the caret navigation. + *

+ * The core implementation + * supports operation type (token) as a key and original intended movement direction as a hint + * for possible filtering. Future API versions may define more details. + *

+ * This class is used in two modes: + *

+ *
    + *
  1. When moving a caret ({@link EditorCaret#setDot(int, org.netbeans.api.editor.caret.MoveCaretsOrigin) setDot(pos, origin)}, + * {@link EditorCaret#moveDot(int, org.netbeans.api.editor.caret.MoveCaretsOrigin) moveDot(pos, origin)}, + * {@link EditorCaret#moveCarets(org.netbeans.spi.editor.caret.CaretMoveHandler, org.netbeans.api.editor.caret.MoveCaretsOrigin) moveCarets(handler, origin)}), + * as a information on the originating operation ({@link #getActionType getActionType()}) and possibly additional + * hints that describes the intended caret move ({@link #getDirection() getDirection()}). + *
  2. + *
  3. + * When {@link EditorCaret#setNavigationFilter(org.netbeans.api.editor.caret.MoveCaretsOrigin, javax.swing.text.NavigationFilter) registering a NavigationFilter} + * as a filtering template. + *
  4. + *
+ * + *
+ *

+ * The intended usage in caret moving code (actions) is as follows: + *

+ *
+ * // Action perform method
+ * editorCaret.moveCarets(new CaretMoveHandler() {
+ *      @Override
+ *      public void moveCarets(CaretMoveContext context) {
+ *          ...
+ *      }
+ *  }, new MoveCaretsOrigin(
+ *          // The action is a raw movement command
+ *          MoveCaretsOrigin.DIRECT_NAVIGATION, 
+ *          // The approximate direction of the movement; can be 0.
+ *          SwingConstants.NORTH)
+ *  );
+ * 
+ *

+ * If a {@link javax.swing.text.NavigationFilter} only wants to intercept certain type of moevements, it can register as follows: + *

+ *
+ * EditorCaret eCaret = .... ; // obtain EditorCaret
+ * eCaret.setNavigationFilter(
+ *   new NavigationFilter() {
+ *          // navigation filter implementation, not important for the example
+ *   }, 
+ *   new MoveCaretsOrigin(MoveCaretsOrigin.DIRECT_NAVIGATION)
+ * );
+ * 
+ *

+ * If the NavigationFilter implementation wants to obtain the extended information for the caret movement, + * it can downcast the received FilterBypass: + *

+ *
+ *  public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
+ *    if (fb instanceof NavigationFilterBypass) {
+ *      NavigationFilterBypass nfb = (NavigationFilterBypass)fb;
+ * 
+ *      // get the Origin object created by the caret-moving operation, can query the details
+ *      MoveCaretsOrigin origin = nfb.getOrigin();
+ * 
+ *      // get the individual caret in multi-caret scenario
+ *      CaretInfo info = nfb.getCaretInfo();
+ * 
+ *      // get the whole EditorCaret
+ *      EditorCaret eCaret = nfb.getEditorCaret();
+ *    }
+ *  }
+ * 
+ *
+ * @see NavigationFilterBypass + * @since 2.9 + */ +public final class MoveCaretsOrigin { + /** + * Actions, which are defined as moving or setting the caret. Do not user for actions + * like search (moves caret to the found string), goto type (moves to the definition) etc. + */ + public static final String DIRECT_NAVIGATION = "navigation.action"; // NOI18N + /** + * Undefined action type. Use this description when registering for all possible + * caret movements. + */ + public static final MoveCaretsOrigin DEFAULT = new MoveCaretsOrigin("default", 0); // NOI18N + private final String actionType; + private final int direction; + + /** + * Describes the origin by just the action type. + * @param actionType action type + */ + public MoveCaretsOrigin(String actionType) { + this.actionType = actionType; + this.direction = 0; + } + + /** + * Specifies the origin by action type, and the overall moving direction + * @param actionType action type + * @param direction the intended direction of movement + */ + public MoveCaretsOrigin(@NonNull String actionType, int direction) { + Parameters.notNull("actionType", actionType); // NOI18N + this.actionType = actionType; + this.direction = direction; + } + + /** + * Returns the type of the action which originated the caret movement. + * @return the action type. + */ + @NonNull + public String getActionType() { + return actionType; + } + + /** + * Specifies the desired movement direction. Use {@link javax.swing.SwingConstants} + * compass constants to specify the direction. 0 means the direction + * is unspecified. + * + * @return The initial direction of movemnet + */ + public int getDirection() { + return direction; + } + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/package.html b/editor.lib2/src/org/netbeans/api/editor/caret/package.html --- a/editor.lib2/src/org/netbeans/api/editor/caret/package.html +++ b/editor.lib2/src/org/netbeans/api/editor/caret/package.html @@ -131,13 +131,105 @@ concurrently.

+

Reason of Caret Movement

+

+ It may be desirable (especially for NavigationListeners, but possibly for + EditorCaretListeners too) to determine + what was the reason leading to caret movement. The caller of Caret API which intends to position the caret + may provide an optional MoveCaretsOrigin instance to Caret API methods to indicate + type of action leading to the movement. + This description is used to select NavigationFilters which receive and can modify the movement, and is available + to all invoked NavigationFilters or EditorCaretListeners contacted by the Caret API operation. +

+

+ Only one action type is currently defined by the + API - DIRECT_NAVIGATION. This type identifies operations + actions, whose only task is to reposition the caret (i.e. left/right, page-up, document-begin) from actions, + which position caret as a part of larger task (i.e. search, goto type, ...). +

+ +

Navigation Filters

+

+ The Caret API implements and extends the concept of swing + NavigationFilters. The Filter + gets notified on CaretInfo movement; so if more Carets are present, + a NavigationFilter will see all their moves. Navigation Filters are + called with a special instance of + FilterBypass extending + NavigationFilterBypass. The Filter can downcast the FilterBypass to access extended information + about the current caret's movement: +

+
+

+    public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
+      if (fb instanceof NavigationFilterBypass) {
+        NavigationFilterBypass nfb = (NavigationFilterBypass)fb;
+   
+        // get the Origin object created by the caret-moving operation, can query the details
+        MoveCaretsOrigin origin = nfb.getOrigin();
+   
+        // get the individual caret in multi-caret scenario
+        CaretInfo info = nfb.getCaretInfo();
+   
+        // get the whole EditorCaret
+        EditorCaret eCaret = nfb.getEditorCaret();
+      }
+    }
+    
+
+

+ Navigation filters can be also selectively registered for only certain type of actions described by + MoveCaretsOrigin instance. +

+
+

+        EditorCaret eCaret = .... ; // obtain EditorCaret
+        eCaret.setNavigationFilter(
+          new NavigationFilter() {
+                 // navigation filter implementation, not important for the example
+          }, 
+          new MoveCaretsOrigin(MoveCaretsOrigin.DIRECT_NAVIGATION)
+        );
+    
+
+

+ Such filters are only called if the caller of Caret API provides a suitable MoveCaretsOrigin description of the move operatoion. +

+
+
+          
+        // Action perform method
+        editorCaret.moveCarets(new CaretMoveHandler() {
+             @Override
+             public void moveCarets(CaretMoveContext context) {
+                 ...
+             }
+         }, new MoveCaretsOrigin(
+                 // The action is a raw movement command
+                 MoveCaretsOrigin.DIRECT_NAVIGATION, 
+                 // The approximate direction of the movement; can be 0.
+                 SwingConstants.NORTH)
+         );
+        
+      
+
+ Swing NavigationFilters, specified by TextComponent.setNavigationFilter() + are called for all caret movements. +

+ Abstract boilerplate for NavigationFilters is created, + CascadingNavigationFilter + which allows to chain individual filters. Implementors are encouraged to use it, or otherwise + pass the control to previously registered NavigationFilter in case the movement + event is not handled by the custom implementation. +

+

Backwards compatibility

As a technical limitation, EditorCaret has to implement Caret to be able to work with Swings Text API. The Caret interface is not aware of multiple carets and a call to setDot(int) will only retain a single caret. For multiple carets a call to moveDot(int) will move the last - caret only (but it retains other existing carets). + caret only (but it retains other existing carets).+
editorTextComponent.getCaret() will now always return EditorCaret instance instead of the original caret implementation org.netbeans.editor.BaseCaret. diff --git a/editor.lib2/src/org/netbeans/spi/editor/caret/CascadingNavigationFilter.java b/editor.lib2/src/org/netbeans/spi/editor/caret/CascadingNavigationFilter.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/spi/editor/caret/CascadingNavigationFilter.java @@ -0,0 +1,176 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.caret; + +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; +import javax.swing.text.NavigationFilter; +import javax.swing.text.Position; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.editor.caret.EditorCaret; +import org.netbeans.api.editor.caret.MoveCaretsOrigin; +import org.openide.util.Parameters; + +/** + * Boilerplate {@link NavigationFilter}, which supports chaining of filters + * on an JTextComponent. + *

+ * The implementation should call super methods to + * allow lower-precedence filters to react. If the implementation desires to + * disable the filters and take the movement action directly, it can still use + * the {@link FilterBypass} instance passed. + *

+ * There are helper {@link #register} and {@link #unregister} methods which + * ensure the chain of filters is correctly maintained. After registering, methods + * implemented by this class will delegate to the remembered formerly-toplevel filter. + * Implementor of this class may safely call super.* methods to delegate to filters + * further in the chain. + *

+ * + * @author sdedic + * @since 2.9 + */ +public abstract class CascadingNavigationFilter extends NavigationFilter { + private NavigationFilter previous; + private EditorCaret owner; + private MoveCaretsOrigin regKey; + + /** + * Returns the next filter in the chain. This class' implementations of NavigationFilter + * API methods delegate to that filter, if non-null. Results after this Filter is + * unregistered (removed from the NavigationFilter) chain + * are undefined. + * + * @return next NavigationFilter. + */ + protected final NavigationFilter getNextFilter() { + return previous; + } + + @Override + public int getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet) throws BadLocationException { + return previous != null ? + previous.getNextVisualPositionFrom(text, pos, bias, direction, biasRet) : + super.getNextVisualPositionFrom(text, pos, bias, direction, biasRet); + } + + @Override + public void moveDot(FilterBypass fb, int dot, Position.Bias bias) { + if (previous != null) { + previous.moveDot(fb, dot, bias); + } else { + super.moveDot(fb, dot, bias); + } + } + + @Override + public void setDot(FilterBypass fb, int dot, Position.Bias bias) { + if (previous != null) { + previous.setDot(fb, dot, bias); + } else { + super.setDot(fb, dot, bias); + } + } + + /** + * Removes this NavigationFilter from the chain; preceding filter will + * be connected to the following one, so the chain will not be broken. + */ + public final void unregister() { + if (regKey == null) { + // not registered + return; + } + NavigationFilter f = owner.getNavigationFilter(regKey); + CascadingNavigationFilter next = null; + + while (f instanceof CascadingNavigationFilter && f != this) { + next = (CascadingNavigationFilter)f; + f = next.getNextFilter(); + } + if (f != this) { + return; + } + if (next == null) { + owner.setNavigationFilter(regKey, previous); + } else { + next.previous = previous; + } + // reset state + this.owner = null; + this.previous = null; + } + + /** + * Registers this Filter into the NavigationFilter chain. + *

+ * This filter will + * be placed on top of the filter's chain and the formerly-toplevel filter will + * be remembered for delegation. + *

+ * It is not permitted to register with more carets; make multiple instances of + * the filter for that case. + *

+ * + * @param caret where this Filter should be registered. + * @param origin operation specifier + */ + public final void register( + @NonNull EditorCaret caret, + @NonNull MoveCaretsOrigin origin) { + Parameters.notNull("caret", caret); + Parameters.notNull("origin", origin); + if (owner != null) { + throw new IllegalStateException(); + } + caret.setNavigationFilter(origin, this); + } + + public void setOwnerAndPrevious(EditorCaret ec, MoveCaretsOrigin orig, NavigationFilter prev) { + if (this.owner != null) { + throw new IllegalStateException("Can be registered only once"); + } + this.owner = ec; + this.previous = prev; + this.regKey = orig; + } +} diff --git a/editor.lib2/src/org/netbeans/spi/editor/caret/NavigationFilterBypass.java b/editor.lib2/src/org/netbeans/spi/editor/caret/NavigationFilterBypass.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/spi/editor/caret/NavigationFilterBypass.java @@ -0,0 +1,82 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.caret; + +import javax.swing.text.NavigationFilter; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.editor.caret.CaretInfo; +import org.netbeans.api.editor.caret.EditorCaret; +import org.netbeans.api.editor.caret.MoveCaretsOrigin; + +/** + * Enhanced FilterBypass which understands multicaret. + *

+ * Implementations of + * {@link NavigationFilter} may check if the FilterBypass is instanceof this class, + * and if so, they can access extended information. + *

+ * If the caret move operation is initiated by new caret APIs, the FilterBypass passed + * to NavigationFilters always satisfies this interface. + *

+ * @author sdedic + * @since 2.9 + */ +public abstract class NavigationFilterBypass extends NavigationFilter.FilterBypass { + /** + * Returns the currently changing CaretItem. + * + * @return CaretItem the caret instance being changed + */ + public abstract @NonNull CaretInfo getCaretItem(); + + /** + * Access to the entire EditorCaret abstraction + * @return the editor caret + */ + public abstract @NonNull EditorCaret getEditorCaret(); + + /** + * Describes the origin / reason of the movement. + * @return The origin object provided by the caret movement initiator. + */ + public abstract @NonNull MoveCaretsOrigin getOrigin(); +} diff --git a/editor.lib2/src/org/netbeans/spi/editor/caret/package.html b/editor.lib2/src/org/netbeans/spi/editor/caret/package.html --- a/editor.lib2/src/org/netbeans/spi/editor/caret/package.html +++ b/editor.lib2/src/org/netbeans/spi/editor/caret/package.html @@ -70,7 +70,23 @@ }); - + +

Navigation Filters

+

+ A boilerplate CascadingNavigationFilter is provided to + make implementation of NavigationFilters easier. The boilerplate + remembers the preceding filter and will delegate to it. If you create a subclass, you may call super methods + moveDot and setDot to delegate to that previous filter and ultimately perform the action. Calling + methods of FilterBypass will perform the caret action + directly. +

+

+ The filter can find out the origin of the movement or + the info for actual caret being moved. The + FilterBypass implementation passed to + NavigationFilter can be downcasted to NavigationFilterBypass, + which provides this extended information. +

Backwards compatibility

Use cases