/*
* @(#)DefaultCaret.java 1.148 06/11/30
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package javax.swing.text;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.beans.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.*;
import java.util.EventListener;
import sun.swing.SwingUtilities2;
/**
* A default implementation of Caret. The caret is rendered as
* a vertical line in the color specified by the CaretColor property
* of the associated JTextComponent. It can blink at the rate specified
* by the BlinkRate property.
* <p>
* This implementation expects two sources of asynchronous notification.
* The timer thread fires asynchronously, and causes the caret to simply
* repaint the most recent bounding box. The caret also tracks change
* as the document is modified. Typically this will happen on the
* event dispatch thread as a result of some mouse or keyboard event.
* The caret behavior on both synchronous and asynchronous documents updates
* is controlled by <code>UpdatePolicy</code> property. The repaint of the
* new caret location will occur on the event thread in any case, as calls to
* <code>modelToView</code> are only safe on the event thread.
* <p>
* The caret acts as a mouse and focus listener on the text component
* it has been installed in, and defines the caret semantics based upon
* those events. The listener methods can be reimplemented to change the
* semantics.
* By default, the first mouse button will be used to set focus and caret
* position. Dragging the mouse pointer with the first mouse button will
* sweep out a selection that is contiguous in the model. If the associated
* text component is editable, the caret will become visible when focus
* is gained, and invisible when focus is lost.
* <p>
* The Highlighter bound to the associated text component is used to
* render the selection by default.
* Selection appearance can be customized by supplying a
* painter to use for the highlights. By default a painter is used that
* will render a solid color as specified in the associated text component
* in the <code>SelectionColor</code> property. This can easily be changed
* by reimplementing the
* <a href="#getSelectionHighlighter">getSelectionHighlighter</a>
* method.
* <p>
* A customized caret appearance can be achieved by reimplementing
* the paint method. If the paint method is changed, the damage method
* should also be reimplemented to cause a repaint for the area needed
* to render the caret. The caret extends the Rectangle class which
* is used to hold the bounding box for where the caret was last rendered.
* This enables the caret to repaint in a thread-safe manner when the
* caret moves without making a call to modelToView which is unstable
* between model updates and view repair (i.e. the order of delivery
* to DocumentListeners is not guaranteed).
* <p>
* The magic caret position is set to null when the caret position changes.
* A timer is used to determine the new location (after the caret change).
* When the timer fires, if the magic caret position is still null it is
* reset to the current caret position. Any actions that change
* the caret position and want the magic caret position to remain the
* same, must remember the magic caret position, change the cursor, and
* then set the magic caret position to its original value. This has the
* benefit that only actions that want the magic caret position to persist
* (such as open/down) need to know about it.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans<sup><font size="-2">TM</font></sup>
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*
* @author Timothy Prinzing
* @version 1.148 11/30/06
* @see Caret
*/
public class DefaultCaret extends Rectangle implements Caret, FocusListener, MouseListener, MouseMotionListener {
/**
* Indicates that the caret position is to be updated only when
* document changes are performed on the Event Dispatching Thread.
* @see #setUpdatePolicy
* @see #getUpdatePolicy
* @since 1.5
*/
public static final int UPDATE_WHEN_ON_EDT = 0;
/**
* Indicates that the caret should remain at the same
* absolute position in the document regardless of any document
* updates, except when the document length becomes less than
* the current caret position due to removal. In that case the caret
* position is adjusted to the end of the document.
*
* @see #setUpdatePolicy
* @see #getUpdatePolicy
* @since 1.5
*/
public static final int NEVER_UPDATE = 1;
/**
* Indicates that the caret position is to be <b>always</b>
* updated accordingly to the document changes regardless whether
* the document updates are performed on the Event Dispatching Thread
* or not.
*
* @see #setUpdatePolicy
* @see #getUpdatePolicy
* @since 1.5
*/
public static final int ALWAYS_UPDATE = 2;
/**
* Constructs a default caret.
*/
public DefaultCaret() {
}
/**
* Sets the caret movement policy on the document updates. Normally
* the caret updates its absolute position within the document on
* insertions occurred before or at the caret position and
* on removals before the caret position. 'Absolute position'
* means here the position relative to the start of the document.
* For example if
* a character is typed within editable text component it is inserted
* at the caret position and the caret moves to the next absolute
* position within the document due to insertion and if
* <code>BACKSPACE</code> is typed then caret decreases its absolute
* position due to removal of a character before it. Sometimes
* it may be useful to turn off the caret position updates so that
* the caret stays at the same absolute position within the
* document position regardless of any document updates.
* <p>
* The following update policies are allowed:
* <ul>
* <li><code>NEVER_UPDATE</code>: the caret stays at the same
* absolute position in the document regardless of any document
* updates, except when document length becomes less than
* the current caret position due to removal. In that case caret
* position is adjusted to the end of the document.
* The caret doesn't try to keep itself visible by scrolling
* the associated view when using this policy. </li>
* <li><code>ALWAYS_UPDATE</code>: the caret always tracks document
* changes. For regular changes it increases its position
* if an insertion occurs before or at its current position,
* and decreases position if a removal occurs before
* its current position. For undo/redo updates it is always
* moved to the position where update occurred. The caret
* also tries to keep itself visible by calling
* <code>adjustVisibility</code> method.</li>
* <li><code>UPDATE_WHEN_ON_EDT</code>: acts like <code>ALWAYS_UPDATE</code>
* if the document updates are performed on the Event Dispatching Thread
* and like <code>NEVER_UPDATE</code> if updates are performed on
* other thread. </li>
* </ul> <p>
* The default property value is <code>UPDATE_WHEN_ON_EDT</code>.
*
* @param policy one of the following values : <code>UPDATE_WHEN_ON_EDT</code>,
* <code>NEVER_UPDATE</code>, <code>ALWAYS_UPDATE</code>
* @throws IllegalArgumentException if invalid value is passed
*
* @see #getUpdatePolicy
* @see #adjustVisibility
* @see #UPDATE_WHEN_ON_EDT
* @see #NEVER_UPDATE
* @see #ALWAYS_UPDATE
*
* @since 1.5
*/
public void setUpdatePolicy(int policy) {
updatePolicy = policy;
}
/**
* Gets the caret movement policy on document updates.
*
* @return one of the following values : <code>UPDATE_WHEN_ON_EDT</code>,
* <code>NEVER_UPDATE</code>, <code>ALWAYS_UPDATE</code>
*
* @see #setUpdatePolicy
* @see #UPDATE_WHEN_ON_EDT
* @see #NEVER_UPDATE
* @see #ALWAYS_UPDATE
*
* @since 1.5
*/
public int getUpdatePolicy() {
return updatePolicy;
}
/**
* Gets the text editor component that this caret is
* is bound to.
*
* @return the component
*/
protected final JTextComponent getComponent() {
return component;
}
/**
* Cause the caret to be painted. The repaint
* area is the bounding box of the caret (i.e.
* the caret rectangle or <em>this</em>).
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
* to Use Threads</A> for more information.
*/
protected final synchronized void repaint() {
if (component != null) {
component.repaint(x, y, width, height);
}
}
/**
* Damages the area surrounding the caret to cause
* it to be repainted in a new location. If paint()
* is reimplemented, this method should also be
* reimplemented. This method should update the
* caret bounds (x, y, width, and height).
*
* @param r the current location of the caret
* @see #paint
*/
protected synchronized void damage(Rectangle r) {
if (r != null) {
int damageWidth = getCaretWidth(r.height);
x = r.x - 4 - (damageWidth >> 1);
y = r.y;
width = 9 + damageWidth;
height = r.height;
repaint();
}
}
/**
* Scrolls the associated view (if necessary) to make
* the caret visible. Since how this should be done
* is somewhat of a policy, this method can be
* reimplemented to change the behavior. By default
* the scrollRectToVisible method is called on the
* associated component.
*
* @param nloc the new position to scroll to
*/
protected void adjustVisibility(Rectangle nloc) {
if(component == null) {
return;
}
if (SwingUtilities.isEventDispatchThread()) {
component.scrollRectToVisible(nloc);
} else {
SwingUtilities.invokeLater(new SafeScroller(nloc));
}
}
/**
* Gets the painter for the Highlighter.
*
* @return the painter
*/
protected Highlighter.HighlightPainter getSelectionPainter() {
return DefaultHighlighter.DefaultPainter;
}
/**
* Tries to set the position of the caret from
* the coordinates of a mouse event, using viewToModel().
*
* @param e the mouse event
*/
protected void positionCaret(MouseEvent e) {
Point pt = new Point(e.getX(), e.getY());
Position.Bias[] biasRet = new Position.Bias[1];
int pos = component.getUI().viewToModel(component, pt, biasRet);
if(biasRet[0] == null)
biasRet[0] = Position.Bias.Forward;
if (pos >= 0) {
setDot(pos, biasRet[0]);
}
}
/**
* Tries to move the position of the caret from
* the coordinates of a mouse event, using viewToModel().
* This will cause a selection if the dot and mark
* are different.
*
* @param e the mouse event
*/
protected void moveCaret(MouseEvent e) {
Point pt = new Point(e.getX(), e.getY());
Position.Bias[] biasRet = new Position.Bias[1];
int pos = component.getUI().viewToModel(component, pt, biasRet);
if(biasRet[0] == null)
biasRet[0] = Position.Bias.Forward;
if (pos >= 0) {
moveDot(pos, biasRet[0]);
}
}
// --- FocusListener methods --------------------------
/**
* Called when the component containing the caret gains
* focus. This is implemented to set the caret to visible
* if the component is editable.
*
* @param e the focus event
* @see FocusListener#focusGained
*/
public void focusGained(FocusEvent e) {
if (component.isEnabled()) {
if (component.isEditable()) {
setVisible(true);
}
setSelectionVisible(true);
}
}
/**
* Called when the component containing the caret loses
* focus. This is implemented to set the caret to visibility
* to false.
*
* @param e the focus event
* @see FocusListener#focusLost
*/
public void focusLost(FocusEvent e) {
setVisible(false);
setSelectionVisible(ownsSelection || e.isTemporary());
}
/**
* Selects word based on the MouseEvent
*/
private void selectWord(MouseEvent e) {
if (selectedWordEvent != null
&& selectedWordEvent.getX() == e.getX()
&& selectedWordEvent.getY() == e.getY()) {
//we already done selection for this
return;
}
Action a = null;
ActionMap map = getComponent().getActionMap();
if (map != null) {
a = map.get(DefaultEditorKit.selectWordAction);
}
if (a == null) {
if (selectWord == null) {
selectWord = new DefaultEditorKit.SelectWordAction();
}
a = selectWord;
}
a.actionPerformed(new ActionEvent(getComponent(),
ActionEvent.ACTION_PERFORMED, null, e.getWhen(), e.getModifiers()));
selectedWordEvent = e;
}
// --- MouseListener methods -----------------------------------
/**
* Called when the mouse is clicked. If the click was generated
* from button1, a double click selects a word,
* and a triple click the current line.
*
* @param e the mouse event
* @see MouseListener#mouseClicked
*/
public void mouseClicked(MouseEvent e) {
int nclicks = SwingUtilities2.getAdjustedClickCount(getComponent(), e);
if (! e.isConsumed()) {
if (SwingUtilities.isLeftMouseButton(e)) {
// mouse 1 behavior
if(nclicks == 1) {
selectedWordEvent = null;
} else if(nclicks == 2
&& SwingUtilities2.canEventAccessSystemClipboard(e)) {
selectWord(e);
selectedWordEvent = null;
} else if(nclicks == 3
&& SwingUtilities2.canEventAccessSystemClipboard(e)) {
Action a = null;
ActionMap map = getComponent().getActionMap();
if (map != null) {
a = map.get(DefaultEditorKit.selectLineAction);
}
if (a == null) {
if (selectLine == null) {
selectLine = new DefaultEditorKit.SelectLineAction();
}
a = selectLine;
}
a.actionPerformed(new ActionEvent(getComponent(),
ActionEvent.ACTION_PERFORMED, null, e.getWhen(), e.getModifiers()));
}
} else if (SwingUtilities.isMiddleMouseButton(e)) {
// mouse 2 behavior
if (nclicks == 1 && component.isEditable() && component.isEnabled()
&& SwingUtilities2.canEventAccessSystemClipboard(e)) {
// paste system selection, if it exists
JTextComponent c = (JTextComponent) e.getSource();
if (c != null) {
try {
Toolkit tk = c.getToolkit();
Clipboard buffer = tk.getSystemSelection();
if (buffer != null) {
// platform supports system selections, update it.
adjustCaret(e);
TransferHandler th = c.getTransferHandler();
if (th != null) {
Transferable trans = null;
try {
trans = buffer.getContents(null);
} catch (IllegalStateException ise) {
// clipboard was unavailable
UIManager.getLookAndFeel().provideErrorFeedback(c);
}
if (trans != null) {
th.importData(c, trans);
}
}
adjustFocus(true);
}
} catch (HeadlessException he) {
// do nothing... there is no system clipboard
}
}
}
}
}
}
/**
* If button 1 is pressed, this is implemented to
* request focus on the associated text component,
* and to set the caret position. If the shift key is held down,
* the caret will be moved, potentially resulting in a selection,
* otherwise the
* caret position will be set to the new location. If the component
* is not enabled, there will be no request for focus.
*
* @param e the mouse event
* @see MouseListener#mousePressed
*/
public void mousePressed(MouseEvent e) {
int nclicks = SwingUtilities2.getAdjustedClickCount(getComponent(), e);
if (SwingUtilities.isLeftMouseButton(e)) {
if (e.isConsumed()) {
shouldHandleRelease = true;
} else {
shouldHandleRelease = false;
adjustCaretAndFocus(e);
if (nclicks == 2
&& SwingUtilities2.canEventAccessSystemClipboard(e)) {
selectWord(e);
}
}
}
}
void adjustCaretAndFocus(MouseEvent e) {
adjustCaret(e);
adjustFocus(false);
}
/**
* Adjusts the caret location based on the MouseEvent.
*/
private void adjustCaret(MouseEvent e) {
if ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0 &&
getDot() != -1) {
moveCaret(e);
} else {
positionCaret(e);
}
}
/**
* Adjusts the focus, if necessary.
*
* @param inWindow if true indicates requestFocusInWindow should be used
*/
private void adjustFocus(boolean inWindow) {
if ((component != null) && component.isEnabled() &&
component.isRequestFocusEnabled()) {
if (inWindow) {
component.requestFocusInWindow();
}
else {
component.requestFocus();
}
}
}
/**
* Called when the mouse is released.
*
* @param e the mouse event
* @see MouseListener#mouseReleased
*/
public void mouseReleased(MouseEvent e) {
if (!e.isConsumed()
&& shouldHandleRelease
&& SwingUtilities.isLeftMouseButton(e)) {
adjustCaretAndFocus(e);
}
}
/**
* Called when the mouse enters a region.
*
* @param e the mouse event
* @see MouseListener#mouseEntered
*/
public void mouseEntered(MouseEvent e) {
}
/**
* Called when the mouse exits a region.
*
* @param e the mouse event
* @see MouseListener#mouseExited
*/
public void mouseExited(MouseEvent e) {
}
// --- MouseMotionListener methods -------------------------
/**
* Moves the caret position
* according to the mouse pointer's current
* location. This effectively extends the
* selection. By default, this is only done
* for mouse button 1.
*
* @param e the mouse event
* @see MouseMotionListener#mouseDragged
*/
public void mouseDragged(MouseEvent e) {
if ((! e.isConsumed()) && SwingUtilities.isLeftMouseButton(e)) {
moveCaret(e);
}
}
/**
* Called when the mouse is moved.
*
* @param e the mouse event
* @see MouseMotionListener#mouseMoved
*/
public void mouseMoved(MouseEvent e) {
}
// ---- Caret methods ---------------------------------
/**
* Renders the caret as a vertical line. If this is reimplemented
* the damage method should also be reimplemented as it assumes the
* shape of the caret is a vertical line. Sets the caret color to
* the value returned by getCaretColor().
* <p>
* If there are multiple text directions present in the associated
* document, a flag indicating the caret bias will be rendered.
* This will occur only if the associated document is a subclass
* of AbstractDocument and there are multiple bidi levels present
* in the bidi element structure (i.e. the text has multiple
* directions associated with it).
*
* @param g the graphics context
* @see #damage
*/
public void paint(Graphics g) {
if(isVisible()) {
try {
TextUI mapper = component.getUI();
Rectangle r = mapper.modelToView(component, dot, dotBias);
if ((r == null) || ((r.width == 0) && (r.height == 0))) {
return;
}
if (width > 0 && height > 0 &&
!this._contains(r.x, r.y, r.width, r.height)) {
// We seem to have gotten out of sync and no longer
// contain the right location, adjust accordingly.
Rectangle clip = g.getClipBounds();
if (clip != null && !clip.contains(this)) {
// Clip doesn't contain the old location, force it
// to be repainted lest we leave a caret around.
repaint();
}
// This will potentially cause a repaint of something
// we're already repainting, but without changing the
// semantics of damage we can't really get around this.
damage(r);
}
g.setColor(component.getCaretColor());
int paintWidth = getCaretWidth(r.height);
r.x -= paintWidth >> 1;
g.fillRect(r.x, r.y, paintWidth, r.height);
// see if we should paint a flag to indicate the bias
// of the caret.
// PENDING(prinz) this should be done through
// protected methods so that alternative LAF
// will show bidi information.
Document doc = component.getDocument();
if (doc instanceof AbstractDocument) {
Element bidi = ((AbstractDocument)doc).getBidiRootElement();
if ((bidi != null) && (bidi.getElementCount() > 1)) {
// there are multiple directions present.
flagXPoints[0] = r.x + ((dotLTR) ? paintWidth : 0);
flagYPoints[0] = r.y;
flagXPoints[1] = flagXPoints[0];
flagYPoints[1] = flagYPoints[0] + 4;
flagXPoints[2] = flagXPoints[0] + ((dotLTR) ? 4 : -4);
flagYPoints[2] = flagYPoints[0];
g.fillPolygon(flagXPoints, flagYPoints, 3);
}
}
} catch (BadLocationException e) {
// can't render I guess
//System.err.println("Can't render cursor");
}
}
}
/**
* Called when the UI is being installed into the
* interface of a JTextComponent. This can be used
* to gain access to the model that is being navigated
* by the implementation of this interface. Sets the dot
* and mark to 0, and establishes document, property change,
* focus, mouse, and mouse motion listeners.
*
* @param c the component
* @see Caret#install
*/
public void install(JTextComponent c) {
component = c;
Document doc = c.getDocument();
dot = mark = 0;
dotLTR = markLTR = true;
dotBias = markBias = Position.Bias.Forward;
if (doc != null) {
doc.addDocumentListener(handler);
}
c.addPropertyChangeListener(handler);
c.addFocusListener(this);
c.addMouseListener(this);
c.addMouseMotionListener(this);
// if the component already has focus, it won't
// be notified.
if (component.hasFocus()) {
focusGained(null);
}
Number ratio = (Number) c.getClientProperty("caretAspectRatio");
if (ratio != null) {
aspectRatio = ratio.floatValue();
} else {
aspectRatio = -1;
}
Integer width = (Integer) c.getClientProperty("caretWidth");
if (width != null) {
caretWidth = width.intValue();
} else {
caretWidth = -1;
}
}
/**
* Called when the UI is being removed from the
* interface of a JTextComponent. This is used to
* unregister any listeners that were attached.
*
* @param c the component
* @see Caret#deinstall
*/
public void deinstall(JTextComponent c) {
c.removeMouseListener(this);
c.removeMouseMotionListener(this);
c.removeFocusListener(this);
c.removePropertyChangeListener(handler);
Document doc = c.getDocument();
if (doc != null) {
doc.removeDocumentListener(handler);
}
synchronized(this) {
component = null;
}
if (flasher != null) {
flasher.stop();
}
}
/**
* Adds a listener to track whenever the caret position has
* been changed.
*
* @param l the listener
* @see Caret#addChangeListener
*/
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
/**
* Removes a listener that was tracking caret position changes.
*
* @param l the listener
* @see Caret#removeChangeListener
*/
public void removeChangeListener(ChangeListener l) {
listenerList.remove(ChangeListener.class, l);
}
/**
* Returns an array of all the change listeners
* registered on this caret.
*
* @return all of this caret's <code>ChangeListener</code>s
* or an empty
* array if no change listeners are currently registered
*
* @see #addChangeListener
* @see #removeChangeListener
*
* @since 1.4
*/
public ChangeListener[] getChangeListeners() {
return (ChangeListener[])listenerList.getListeners(
ChangeListener.class);
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method. The listener list is processed last to first.
*
* @see EventListenerList
*/
protected void fireStateChanged() {
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==ChangeListener.class) {
// Lazily create the event:
if (changeEvent == null)
changeEvent = new ChangeEvent(this);
((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
}
}
}
/**
* Returns an array of all the objects currently registered
* as <code><em>Foo</em>Listener</code>s
* upon this caret.
* <code><em>Foo</em>Listener</code>s are registered using the
* <code>add<em>Foo</em>Listener</code> method.
*
* <p>
*
* You can specify the <code>listenerType</code> argument
* with a class literal,
* such as
* <code><em>Foo</em>Listener.class</code>.
* For example, you can query a
* <code>DefaultCaret</code> <code>c</code>
* for its change listeners with the following code:
*
* <pre>ChangeListener[] cls = (ChangeListener[])(c.getListeners(ChangeListener.class));</pre>
*
* If no such listeners exist, this method returns an empty array.
*
* @param listenerType the type of listeners requested; this parameter
* should specify an interface that descends from
* <code>java.util.EventListener</code>
* @return an array of all objects registered as
* <code><em>Foo</em>Listener</code>s on this component,
* or an empty array if no such
* listeners have been added
* @exception ClassCastException if <code>listenerType</code>
* doesn't specify a class or interface that implements
* <code>java.util.EventListener</code>
*
* @see #getChangeListeners
*
* @since 1.3
*/
public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
return listenerList.getListeners(listenerType);
}
/**
* Changes the selection visibility.
*
* @param vis the new visibility
*/
public void setSelectionVisible(boolean vis) {
if (vis != selectionVisible) {
selectionVisible = vis;
if (selectionVisible) {
// show
Highlighter h = component.getHighlighter();
if ((dot != mark) && (h != null) && (selectionTag == null)) {
int p0 = Math.min(dot, mark);
int p1 = Math.max(dot, mark);
Highlighter.HighlightPainter p = getSelectionPainter();
try {
selectionTag = h.addHighlight(p0, p1, p);
} catch (BadLocationException bl) {
selectionTag = null;
}
}
} else {
// hide
if (selectionTag != null) {
Highlighter h = component.getHighlighter();
h.removeHighlight(selectionTag);
selectionTag = null;
}
}
}
}
/**
* Checks whether the current selection is visible.
*
* @return true if the selection is visible
*/
public boolean isSelectionVisible() {
return selectionVisible;
}
/**
* Determines if the caret is currently active.
* <p>
* This method returns whether or not the <code>Caret</code>
* is currently in a blinking state. It does not provide
* information as to whether it is currently blinked on or off.
* To determine if the caret is currently painted use the
* <code>isVisible</code> method.
*
* @return <code>true</code> if active else <code>false</code>
* @see #isVisible
*
* @since 1.5
*/
public boolean isActive() {
return active;
}
/**
* Indicates whether or not the caret is currently visible. As the
* caret flashes on and off the return value of this will change
* between true, when the caret is painted, and false, when the
* caret is not painted. <code>isActive</code> indicates whether
* or not the caret is in a blinking state, such that it <b>can</b>
* be visible, and <code>isVisible</code> indicates whether or not
* the caret <b>is</b> actually visible.
* <p>
* Subclasses that wish to render a different flashing caret
* should override paint and only paint the caret if this method
* returns true.
*
* @return true if visible else false
* @see Caret#isVisible
* @see #isActive
*/
public boolean isVisible() {
return visible;
}
/**
* Sets the caret visibility, and repaints the caret.
* It is important to understand the relationship between this method,
* <code>isVisible</code> and <code>isActive</code>.
* Calling this method with a value of <code>true</code> activates the
* caret blinking. Setting it to <code>false</code> turns it completely off.
* To determine whether the blinking is active, you should call
* <code>isActive</code>. In effect, <code>isActive</code> is an
* appropriate corresponding "getter" method for this one.
* <code>isVisible</code> can be used to fetch the current
* visibility status of the caret, meaning whether or not it is currently
* painted. This status will change as the caret blinks on and off.
* <p>
* Here's a list showing the potential return values of both
* <code>isActive</code> and <code>isVisible</code>
* after calling this method:
* <p>
* <b><code>setVisible(true)</code></b>:
* <ul>
* <li>isActive(): true</li>
* <li>isVisible(): true or false depending on whether
* or not the caret is blinked on or off</li>
* </ul>
* <p>
* <b><code>setVisible(false)</code></b>:
* <ul>
* <li>isActive(): false</li>
* <li>isVisible(): false</li>
* </ul>
*
* @param e the visibility specifier
* @see #isActive
* @see Caret#setVisible
*/
public void setVisible(boolean e) {
// focus lost notification can come in later after the
// caret has been deinstalled, in which case the component
// will be null.
if (component != null) {
active = e;
TextUI mapper = component.getUI();
if (visible != e) {
visible = e;
// repaint the caret
try {
Rectangle loc = mapper.modelToView(component, dot,dotBias);
damage(loc);
} catch (BadLocationException badloc) {
// hmm... not legally positioned
}
}
}
if (flasher != null) {
if (visible) {
flasher.start();
} else {
flasher.stop();
}
}
}
/**
* Sets the caret blink rate.
*
* @param rate the rate in milliseconds, 0 to stop blinking
* @see Caret#setBlinkRate
*/
public void setBlinkRate(int rate) {
if (rate != 0) {
if (flasher == null) {
flasher = new Timer(rate, handler);
}
flasher.setDelay(rate);
} else {
if (flasher != null) {
flasher.stop();
flasher.removeActionListener(handler);
flasher = null;
}
}
}
/**
* Gets the caret blink rate.
*
* @return the delay in milliseconds. If this is
* zero the caret will not blink.
* @see Caret#getBlinkRate
*/
public int getBlinkRate() {
return (flasher == null) ? 0 : flasher.getDelay();
}
/**
* Fetches the current position of the caret.
*
* @return the position >= 0
* @see Caret#getDot
*/
public int getDot() {
return dot;
}
/**
* Fetches the current position of the mark. If there is a selection,
* the dot and mark will not be the same.
*
* @return the position >= 0
* @see Caret#getMark
*/
public int getMark() {
return mark;
}
/**
* Sets the caret position and mark to the specified position,
* with a forward bias. This implicitly sets the
* selection range to zero.
*
* @param dot the position >= 0
* @see #setDot(int, Position.Bias)
* @see Caret#setDot
*/
public void setDot(int dot) {
setDot(dot, Position.Bias.Forward);
}
/**
* Moves the caret position to the specified position,
* with a forward bias.
*
* @param dot the position >= 0
* @see #moveDot(int, javax.swing.text.Position.Bias)
* @see Caret#moveDot
*/
public void moveDot(int dot) {
moveDot(dot, Position.Bias.Forward);
}
// ---- Bidi methods (we could put these in a subclass)
/**
* Moves the caret position to the specified position, with the
* specified bias.
*
* @param dot the position >= 0
* @param dotBias the bias for this position, not <code>null</code>
* @throws IllegalArgumentException if the bias is <code>null</code>
* @see Caret#moveDot
* @since 1.6
*/
public void moveDot(int dot, Position.Bias dotBias) {
if (dotBias == null) {
throw new IllegalArgumentException("null bias");
}
if (! component.isEnabled()) {
// don't allow selection on disabled components.
setDot(dot, dotBias);
return;
}
if (dot != this.dot) {
NavigationFilter filter = component.getNavigationFilter();
if (filter != null) {
filter.moveDot(getFilterBypass(), dot, dotBias);
}
else {
handleMoveDot(dot, dotBias);
}
}
}
void handleMoveDot(int dot, Position.Bias dotBias) {
changeCaretPosition(dot, dotBias);
if (selectionVisible) {
Highlighter h = component.getHighlighter();
if (h != null) {
int p0 = Math.min(dot, mark);
int p1 = Math.max(dot, mark);
// if p0 == p1 then there should be no highlight, remove it if necessary
if (p0 == p1) {
if (selectionTag != null) {
h.removeHighlight(selectionTag);
selectionTag = null;
}
// otherwise, change or add the highlight
} else {
try {
if (selectionTag != null) {
h.changeHighlight(selectionTag, p0, p1);
} else {
Highlighter.HighlightPainter p = getSelectionPainter();
selectionTag = h.addHighlight(p0, p1, p);
}
} catch (BadLocationException e) {
throw new StateInvariantError("Bad caret position");
}
}
}
}
}
/**
* Sets the caret position and mark to the specified position, with the
* specified bias. This implicitly sets the selection range
* to zero.
*
* @param dot the position >= 0
* @param dotBias the bias for this position, not <code>null</code>
* @throws IllegalArgumentException if the bias is <code>null</code>
* @see Caret#setDot
* @since 1.6
*/
public void setDot(int dot, Position.Bias dotBias) {
if (dotBias == null) {
throw new IllegalArgumentException("null bias");
}
NavigationFilter filter = component.getNavigationFilter();
if (filter != null) {
filter.setDot(getFilterBypass(), dot, dotBias);
}
else {
handleSetDot(dot, dotBias);
}
}
void handleSetDot(int dot, Position.Bias dotBias) {
// move dot, if it changed
Document doc = component.getDocument();
if (doc != null) {
dot = Math.min(dot, doc.getLength());
}
dot = Math.max(dot, 0);
// The position (0,Backward) is out of range so disallow it.
if( dot == 0 )
dotBias = Position.Bias.Forward;
mark = dot;
if (this.dot != dot || this.dotBias != dotBias ||
selectionTag != null || forceCaretPositionChange) {
changeCaretPosition(dot, dotBias);
}
this.markBias = this.dotBias;
this.markLTR = dotLTR;
Highlighter h = component.getHighlighter();
if ((h != null) && (selectionTag != null)) {
h.removeHighlight(selectionTag);
selectionTag = null;
}
}
/**
* Returns the bias of the caret position.
*
* @return the bias of the caret position
* @since 1.6
*/
public Position.Bias getDotBias() {
return dotBias;
}
/**
* Returns the bias of the mark.
*
* @return the bias of the mark
* @since 1.6
*/
public Position.Bias getMarkBias() {
return markBias;
}
boolean isDotLeftToRight() {
return dotLTR;
}
boolean isMarkLeftToRight() {
return markLTR;
}
boolean isPositionLTR(int position, Position.Bias bias) {
Document doc = component.getDocument();
if(doc instanceof AbstractDocument ) {
if(bias == Position.Bias.Backward && --position < 0)
position = 0;
return ((AbstractDocument)doc).isLeftToRight(position, position);
}
return true;
}
Position.Bias guessBiasForOffset(int offset, Position.Bias lastBias,
boolean lastLTR) {
// There is an abiguous case here. That if your model looks like:
// abAB with the cursor at abB]A (visual representation of
// 3 forward) deleting could either become abB] or
// ab[B. I'ld actually prefer abB]. But, if I implement that
// a delete at abBA] would result in aBA] vs a[BA which I
// think is totally wrong. To get this right we need to know what
// was deleted. And we could get this from the bidi structure
// in the change event. So:
// PENDING: base this off what was deleted.
if(lastLTR != isPositionLTR(offset, lastBias)) {
lastBias = Position.Bias.Backward;
}
else if(lastBias != Position.Bias.Backward &&
lastLTR != isPositionLTR(offset, Position.Bias.Backward)) {
lastBias = Position.Bias.Backward;
}
if (lastBias == Position.Bias.Backward && offset > 0) {
try {
Segment s = new Segment();
component.getDocument().getText(offset - 1, 1, s);
if (s.count > 0 && s.array[s.offset] == '\n') {
lastBias = Position.Bias.Forward;
}
}
catch (BadLocationException ble) {}
}
return lastBias;
}
// ---- local methods --------------------------------------------
/**
* Sets the caret position (dot) to a new location. This
* causes the old and new location to be repainted. It
* also makes sure that the caret is within the visible
* region of the view, if the view is scrollable.
*/
void changeCaretPosition(int dot, Position.Bias dotBias) {
// repaint the old position and set the new value of
// the dot.
repaint();
// Make sure the caret is visible if this window has the focus.
if (flasher != null && flasher.isRunning()) {
visible = true;
flasher.restart();
}
// notify listeners at the caret moved
this.dot = dot;
this.dotBias = dotBias;
dotLTR = isPositionLTR(dot, dotBias);
fireStateChanged();
updateSystemSelection();
setMagicCaretPosition(null);
// We try to repaint the caret later, since things
// may be unstable at the time this is called
// (i.e. we don't want to depend upon notification
// order or the fact that this might happen on
// an unsafe thread).
Runnable callRepaintNewCaret = new Runnable() {
public void run() {
repaintNewCaret();
}
};
SwingUtilities.invokeLater(callRepaintNewCaret);
}
/**
* Repaints the new caret position, with the
* assumption that this is happening on the
* event thread so that calling <code>modelToView</code>
* is safe.
*/
void repaintNewCaret() {
if (component != null) {
TextUI mapper = component.getUI();
Document doc = component.getDocument();
if ((mapper != null) && (doc != null)) {
// determine the new location and scroll if
// not visible.
Rectangle newLoc;
try {
newLoc = mapper.modelToView(component, this.dot, this.dotBias);
} catch (BadLocationException e) {
newLoc = null;
}
if (newLoc != null) {
adjustVisibility(newLoc);
// If there is no magic caret position, make one
if (getMagicCaretPosition() == null) {
setMagicCaretPosition(new Point(newLoc.x, newLoc.y));
}
}
// repaint the new position
damage(newLoc);
}
}
}
private void updateSystemSelection() {
if ( ! SwingUtilities2.canCurrentEventAccessSystemClipboard() ) {
return;
}
if (this.dot != this.mark && component != null) {
Clipboard clip = getSystemSelection();
if (clip != null) {
String selectedText = null;
if (component instanceof JPasswordField
&& component.getClientProperty("JPasswordField.cutCopyAllowed") !=
Boolean.TRUE) {
//fix for 4793761
StringBuffer txt = null;
char echoChar = ((JPasswordField)component).getEchoChar();
int p0 = Math.min(getDot(), getMark());
int p1 = Math.max(getDot(), getMark());
for (int i = p0; i < p1; i++) {
if (txt == null) {
txt = new StringBuffer();
}
txt.append(echoChar);
}
selectedText = (txt != null) ? txt.toString() : null;
} else {
selectedText = component.getSelectedText();
}
try {
clip.setContents(
new StringSelection(selectedText), getClipboardOwner());
ownsSelection = true;
} catch (IllegalStateException ise) {
// clipboard was unavailable
// no need to provide error feedback to user since updating
// the system selection is not a user invoked action
}
}
}
}
private Clipboard getSystemSelection() {
try {
return component.getToolkit().getSystemSelection();
} catch (HeadlessException he) {
// do nothing... there is no system clipboard
} catch (SecurityException se) {
// do nothing... there is no allowed system clipboard
}
return null;
}
private ClipboardOwner getClipboardOwner() {
return handler;
}
/**
* This is invoked after the document changes to verify the current
* dot/mark is valid. We do this in case the <code>NavigationFilter</code>
* changed where to position the dot, that resulted in the current location
* being bogus.
*/
private void ensureValidPosition() {
int length = component.getDocument().getLength();
if (dot > length || mark > length) {
// Current location is bogus and filter likely vetoed the
// change, force the reset without giving the filter a
// chance at changing it.
handleSetDot(length, Position.Bias.Forward);
}
}
/**
* Saves the current caret position. This is used when
* caret up/down actions occur, moving between lines
* that have uneven end positions.
*
* @param p the position
* @see #getMagicCaretPosition
*/
public void setMagicCaretPosition(Point p) {
magicCaretPosition = p;
}
/**
* Gets the saved caret position.
*
* @return the position
* see #setMagicCaretPosition
*/
public Point getMagicCaretPosition() {
return magicCaretPosition;
}
/**
* Compares this object to the specified object.
* The superclass behavior of comparing rectangles
* is not desired, so this is changed to the Object
* behavior.
*
* @param obj the object to compare this font with
* @return <code>true</code> if the objects are equal;
* <code>false</code> otherwise
*/
public boolean equals(Object obj) {
return (this == obj);
}
public String toString() {
String s = "Dot=(" + dot + ", " + dotBias + ")";
s += " Mark=(" + mark + ", " + markBias + ")";
return s;
}
private NavigationFilter.FilterBypass getFilterBypass() {
if (filterBypass == null) {
filterBypass = new DefaultFilterBypass();
}
return filterBypass;
}
// Rectangle.contains returns false if passed a rect with a w or h == 0,
// this won't (assuming X,Y are contained with this rectangle).
private boolean _contains(int X, int Y, int W, int H) {
int w = this.width;
int h = this.height;
if ((w | h | W | H) < 0) {
// At least one of the dimensions is negative...
return false;
}
// Note: if any dimension is zero, tests below must return false...
int x = this.x;
int y = this.y;
if (X < x || Y < y) {
return false;
}
if (W > 0) {
w += x;
W += X;
if (W <= X) {
// X+W overflowed or W was zero, return false if...
// either original w or W was zero or
// x+w did not overflow or
// the overflowed x+w is smaller than the overflowed X+W
if (w >= x || W > w) return false;
} else {
// X+W did not overflow and W was not zero, return false if...
// original w was zero or
// x+w did not overflow and x+w is smaller than X+W
if (w >= x && W > w) return false;
}
}
else if ((x + w) < X) {
return false;
}
if (H > 0) {
h += y;
H += Y;
if (H <= Y) {
if (h >= y || H > h) return false;
} else {
if (h >= y && H > h) return false;
}
}
else if ((y + h) < Y) {
return false;
}
return true;
}
int getCaretWidth(int height) {
if (aspectRatio > -1) {
return (int) (aspectRatio * height) + 1;
}
if (caretWidth > -1) {
return caretWidth;
}
return 1;
}
// --- serialization ---------------------------------------------
private void readObject(ObjectInputStream s)
throws ClassNotFoundException, IOException
{
s.defaultReadObject();
handler = new Handler();
if (!s.readBoolean()) {
dotBias = Position.Bias.Forward;
}
else {
dotBias = Position.Bias.Backward;
}
if (!s.readBoolean()) {
markBias = Position.Bias.Forward;
}
else {
markBias = Position.Bias.Backward;
}
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeBoolean((dotBias == Position.Bias.Backward));
s.writeBoolean((markBias == Position.Bias.Backward));
}
// ---- member variables ------------------------------------------
/**
* The event listener list.
*/
protected EventListenerList listenerList = new EventListenerList();
/**
* The change event for the model.
* Only one ChangeEvent is needed per model instance since the
* event's only (read-only) state is the source property. The source
* of events generated here is always "this".
*/
protected transient ChangeEvent changeEvent = null;
// package-private to avoid inner classes private member
// access bug
JTextComponent component;
int updatePolicy = UPDATE_WHEN_ON_EDT;
boolean visible;
boolean active;
int dot;
int mark;
Object selectionTag;
boolean selectionVisible;
Timer flasher;
Point magicCaretPosition;
transient Position.Bias dotBias;
transient Position.Bias markBias;
boolean dotLTR;
boolean markLTR;
transient Handler handler = new Handler();
transient private int[] flagXPoints = new int[3];
transient private int[] flagYPoints = new int[3];
private transient NavigationFilter.FilterBypass filterBypass;
static private transient Action selectWord = null;
static private transient Action selectLine = null;
/**
* This is used to indicate if the caret currently owns the selection.
* This is always false if the system does not support the system
* clipboard.
*/
private boolean ownsSelection;
/**
* If this is true, the location of the dot is updated regardless of
* the current location. This is set in the DocumentListener
* such that even if the model location of dot hasn't changed (perhaps do
* to a forward delete) the visual location is updated.
*/
private boolean forceCaretPositionChange;
/**
* Whether or not mouseReleased should adjust the caret and focus.
* This flag is set by mousePressed if it wanted to adjust the caret
* and focus but couldn't because of a possible DnD operation.
*/
private transient boolean shouldHandleRelease;
/**
* holds last MouseEvent which caused the word selection
*/
private transient MouseEvent selectedWordEvent = null;
/**
* The width of the caret in pixels.
*/
private int caretWidth = -1;
private float aspectRatio = -1;
class SafeScroller implements Runnable {
SafeScroller(Rectangle r) {
this.r = r;
}
public void run() {
if (component != null) {
component.scrollRectToVisible(r);
}
}
Rectangle r;
}
class Handler implements PropertyChangeListener, DocumentListener, ActionListener, ClipboardOwner {
// --- ActionListener methods ----------------------------------
/**
* Invoked when the blink timer fires. This is called
* asynchronously. The simply changes the visibility
* and repaints the rectangle that last bounded the caret.
*
* @param e the action event
*/
public void actionPerformed(ActionEvent e) {
if (width == 0 || height == 0) {
// setVisible(true) will cause a scroll, only do this if the
// new location is really valid.
if (component != null) {
TextUI mapper = component.getUI();
try {
Rectangle r = mapper.modelToView(component, dot,
dotBias);
if (r != null && r.width != 0 && r.height != 0) {
damage(r);
}
} catch (BadLocationException ble) {
}
}
}
visible = !visible;
repaint();
}
// --- DocumentListener methods --------------------------------
/**
* Updates the dot and mark if they were changed by
* the insertion.
*
* @param e the document event
* @see DocumentListener#insertUpdate
*/
public void insertUpdate(DocumentEvent e) {
if (getUpdatePolicy() == NEVER_UPDATE ||
(getUpdatePolicy() == UPDATE_WHEN_ON_EDT &&
!SwingUtilities.isEventDispatchThread())) {
if ((e.getOffset() <= dot || e.getOffset() <= mark)
&& selectionTag != null) {
try {
component.getHighlighter().changeHighlight(selectionTag,
Math.min(dot, mark), Math.max(dot, mark));
} catch (BadLocationException e1) {
e1.printStackTrace();
}
}
return;
}
int adjust = 0;
int offset = e.getOffset();
int length = e.getLength();
int newDot = dot;
short changed = 0;
if (e instanceof AbstractDocument.UndoRedoDocumentEvent) {
setDot(offset + length);
return;
}
if (newDot >= offset) {
newDot += length;
changed |= 1;
}
int newMark = mark;
if (newMark >= offset) {
newMark += length;
changed |= 2;
}
if (changed != 0) {
Position.Bias dotBias = DefaultCaret.this.dotBias;
if (dot == offset) {
Document doc = component.getDocument();
boolean isNewline;
try {
Segment s = new Segment();
doc.getText(newDot - 1, 1, s);
isNewline = (s.count > 0 &&
s.array[s.offset] == '\n');
} catch (BadLocationException ble) {
isNewline = false;
}
if (isNewline) {
dotBias = Position.Bias.Forward;
} else {
dotBias = Position.Bias.Backward;
}
}
if (newMark == newDot) {
setDot(newDot, dotBias);
ensureValidPosition();
}
else {
setDot(newMark, markBias);
if (getDot() == newMark) {
// Due this test in case the filter vetoed the
// change in which case this probably won't be
// valid either.
moveDot(newDot, dotBias);
}
ensureValidPosition();
}
}
}
/**
* Updates the dot and mark if they were changed
* by the removal.
*
* @param e the document event
* @see DocumentListener#removeUpdate
*/
public void removeUpdate(DocumentEvent e) {
if (getUpdatePolicy() == NEVER_UPDATE ||
(getUpdatePolicy() == UPDATE_WHEN_ON_EDT &&
!SwingUtilities.isEventDispatchThread())) {
int length = component.getDocument().getLength();
dot = Math.min(dot, length);
mark = Math.min(mark, length);
if ((e.getOffset() < dot || e.getOffset() < mark)
&& selectionTag != null) {
try {
component.getHighlighter().changeHighlight(selectionTag,
Math.min(dot, mark), Math.max(dot, mark));
} catch (BadLocationException e1) {
e1.printStackTrace();
}
}
return;
}
int offs0 = e.getOffset();
int offs1 = offs0 + e.getLength();
int adjust = 0;
int newDot = dot;
boolean adjustDotBias = false;
int newMark = mark;
boolean adjustMarkBias = false;
if(e instanceof AbstractDocument.UndoRedoDocumentEvent) {
setDot(offs0);
return;
}
if (newDot >= offs1) {
newDot -= (offs1 - offs0);
if(newDot == offs1) {
adjustDotBias = true;
}
} else if (newDot >= offs0) {
newDot = offs0;
adjustDotBias = true;
}
if (newMark >= offs1) {
newMark -= (offs1 - offs0);
if(newMark == offs1) {
adjustMarkBias = true;
}
} else if (newMark >= offs0) {
newMark = offs0;
adjustMarkBias = true;
}
if (newMark == newDot) {
forceCaretPositionChange = true;
try {
setDot(newDot, guessBiasForOffset(newDot, dotBias,
dotLTR));
} finally {
forceCaretPositionChange = false;
}
ensureValidPosition();
} else {
Position.Bias dotBias = DefaultCaret.this.dotBias;
Position.Bias markBias = DefaultCaret.this.markBias;
if(adjustDotBias) {
dotBias = guessBiasForOffset(newDot, dotBias, dotLTR);
}
if(adjustMarkBias) {
markBias = guessBiasForOffset(mark, markBias, markLTR);
}
setDot(newMark, markBias);
if (getDot() == newMark) {
// Due this test in case the filter vetoed the change
// in which case this probably won't be valid either.
moveDot(newDot, dotBias);
}
ensureValidPosition();
}
}
/**
* Gives notification that an attribute or set of attributes changed.
*
* @param e the document event
* @see DocumentListener#changedUpdate
*/
public void changedUpdate(DocumentEvent e) {
if (getUpdatePolicy() == NEVER_UPDATE ||
(getUpdatePolicy() == UPDATE_WHEN_ON_EDT &&
!SwingUtilities.isEventDispatchThread())) {
return;
}
if(e instanceof AbstractDocument.UndoRedoDocumentEvent) {
setDot(e.getOffset() + e.getLength());
}
}
// --- PropertyChangeListener methods -----------------------
/**
* This method gets called when a bound property is changed.
* We are looking for document changes on the editor.
*/
public void propertyChange(PropertyChangeEvent evt) {
Object oldValue = evt.getOldValue();
Object newValue = evt.getNewValue();
if ((oldValue instanceof Document) || (newValue instanceof Document)) {
setDot(0);
if (oldValue != null) {
((Document)oldValue).removeDocumentListener(this);
}
if (newValue != null) {
((Document)newValue).addDocumentListener(this);
}
} else if("enabled".equals(evt.getPropertyName())) {
Boolean enabled = (Boolean) evt.getNewValue();
if(component.isFocusOwner()) {
if(enabled == Boolean.TRUE) {
if(component.isEditable()) {
setVisible(true);
}
setSelectionVisible(true);
} else {
setVisible(false);
setSelectionVisible(false);
}
}
} else if("caretWidth".equals(evt.getPropertyName())) {
Integer newWidth = (Integer) evt.getNewValue();
if (newWidth != null) {
caretWidth = newWidth.intValue();
} else {
caretWidth = -1;
}
repaint();
} else if("caretAspectRatio".equals(evt.getPropertyName())) {
Number newRatio = (Number) evt.getNewValue();
if (newRatio != null) {
aspectRatio = newRatio.floatValue();
} else {
aspectRatio = -1;
}
repaint();
}
}
//
// ClipboardOwner
//
/**
* Toggles the visibility of the selection when ownership is lost.
*/
public void lostOwnership(Clipboard clipboard,
Transferable contents) {
if (ownsSelection) {
ownsSelection = false;
if (component != null && !component.hasFocus()) {
setSelectionVisible(false);
}
}
}
}
private class DefaultFilterBypass extends NavigationFilter.FilterBypass {
public Caret getCaret() {
return DefaultCaret.this;
}
public void setDot(int dot, Position.Bias bias) {
handleSetDot(dot, bias);
}
public void moveDot(int dot, Position.Bias bias) {
handleMoveDot(dot, bias);
}
}
}